Skip to content
ecdsa.com

Sign a message · Java

How to sign a message with ECDSA in Java

To sign a message with ECDSA in Java you need only the JDK: load the PKCS#8 private key with KeyFactory, then let java.security.Signature hash and sign in one call. The algorithm name decides the output format — SHA256withECDSA returns ASN.1 DER for OpenSSL-style verifiers, SHA256withECDSAinP1363Format returns the 64-byte r‖s that JWS and WebCrypto expect.

Tested with OpenJDK 25.0.4.1 (Eclipse Temurin 25.0.4.1+1, SunEC provider) and OpenSSL 3.0.13 (openssl dgst cross-check).

Sign.java — SHA-256 with ECDSA, DER output
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
public class Sign {
public static void main(String[] args) throws Exception {
// "BEGIN PRIVATE KEY" is PKCS#8; the JDK has no reader for "BEGIN EC PRIVATE KEY"
String pem = Files.readString(Path.of("key.pem"));
byte[] pkcs8 = Base64.getMimeDecoder().decode(pem.replaceAll("-----[^-]+-----", ""));
PrivateKey privateKey = KeyFactory.getInstance("EC").generatePrivate(new PKCS8EncodedKeySpec(pkcs8));
Signature signer = Signature.getInstance("SHA256withECDSA");
signer.initSign(privateKey); // nonce drawn from the default SecureRandom
signer.update(Files.readAllBytes(Path.of("message.txt")));
byte[] der = signer.sign(); // ASN.1 DER
Files.write(Path.of("sig.der"), der);
System.out.println("DER signature: " + der.length + " bytes"); // at most 72 on P-256
}
}
raw r‖s output for JWS and WebCrypto consumers
Signature p1363 = Signature.getInstance("SHA256withECDSAinP1363Format");
p1363.initSign(privateKey);
p1363.update(Files.readAllBytes(Path.of("message.txt")));
byte[] raw = p1363.sign(); // r‖s, always 64 bytes on P-256
Files.write(Path.of("sig.raw"), raw);
System.out.println("raw signature: " + raw.length + " bytes");

How it works

  1. PKCS8EncodedKeySpec reads the base64 body of a BEGIN PRIVATE KEY block; the curve comes from the key, so the same code signs with P-256, P-384 or P-521 keys.
  2. initSign(privateKey) seeds the per-signature nonce from the default SecureRandom — there is nothing to configure.
  3. update takes the message itself. SHA256withECDSA computes the SHA-256 digest internally; for a digest computed elsewhere use NONEwithECDSA, which signs the bytes it is given as the digest.
  4. sign() returns DER for SHA256withECDSA and fixed-length r‖s for SHA256withECDSAinP1363Format. The run above checks the DER output with openssl dgst -verify and the raw output with the P1363 verifier.

Gotchas

  • Every call produces different bytes: the JDK draws a random nonce and does not implement RFC 6979 deterministic signing. Tests must verify signatures, never compare them with a stored value.
  • Hashing the message yourself and passing the digest to SHA256withECDSA hashes twice. The result is a valid signature over the digest — which no verifier given the original message will accept.
  • A BEGIN EC PRIVATE KEY (SEC 1) file fails with InvalidKeySpecException caused by algid parse error, not a sequence. Convert it once with openssl pkcs8 -topk8 -nocrypt -in ec.pem -out key.pem.
  • The JDK does not normalize s: about half of its signatures are high-S. Generic verifiers accept both forms, but Bitcoin-style verifiers that require low-S reject them — replace s with n − s when s > n/2 before handing the signature over.

Related recipes