Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 19 additions & 17 deletions src/main/java/com/trilead/ssh2/crypto/PublicKeyUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -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());
}
Expand Down
56 changes: 34 additions & 22 deletions src/main/java/com/trilead/ssh2/crypto/keys/Ed25519PrivateKey.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
48 changes: 29 additions & 19 deletions src/main/java/com/trilead/ssh2/crypto/keys/Ed25519PublicKey.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
}
}

Expand Down
42 changes: 42 additions & 0 deletions src/test/java/com/trilead/ssh2/crypto/PublicKeyUtilsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
Expand Down
118 changes: 118 additions & 0 deletions src/test/java/com/trilead/ssh2/crypto/keys/Ed25519KeyFactoryTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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++) {
Expand Down Expand Up @@ -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));
}

}
Loading
Loading