From 18e7f216f90d8882fe6da62c33382552f7b03437 Mon Sep 17 00:00:00 2001 From: Kenny Root Date: Sun, 4 Jan 2026 17:34:55 -0800 Subject: [PATCH] Update Ed25519 decoding for other providers Now that other providers are using Ed25519, we need to make sure we are compatible with them. Use SimpleDERReader for the parsing instead of hand-writing the types comparison. Also add compatibility for the older Ed25519 keys that were just 32-byte seed values from this library. This shouldn't conflict with the new PKCS#8 format since that is always >=48 bytes. --- .../trilead/ssh2/crypto/PublicKeyUtils.java | 36 +++--- .../ssh2/crypto/keys/Ed25519PrivateKey.java | 56 +++++---- .../ssh2/crypto/keys/Ed25519PublicKey.java | 48 ++++--- .../ssh2/crypto/PublicKeyUtilsTest.java | 42 +++++++ .../crypto/keys/Ed25519KeyFactoryTest.java | 118 ++++++++++++++++++ .../ssh2/signature/Ed25519VerifyTest.java | 40 ++++++ 6 files changed, 282 insertions(+), 58 deletions(-) diff --git a/src/main/java/com/trilead/ssh2/crypto/PublicKeyUtils.java b/src/main/java/com/trilead/ssh2/crypto/PublicKeyUtils.java index 9a72f304..8db5ea76 100644 --- a/src/main/java/com/trilead/ssh2/crypto/PublicKeyUtils.java +++ b/src/main/java/com/trilead/ssh2/crypto/PublicKeyUtils.java @@ -42,31 +42,32 @@ public static String toAuthorizedKeysFormat(PublicKey publicKey, String comment) comment = ""; } + String keyType; + byte[] encoded; + if (publicKey instanceof RSAPublicKey) { RSAPublicKey rsaKey = (RSAPublicKey) publicKey; - byte[] encoded = RSASHA1Verify.get().encodePublicKey(rsaKey); - String data = "ssh-rsa " + new String(Base64.encode(encoded)); - return comment.isEmpty() ? data : data + " " + comment; + keyType = "ssh-rsa"; + encoded = RSASHA1Verify.get().encodePublicKey(rsaKey); } else if (publicKey instanceof DSAPublicKey) { DSAPublicKey dsaKey = (DSAPublicKey) publicKey; - byte[] encoded = DSASHA1Verify.get().encodePublicKey(dsaKey); - String data = "ssh-dss " + new String(Base64.encode(encoded)); - return comment.isEmpty() ? data : data + " " + comment; + keyType = "ssh-dss"; + encoded = DSASHA1Verify.get().encodePublicKey(dsaKey); } else if (publicKey instanceof ECPublicKey) { ECPublicKey ecKey = (ECPublicKey) publicKey; - String keyType = ECDSASHA2Verify.getSshKeyType(ecKey); + keyType = ECDSASHA2Verify.getSshKeyType(ecKey); SSHSignature verifier = ECDSASHA2Verify.getVerifierForKey(ecKey); - byte[] encoded = verifier.encodePublicKey(ecKey); - String data = keyType + " " + new String(Base64.encode(encoded)); - return comment.isEmpty() ? data : data + " " + comment; - } else if (publicKey instanceof Ed25519PublicKey) { - Ed25519PublicKey ed25519Key = (Ed25519PublicKey) publicKey; - byte[] encoded = Ed25519Verify.get().encodePublicKey(ed25519Key); - String data = Ed25519Verify.ED25519_ID + " " + new String(Base64.encode(encoded)); - return comment.isEmpty() ? data : data + " " + comment; + encoded = verifier.encodePublicKey(ecKey); + } else if ("EdDSA".equals(publicKey.getAlgorithm()) || "Ed25519".equals(publicKey.getAlgorithm())) { + Ed25519PublicKey ed25519Key = Ed25519Verify.convertPublicKey(publicKey); + keyType = Ed25519Verify.ED25519_ID; + encoded = Ed25519Verify.get().encodePublicKey(ed25519Key); } else { throw new InvalidKeyException("Unknown key type: " + publicKey.getClass().getName()); } + + String data = keyType + " " + new String(Base64.encode(encoded)); + return comment.isEmpty() ? data : data + " " + comment; } /** @@ -87,8 +88,9 @@ public static byte[] extractPublicKeyBlob(PublicKey publicKey) ECPublicKey ecKey = (ECPublicKey) publicKey; SSHSignature verifier = ECDSASHA2Verify.getVerifierForKey(ecKey); return verifier.encodePublicKey(ecKey); - } else if (publicKey instanceof Ed25519PublicKey) { - return Ed25519Verify.get().encodePublicKey((Ed25519PublicKey) publicKey); + } else if ("EdDSA".equals(publicKey.getAlgorithm()) || "Ed25519".equals(publicKey.getAlgorithm())) { + Ed25519PublicKey ed25519Key = Ed25519Verify.convertPublicKey(publicKey); + return Ed25519Verify.get().encodePublicKey(ed25519Key); } else { throw new InvalidKeyException("Unknown key type: " + publicKey.getClass().getName()); } diff --git a/src/main/java/com/trilead/ssh2/crypto/keys/Ed25519PrivateKey.java b/src/main/java/com/trilead/ssh2/crypto/keys/Ed25519PrivateKey.java index c9226511..9e4f1d94 100644 --- a/src/main/java/com/trilead/ssh2/crypto/keys/Ed25519PrivateKey.java +++ b/src/main/java/com/trilead/ssh2/crypto/keys/Ed25519PrivateKey.java @@ -1,9 +1,10 @@ package com.trilead.ssh2.crypto.keys; -import com.trilead.ssh2.packets.TypesReader; +import com.trilead.ssh2.crypto.SimpleDERReader; import com.trilead.ssh2.packets.TypesWriter; import java.io.IOException; +import java.math.BigInteger; import java.security.PrivateKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; @@ -102,31 +103,42 @@ public byte[] getEncoded() { private static byte[] decode(PKCS8EncodedKeySpec keySpec) throws InvalidKeySpecException { byte[] encoded = keySpec.getEncoded(); - if (encoded.length != ENCODED_SIZE) { - throw new InvalidKeySpecException("Key spec is of invalid size"); + + // Handle legacy RAW format (32 bytes) from commits f01a8b9 to 91bf5d0 (May-July 2020) + // Before commit 91bf5d0, getEncoded() returned just the raw 32-byte seed with format "RAW" + if (encoded.length == KEY_BYTES_LENGTH) { + return encoded; } + + // Handle standard PKCS#8 format (48+ bytes) from commit 91bf5d0 onwards try { - TypesReader tr = new TypesReader(keySpec.getEncoded()); - if (tr.readByte() != 0x30 || // ASN.1 sequence - tr.readByte() != ENCODED_SIZE - 2 || // Expected size - tr.readByte() != 0x02 || // ASN.1 Integer - tr.readByte() != 1 || // length - tr.readByte() != 0 || // v1 - tr.readByte() != 0x30 || // ASN.1 Sequence - tr.readByte() != ED25519_OID.length + 2 || // OID length - tr.readByte() != 0x06 || // ASN.1 OID - tr.readByte() != ED25519_OID.length) { - throw new InvalidKeySpecException("Key was not encoded correctly"); + SimpleDERReader reader = new SimpleDERReader(encoded); + + byte[] sequenceData = reader.readSequenceAsByteArray(); + SimpleDERReader sequenceReader = new SimpleDERReader(sequenceData); + + BigInteger version = sequenceReader.readInt(); + if (!version.equals(BigInteger.ZERO)) { + throw new InvalidKeySpecException("Unsupported PKCS#8 version: " + version); + } + + int algType = sequenceReader.readConstructedType(); + SimpleDERReader algReader = sequenceReader.readConstructed(); + + String oid = algReader.readOid(); + if (!"1.3.101.112".equals(oid)) { + throw new InvalidKeySpecException("Expected Ed25519 OID (1.3.101.112), got: " + oid); } - byte[] oid = tr.readBytes(ED25519_OID.length); - if (!Arrays.equals(ED25519_OID, oid) || - tr.readByte() != 0x04 || // ASN.1 octet string - tr.readByte() != KEY_BYTES_LENGTH + 2 || // length - tr.readByte() != 0x04 || // ASN.1 octet string - tr.readByte() != KEY_BYTES_LENGTH) { - throw new InvalidKeySpecException("Key was not encoded correctly"); + + byte[] privateKeyOctetString = sequenceReader.readOctetString(); + SimpleDERReader privateKeyReader = new SimpleDERReader(privateKeyOctetString); + byte[] seed = privateKeyReader.readOctetString(); + + if (seed.length != KEY_BYTES_LENGTH) { + throw new InvalidKeySpecException("Expected " + KEY_BYTES_LENGTH + " byte seed, got " + seed.length); } - return tr.readBytes(KEY_BYTES_LENGTH); + + return seed; } catch (IOException e) { throw new InvalidKeySpecException("Key was not encoded correctly", e); } diff --git a/src/main/java/com/trilead/ssh2/crypto/keys/Ed25519PublicKey.java b/src/main/java/com/trilead/ssh2/crypto/keys/Ed25519PublicKey.java index 740ce8d5..c5f6d259 100644 --- a/src/main/java/com/trilead/ssh2/crypto/keys/Ed25519PublicKey.java +++ b/src/main/java/com/trilead/ssh2/crypto/keys/Ed25519PublicKey.java @@ -1,6 +1,6 @@ package com.trilead.ssh2.crypto.keys; -import com.trilead.ssh2.packets.TypesReader; +import com.trilead.ssh2.crypto.SimpleDERReader; import com.trilead.ssh2.packets.TypesWriter; import java.io.IOException; @@ -81,30 +81,40 @@ public boolean equals(Object o) { } private static byte[] decode(byte[] input) throws InvalidKeySpecException { - if (input.length != ENCODED_SIZE) { - throw new InvalidKeySpecException("Key is not of correct size"); + // Handle legacy RAW format (32 bytes) from commits f01a8b9 to 91bf5d0 (May-July 2020) + // Before commit 91bf5d0, getEncoded() returned just the raw 32-byte public key with format "RAW" + if (input.length == KEY_BYTES_LENGTH) { + return input; } + // Handle standard X.509 format (44 bytes) from commit 91bf5d0 onwards try { - TypesReader tr = new TypesReader(input); - if (tr.readByte() != 0x30 || - tr.readByte() != 7 + ED25519_OID.length + KEY_BYTES_LENGTH || - tr.readByte() != 0x30 || - tr.readByte() != 2 + ED25519_OID.length || - tr.readByte() != 0x06 || - tr.readByte() != ED25519_OID.length) { - throw new InvalidKeySpecException("Key was not encoded correctly"); + SimpleDERReader reader = new SimpleDERReader(input); + + byte[] sequenceData = reader.readSequenceAsByteArray(); + SimpleDERReader sequenceReader = new SimpleDERReader(sequenceData); + + int algType = sequenceReader.readConstructedType(); + SimpleDERReader algReader = sequenceReader.readConstructed(); + + String oid = algReader.readOid(); + if (!"1.3.101.112".equals(oid)) { + throw new InvalidKeySpecException("Expected Ed25519 OID (1.3.101.112), got: " + oid); } - byte[] oid = tr.readBytes(ED25519_OID.length); - if (!Arrays.equals(oid, ED25519_OID) || - tr.readByte() != 0x03 || - tr.readByte() != KEY_BYTES_LENGTH + 1 || - tr.readByte() != 0) { - throw new InvalidKeySpecException("Key was not encoded correctly"); + + byte[] publicKeyBitString = sequenceReader.readOctetString(); + + if (publicKeyBitString.length == KEY_BYTES_LENGTH + 1 && publicKeyBitString[0] == 0) { + byte[] result = new byte[KEY_BYTES_LENGTH]; + System.arraycopy(publicKeyBitString, 1, result, 0, KEY_BYTES_LENGTH); + return result; + } else if (publicKeyBitString.length == KEY_BYTES_LENGTH) { + return publicKeyBitString; + } else { + throw new InvalidKeySpecException("Expected " + KEY_BYTES_LENGTH + " byte public key, got " + publicKeyBitString.length); } - return tr.readBytes(KEY_BYTES_LENGTH); } catch (IOException e) { - throw new InvalidKeySpecException("Key was not encoded correctly"); + throw new InvalidKeySpecException("Key was not encoded correctly", e); } } diff --git a/src/test/java/com/trilead/ssh2/crypto/PublicKeyUtilsTest.java b/src/test/java/com/trilead/ssh2/crypto/PublicKeyUtilsTest.java index d99d6c55..c3cd1d85 100644 --- a/src/test/java/com/trilead/ssh2/crypto/PublicKeyUtilsTest.java +++ b/src/test/java/com/trilead/ssh2/crypto/PublicKeyUtilsTest.java @@ -6,6 +6,9 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -200,6 +203,45 @@ void testRoundTripExtractAndFormat() throws Exception { assertTrue(formatted.endsWith(" test")); } + @Test + void testExtractPublicKeyBlobWithNativeJDKEdDSAKey() throws Exception { + KeyPairGenerator kpg; + try { + kpg = KeyPairGenerator.getInstance("EdDSA"); + } catch (NoSuchAlgorithmException e) { + System.err.println("Skipping test: EdDSA not supported by this JDK"); + return; + } + + KeyPair keyPair = kpg.generateKeyPair(); + PublicKey nativePublicKey = keyPair.getPublic(); + + byte[] blob = PublicKeyUtils.extractPublicKeyBlob(nativePublicKey); + + assertNotNull(blob); + assertTrue(blob.length > 0); + } + + @Test + void testToAuthorizedKeysFormatWithNativeJDKEdDSAKey() throws Exception { + KeyPairGenerator kpg; + try { + kpg = KeyPairGenerator.getInstance("EdDSA"); + } catch (NoSuchAlgorithmException e) { + System.err.println("Skipping test: EdDSA not supported by this JDK"); + return; + } + + KeyPair keyPair = kpg.generateKeyPair(); + PublicKey nativePublicKey = keyPair.getPublic(); + + String result = PublicKeyUtils.toAuthorizedKeysFormat(nativePublicKey, "native-eddsa-key"); + + assertNotNull(result); + assertTrue(result.startsWith("ssh-ed25519 ")); + assertTrue(result.endsWith(" native-eddsa-key")); + } + private KeyPair loadKeyPair(String path) throws Exception { byte[] keyData = Files.readAllBytes(Paths.get(path)); String keyString = new String(keyData, "UTF-8"); diff --git a/src/test/java/com/trilead/ssh2/crypto/keys/Ed25519KeyFactoryTest.java b/src/test/java/com/trilead/ssh2/crypto/keys/Ed25519KeyFactoryTest.java index 17f88b7d..cf2face6 100644 --- a/src/test/java/com/trilead/ssh2/crypto/keys/Ed25519KeyFactoryTest.java +++ b/src/test/java/com/trilead/ssh2/crypto/keys/Ed25519KeyFactoryTest.java @@ -21,6 +21,23 @@ public class Ed25519KeyFactoryTest { private static final byte[] KAT_ED25519_PRIV = toByteArray("f72a0a036e3479e15edb74da5f2a5418e66db450ad50687cad90247eeab6440c"); private static final byte[] KAT_ED25519_PUB = toByteArray("5386ea463b45fe14b4216f3f02a0a3f073b57724db10b86b65b2037e17b48c19"); + private static final byte[] OPENSSL_PRIVATE = toByteArray("302e020100300506032b657004220420afd211ae1c8a61e212ddfc5cc59949f6c37ef2f683772c14088a2f8e7b54baf7"); + private static final byte[] OPENSSL_PUBLIC = toByteArray("302a300506032b657003210006e59c37d4b0567863eb56397f7a2cfb78ae26a53dbd2206d83fb2c9cf1cbaea"); + private static final byte[] OPENSSL_ED25519_PRIV = toByteArray("afd211ae1c8a61e212ddfc5cc59949f6c37ef2f683772c14088a2f8e7b54baf7"); + private static final byte[] OPENSSL_ED25519_PUB = toByteArray("06e59c37d4b0567863eb56397f7a2cfb78ae26a53dbd2206d83fb2c9cf1cbaea"); + + // Generated with library version before commit 55e3ec98 (commit ce1cc53) + private static final byte[] OLD_LIB_PRIVATE = toByteArray("302e020100300506032b6570042204208fbad2fd15d27ca0a7b13c877d31b48e4a53ea55d9afc795f49696a50c389a77"); + private static final byte[] OLD_LIB_PUBLIC = toByteArray("302a300506032b6570032100b018a2d34b081be789c9f1af3896dcc14f7c963d3ce9dd38f7d805865f09ca88"); + private static final byte[] OLD_LIB_ED25519_PRIV = toByteArray("8fbad2fd15d27ca0a7b13c877d31b48e4a53ea55d9afc795f49696a50c389a77"); + private static final byte[] OLD_LIB_ED25519_PUB = toByteArray("b018a2d34b081be789c9f1af3896dcc14f7c963d3ce9dd38f7d805865f09ca88"); + + // Legacy RAW format from commits f01a8b9 to 91bf5d0 (May-July 2020) + // Before 91bf5d0, getEncoded() returned just the raw 32-byte seed with format "RAW" + // This tests backward compatibility for keys stored during that 2-month period + private static final byte[] LEGACY_RAW_PRIVATE = toByteArray("afd211ae1c8a61e212ddfc5cc59949f6c37ef2f683772c14088a2f8e7b54baf7"); + private static final byte[] LEGACY_RAW_PUBLIC = toByteArray("06e59c37d4b0567863eb56397f7a2cfb78ae26a53dbd2206d83fb2c9cf1cbaea"); + private static byte[] toByteArray(String s) { byte[] b = new byte[s.length() / 2]; for (int i = 0; i < b.length; i++) { @@ -81,4 +98,105 @@ public void translatesNativeJDKKeys() throws Exception { assertThat(((Ed25519PrivateKey) translatedPrivKey).getSeed(), is(((Ed25519PrivateKey) keyFactorySpi .engineGeneratePrivate(new PKCS8EncodedKeySpec(nativePrivKey.getEncoded()))).getSeed())); } + + @Test + public void decodesOpenSSLGeneratedPrivateKey() throws Exception { + Ed25519Provider p = new Ed25519Provider(); + KeyFactory kf = KeyFactory.getInstance("Ed25519", p); + Ed25519PrivateKey pk = (Ed25519PrivateKey) kf.generatePrivate(new PKCS8EncodedKeySpec(OPENSSL_PRIVATE)); + assertThat(pk.getSeed(), is(OPENSSL_ED25519_PRIV)); + } + + @Test + public void decodesOpenSSLGeneratedPublicKey() throws Exception { + Ed25519Provider p = new Ed25519Provider(); + KeyFactory kf = KeyFactory.getInstance("Ed25519", p); + Ed25519PublicKey pub = (Ed25519PublicKey) kf.generatePublic(new X509EncodedKeySpec(OPENSSL_PUBLIC)); + assertThat(pub.getAbyte(), is(OPENSSL_ED25519_PUB)); + } + + @Test + public void openSSLKeyRoundTrip() throws Exception { + Ed25519Provider p = new Ed25519Provider(); + KeyFactory kf = KeyFactory.getInstance("Ed25519", p); + + Ed25519PrivateKey privateKey = (Ed25519PrivateKey) kf.generatePrivate(new PKCS8EncodedKeySpec(OPENSSL_PRIVATE)); + Ed25519PublicKey publicKey = (Ed25519PublicKey) kf.generatePublic(new X509EncodedKeySpec(OPENSSL_PUBLIC)); + + byte[] reEncodedPrivate = privateKey.getEncoded(); + byte[] reEncodedPublic = publicKey.getEncoded(); + + assertArrayEquals(OPENSSL_PRIVATE, reEncodedPrivate); + assertArrayEquals(OPENSSL_PUBLIC, reEncodedPublic); + + Ed25519PrivateKey privateKey2 = (Ed25519PrivateKey) kf.generatePrivate(new PKCS8EncodedKeySpec(reEncodedPrivate)); + Ed25519PublicKey publicKey2 = (Ed25519PublicKey) kf.generatePublic(new X509EncodedKeySpec(reEncodedPublic)); + + assertThat(privateKey2.getSeed(), is(OPENSSL_ED25519_PRIV)); + assertThat(publicKey2.getAbyte(), is(OPENSSL_ED25519_PUB)); + } + + @Test + public void decodesOldLibraryGeneratedPrivateKey() throws Exception { + Ed25519Provider p = new Ed25519Provider(); + KeyFactory kf = KeyFactory.getInstance("Ed25519", p); + Ed25519PrivateKey pk = (Ed25519PrivateKey) kf.generatePrivate(new PKCS8EncodedKeySpec(OLD_LIB_PRIVATE)); + assertThat(pk.getSeed(), is(OLD_LIB_ED25519_PRIV)); + } + + @Test + public void decodesOldLibraryGeneratedPublicKey() throws Exception { + Ed25519Provider p = new Ed25519Provider(); + KeyFactory kf = KeyFactory.getInstance("Ed25519", p); + Ed25519PublicKey pub = (Ed25519PublicKey) kf.generatePublic(new X509EncodedKeySpec(OLD_LIB_PUBLIC)); + assertThat(pub.getAbyte(), is(OLD_LIB_ED25519_PUB)); + } + + @Test + public void oldLibraryKeyRoundTrip() throws Exception { + Ed25519Provider p = new Ed25519Provider(); + KeyFactory kf = KeyFactory.getInstance("Ed25519", p); + + Ed25519PrivateKey privateKey = (Ed25519PrivateKey) kf.generatePrivate(new PKCS8EncodedKeySpec(OLD_LIB_PRIVATE)); + Ed25519PublicKey publicKey = (Ed25519PublicKey) kf.generatePublic(new X509EncodedKeySpec(OLD_LIB_PUBLIC)); + + byte[] reEncodedPrivate = privateKey.getEncoded(); + byte[] reEncodedPublic = publicKey.getEncoded(); + + assertArrayEquals(OLD_LIB_PRIVATE, reEncodedPrivate); + assertArrayEquals(OLD_LIB_PUBLIC, reEncodedPublic); + + Ed25519PrivateKey privateKey2 = (Ed25519PrivateKey) kf.generatePrivate(new PKCS8EncodedKeySpec(reEncodedPrivate)); + Ed25519PublicKey publicKey2 = (Ed25519PublicKey) kf.generatePublic(new X509EncodedKeySpec(reEncodedPublic)); + + assertThat(privateKey2.getSeed(), is(OLD_LIB_ED25519_PRIV)); + assertThat(publicKey2.getAbyte(), is(OLD_LIB_ED25519_PUB)); + } + + @Test + public void decodesLegacyRawFormatPrivateKey() throws Exception { + Ed25519Provider p = new Ed25519Provider(); + KeyFactory kf = KeyFactory.getInstance("Ed25519", p); + + Ed25519PrivateKey pk = (Ed25519PrivateKey) kf.generatePrivate(new PKCS8EncodedKeySpec(LEGACY_RAW_PRIVATE)); + + assertThat(pk.getSeed(), is(LEGACY_RAW_PRIVATE)); + + byte[] reEncoded = pk.getEncoded(); + assertThat(reEncoded.length, is(48)); + } + + @Test + public void decodesLegacyRawFormatPublicKey() throws Exception { + Ed25519Provider p = new Ed25519Provider(); + KeyFactory kf = KeyFactory.getInstance("Ed25519", p); + + Ed25519PublicKey pub = (Ed25519PublicKey) kf.generatePublic(new X509EncodedKeySpec(LEGACY_RAW_PUBLIC)); + + assertThat(pub.getAbyte(), is(LEGACY_RAW_PUBLIC)); + + byte[] reEncoded = pub.getEncoded(); + assertThat(reEncoded.length, is(44)); + } + } diff --git a/src/test/java/com/trilead/ssh2/signature/Ed25519VerifyTest.java b/src/test/java/com/trilead/ssh2/signature/Ed25519VerifyTest.java index d5974ba8..9bc28e7e 100644 --- a/src/test/java/com/trilead/ssh2/signature/Ed25519VerifyTest.java +++ b/src/test/java/com/trilead/ssh2/signature/Ed25519VerifyTest.java @@ -223,4 +223,44 @@ public void nativeJDKConversion() throws Exception { Ed25519PrivateKey convertedPrivKey = new Ed25519PrivateKey(privKeySpec); assertArrayEquals(nativePrivKey.getEncoded(), convertedPrivKey.getEncoded()); } + + @Test + public void opensslGeneratedKeySignAndVerify() throws Exception { + byte[] opensslPrivate = toByteArray("302e020100300506032b657004220420afd211ae1c8a61e212ddfc5cc59949f6c37ef2f683772c14088a2f8e7b54baf7"); + byte[] opensslPublic = toByteArray("302a300506032b657003210006e59c37d4b0567863eb56397f7a2cfb78ae26a53dbd2206d83fb2c9cf1cbaea"); + + Ed25519PrivateKey privateKey = new Ed25519PrivateKey(new PKCS8EncodedKeySpec(opensslPrivate)); + Ed25519PublicKey publicKey = new Ed25519PublicKey(new X509EncodedKeySpec(opensslPublic)); + + byte[] message = "Test message for OpenSSL-generated Ed25519 key".getBytes(); + + Ed25519Verify verifier = Ed25519Verify.get(); + byte[] signature = verifier.generateSignature(message, privateKey, new SecureRandom()); + + assertTrue(verifier.verifySignature(message, signature, publicKey)); + + byte[] tampered = Arrays.copyOf(message, message.length); + tampered[0] ^= 1; + assertFalse(verifier.verifySignature(tampered, signature, publicKey)); + } + + @Test + public void oldLibraryGeneratedKeySignAndVerify() throws Exception { + byte[] oldLibPrivate = toByteArray("302e020100300506032b6570042204208fbad2fd15d27ca0a7b13c877d31b48e4a53ea55d9afc795f49696a50c389a77"); + byte[] oldLibPublic = toByteArray("302a300506032b6570032100b018a2d34b081be789c9f1af3896dcc14f7c963d3ce9dd38f7d805865f09ca88"); + + Ed25519PrivateKey privateKey = new Ed25519PrivateKey(new PKCS8EncodedKeySpec(oldLibPrivate)); + Ed25519PublicKey publicKey = new Ed25519PublicKey(new X509EncodedKeySpec(oldLibPublic)); + + byte[] message = "Test message for old library-generated Ed25519 key".getBytes(); + + Ed25519Verify verifier = Ed25519Verify.get(); + byte[] signature = verifier.generateSignature(message, privateKey, new SecureRandom()); + + assertTrue(verifier.verifySignature(message, signature, publicKey)); + + byte[] tampered = Arrays.copyOf(message, message.length); + tampered[0] ^= 1; + assertFalse(verifier.verifySignature(tampered, signature, publicKey)); + } }