import java.io.Fileimport java.security.KeyFactoryimport java.security.Signatureimport java.security.spec.PKCS8EncodedKeySpecimport java.util.Base64import java.util.HexFormat fun main() { val pem = File("key.pem").readText() val pkcs8 = Base64.getMimeDecoder().decode(pem.replace(Regex("-----[^-]+-----"), "")) val privateKey = KeyFactory.getInstance("Ed25519").generatePrivate(PKCS8EncodedKeySpec(pkcs8)) val signature = Signature.getInstance("Ed25519").run { initSign(privateKey) update(File("message.txt").readBytes()) // the message itself: Ed25519 hashes internally sign() } File("sig.bin").writeBytes(signature) println("signature: ${HexFormat.of().formatHex(signature)}")}import java.io.Fileimport java.security.KeyFactoryimport java.security.PublicKeyimport java.security.Signatureimport java.security.spec.X509EncodedKeySpecimport java.util.Base64 fun isValid(key: PublicKey, message: ByteArray, signature: ByteArray): Boolean = Signature.getInstance("Ed25519").run { initVerify(key) update(message) verify(signature) // false for a wrong signature, no exception } fun main() { val pem = File("pub.pem").readText() val spki = Base64.getMimeDecoder().decode(pem.replace(Regex("-----[^-]+-----"), "")) val publicKey = KeyFactory.getInstance("Ed25519").generatePublic(X509EncodedKeySpec(spki)) val message = File("message.txt").readBytes() val signature = File("sig.bin").readBytes() println("valid: ${isValid(publicKey, message, signature)}") println("tampered message valid: ${isValid(publicKey, "hello ed25520".toByteArray(), signature)}")}How it works
- The PEM body is base64 of the DER key: the regex strips the
-----BEGIN …-----armour, andBase64.getMimeDecoder()tolerates the line breaks. Signature.getInstance("Ed25519").run { … }keepsinitSign,updateandsigntogether; the message goes in unhashed.verifyreturnsfalsefor a wrong signature — there is no exception to catch.- The run compiles both files with
kotlinc2.4.21 into one jar and prints the same signature as every other recipe of this task.
Gotchas
- There is no hash-prefixed algorithm:
Signature.getInstance("SHA256withEd25519")throwsNoSuchAlgorithmException. KeyFactory.getInstance("EC")rejects Ed25519 keys withInvalidKeySpecException: java.security.InvalidKeyException: Invalid EC private key; Ed25519 has its own factory.String.toByteArray()defaults to UTF-8 in Kotlin, while Java'sgetBytes()used the platform charset before JDK 18. When one side is older Java code, make the encoding explicit on both sides, or non-ASCII messages will not verify.- Raw 32-byte public keys (JWK
x, libsodium) need the 12-byte SPKI header302a300506032b6570032100in front beforeX509EncodedKeySpecaccepts them.