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)); }} 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
KeyPairGenerator.getInstance("ML-DSA-65").generateKeyPair()returns keys whosegetEncoded()is the SPKI (1974 bytes, formatX.509) and a seed-only PKCS#8 (54 bytes).Signature.getInstance("ML-DSA")withinitSign,updateandsignproduces a 3309-byte signature;initVerifyandverifycheck it.KeyFactory.getInstance("ML-DSA")withX509EncodedKeySpecreads OpenSSL'sBEGIN PUBLIC KEYfile; withPKCS8EncodedKeySpecit reads the seed-only private key file.- 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:
setParameterthrowsInvalidAlgorithmParameterException: 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 throwsInvalidKeyExceptionfor 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.