Skip to content
ecdsa.com

ML-DSA-65 sign/verify · Java

How to sign and verify with ML-DSA-65 in Java

To sign and verify with ML-DSA-65 in Java, use the JDK's built-in provider (JDK 24 and later, JEP 497): KeyPairGenerator.getInstance("ML-DSA-65") creates the key and Signature.getInstance("ML-DSA") signs and verifies. No extra library is needed. This recipe also reads an OpenSSL 3.5 public key from PEM and verifies a signature OpenSSL made.

Tested with OpenJDK 25.0.4.1 (Eclipse Temurin 25.0.4.1+1, SUN provider (JEP 497)).

MlDsaSign.java — key pair, signature, verification
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
public class MlDsaSign {
public static void main(String[] args) throws Exception {
KeyPair keys = KeyPairGenerator.getInstance("ML-DSA-65").generateKeyPair(); // JDK 24+
byte[] message = "hello, post-quantum world".getBytes(StandardCharsets.UTF_8);
Signature signer = Signature.getInstance("ML-DSA"); // accepts ML-DSA-44, -65 and -87 keys
signer.initSign(keys.getPrivate());
signer.update(message);
byte[] signature = signer.sign(); // randomized (hedged) signing
Signature verifier = Signature.getInstance("ML-DSA");
verifier.initVerify(keys.getPublic());
verifier.update(message);
System.out.println("public key " + keys.getPublic().getEncoded().length + " bytes ("
+ keys.getPublic().getFormat() + "), private key " + keys.getPrivate().getEncoded().length
+ " bytes (" + keys.getPrivate().getFormat() + "), signature " + signature.length + " bytes");
System.out.println("verified: " + verifier.verify(signature));
verifier.initVerify(keys.getPublic());
verifier.update("hello, post-quantum world!".getBytes(StandardCharsets.UTF_8));
System.out.println("tampered message: " + verifier.verify(signature));
}
}
MlDsaPem.java — read an SPKI public key file and verify
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public final class MlDsaPem {
private MlDsaPem() {}
/** An ML-DSA public key from an SPKI ("BEGIN PUBLIC KEY") file, e.g. written by OpenSSL 3.5. */
public static PublicKey readPublicKey(Path path) throws Exception {
String pem = Files.readString(path).replaceAll("-----[^-]+-----", "");
byte[] spki = Base64.getMimeDecoder().decode(pem);
return KeyFactory.getInstance("ML-DSA").generatePublic(new X509EncodedKeySpec(spki));
}
public static boolean verify(PublicKey key, byte[] message, byte[] signature) throws Exception {
Signature verifier = Signature.getInstance("ML-DSA");
verifier.initVerify(key);
verifier.update(message);
return verifier.verify(signature);
}
}

How it works

  1. KeyPairGenerator.getInstance("ML-DSA-65").generateKeyPair() returns keys whose getEncoded() is the SPKI (1974 bytes, format X.509) and a seed-only PKCS#8 (54 bytes).
  2. Signature.getInstance("ML-DSA") with initSign, update and sign produces a 3309-byte signature; initVerify and verify check it.
  3. KeyFactory.getInstance("ML-DSA") with X509EncodedKeySpec reads OpenSSL's BEGIN PUBLIC KEY file; with PKCS8EncodedKeySpec it reads the seed-only private key file.
  4. The run verifies OpenSSL's signature and checks that the shared seed file signs for OpenSSL's public key.

Gotchas

  • The JDK has no API for the context string: setParameter throws InvalidAlgorithmParameterException: The ML-DSA algorithm does not take any parameters. Signatures made with a context elsewhere cannot be verified with the JDK provider.
  • Signature.getInstance("ML-DSA") accepts keys of every parameter set; "ML-DSA-65" accepts only ML-DSA-65 keys and throws InvalidKeyException for an ML-DSA-44 key. Use the specific name when the parameter set is part of your protocol.
  • Signing is randomized: two signatures of the same message differ.

Related recipes