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;import java.util.HexFormat; public class Sign { public static void main(String[] args) throws Exception { String pem = Files.readString(Path.of("key.pem")); byte[] pkcs8 = Base64.getMimeDecoder().decode(pem.replaceAll("-----[^-]+-----", "")); PrivateKey privateKey = KeyFactory.getInstance("Ed25519").generatePrivate(new PKCS8EncodedKeySpec(pkcs8)); Signature signer = Signature.getInstance("Ed25519"); // no "SHA…with": the hash is part of Ed25519 signer.initSign(privateKey); signer.update(Files.readAllBytes(Path.of("message.txt"))); byte[] signature = signer.sign(); Files.write(Path.of("sig.bin"), signature); System.out.println("signature: " + HexFormat.of().formatHex(signature)); }} import java.nio.charset.StandardCharsets;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 class Verify { static boolean verify(PublicKey key, byte[] message, byte[] signature) throws Exception { Signature verifier = Signature.getInstance("Ed25519"); verifier.initVerify(key); verifier.update(message); return verifier.verify(signature); // false for a wrong signature, no exception } public static void main(String[] args) throws Exception { String pem = Files.readString(Path.of("pub.pem")); byte[] spki = Base64.getMimeDecoder().decode(pem.replaceAll("-----[^-]+-----", "")); PublicKey publicKey = KeyFactory.getInstance("Ed25519").generatePublic(new X509EncodedKeySpec(spki)); byte[] message = Files.readAllBytes(Path.of("message.txt")); byte[] signature = Files.readAllBytes(Path.of("sig.bin")); System.out.println("valid: " + verify(publicKey, message, signature)); System.out.println("tampered message valid: " + verify(publicKey, "hello ed25520".getBytes(StandardCharsets.UTF_8), signature)); }} How it works
PKCS8EncodedKeySpectakes the decoded body ofBEGIN PRIVATE KEY;X509EncodedKeySpectakes the body ofBEGIN PUBLIC KEY(the JDK calls SPKI the "X.509 encoding").Signature.getInstance("Ed25519")— or the family name"EdDSA"— hashes with SHA-512 internally, soupdatereceives the plain message bytes.sign()returns the 64-byte signature;verify()returnsfalsefor a wrong signature instead of throwing.- Both files run directly with
java Sign.java(single-file source launch, no build step), and the output matches every other recipe of this task.
Gotchas
- There is no hash-prefixed name:
Signature.getInstance("SHA256withEd25519")throwsNoSuchAlgorithmException: SHA256withEd25519 Signature not available. - The EC factory does not read Ed25519 keys:
KeyFactory.getInstance("EC")throwsInvalidKeySpecException: java.security.InvalidKeyException: Invalid EC private key. In JCA, Ed25519 is an EdEC key — use"Ed25519"or"EdDSA". - A raw 32-byte seed loads with
new EdECPrivateKeySpec(NamedParameterSpec.ED25519, seed), but a raw public key needs anEdECPoint(a little-endian y plus the x-parity bit). Prepending the 12 bytes302a300506032b6570032100and usingX509EncodedKeySpecis simpler and harder to get wrong. - Java 8 and 11 have no Ed25519 in the JDK; there, use Bouncy Castle's
Ed25519Signeror Google Tink.