Removed eddsa library in favor of standard Java Security classes (#993)
Some checks are pending
Build SSHJ / Build with Java 11 (push) Waiting to run
Build SSHJ / Integration test (push) Waiting to run

- Bouncy Castle provides Ed25519 support using standard Java Security classes
- Removed net.i2p.crypto:eddsa:0.3.0 dependency
- Removed Ed25519PublicKey extension of EdDSAPublicKey class from eddsa library
- Added Ed25519KeyFactory for generating Java PublicKey and PrivateKey objects from raw encoded key byte arrays
- Refactored key parsing to use Ed25519KeyFactory
- Refactored SignatureEdDSA to use Java Signature class with Ed25519

Co-authored-by: Jeroen van Erp <jeroen@hierynomus.com>
This commit is contained in:
David Handermann
2025-03-19 07:42:06 -05:00
committed by GitHub
parent b4bc69626e
commit cea67fef73
8 changed files with 128 additions and 98 deletions

View File

@@ -54,7 +54,6 @@ dependencies {
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion" implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion" implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
implementation "com.hierynomus:asn-one:0.6.0" implementation "com.hierynomus:asn-one:0.6.0"
implementation "net.i2p.crypto:eddsa:0.3.0"
} }
license { license {
@@ -182,8 +181,6 @@ jar {
instruction "Import-Package", "!net.schmizz.*" instruction "Import-Package", "!net.schmizz.*"
instruction "Import-Package", "!com.hierynomus.sshj.*" instruction "Import-Package", "!com.hierynomus.sshj.*"
instruction "Import-Package", "javax.crypto*" instruction "Import-Package", "javax.crypto*"
instruction "Import-Package", "!net.i2p.crypto.eddsa.math"
instruction "Import-Package", "net.i2p*"
instruction "Import-Package", "com.jcraft.jzlib*;version=\"[1.1,2)\";resolution:=optional" instruction "Import-Package", "com.jcraft.jzlib*;version=\"[1.1,2)\";resolution:=optional"
instruction "Import-Package", "org.slf4j*;version=\"[1.7,5)\"" instruction "Import-Package", "org.slf4j*;version=\"[1.7,5)\""
instruction "Import-Package", "org.bouncycastle*;resolution:=optional" instruction "Import-Package", "org.bouncycastle*;resolution:=optional"

View File

@@ -1,56 +0,0 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj.signature;
import net.i2p.crypto.eddsa.EdDSAPublicKey;
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec;
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
import net.schmizz.sshj.common.SSHRuntimeException;
import java.util.Arrays;
/**
* Our own extension of the EdDSAPublicKey that comes from ECC-25519, as that class does not implement equality.
* The code uses the equality of the keys as an indicator whether they're the same during host key verification.
*/
@SuppressWarnings("serial")
public class Ed25519PublicKey extends EdDSAPublicKey {
public Ed25519PublicKey(EdDSAPublicKeySpec spec) {
super(spec);
EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName("Ed25519");
if (!spec.getParams().getCurve().equals(ed25519.getCurve())) {
throw new SSHRuntimeException("Cannot create Ed25519 Public Key from wrong spec");
}
}
@Override
public boolean equals(Object other) {
if (!(other instanceof Ed25519PublicKey)) {
return false;
}
Ed25519PublicKey otherKey = (Ed25519PublicKey) other;
return Arrays.equals(getAbyte(), otherKey.getAbyte());
}
@Override
public int hashCode() {
return getA().hashCode();
}
}

View File

@@ -15,14 +15,14 @@
*/ */
package com.hierynomus.sshj.signature; package com.hierynomus.sshj.signature;
import net.i2p.crypto.eddsa.EdDSAEngine;
import net.schmizz.sshj.common.KeyType; import net.schmizz.sshj.common.KeyType;
import net.schmizz.sshj.common.SSHRuntimeException; import net.schmizz.sshj.common.SSHRuntimeException;
import net.schmizz.sshj.common.SecurityUtils;
import net.schmizz.sshj.signature.AbstractSignature; import net.schmizz.sshj.signature.AbstractSignature;
import net.schmizz.sshj.signature.Signature; import net.schmizz.sshj.signature.Signature;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SignatureException; import java.security.SignatureException;
public class SignatureEdDSA extends AbstractSignature { public class SignatureEdDSA extends AbstractSignature {
@@ -43,11 +43,11 @@ public class SignatureEdDSA extends AbstractSignature {
super(getEngine(), KeyType.ED25519.toString()); super(getEngine(), KeyType.ED25519.toString());
} }
private static EdDSAEngine getEngine() { private static java.security.Signature getEngine() {
try { try {
return new EdDSAEngine(MessageDigest.getInstance("SHA-512")); return SecurityUtils.getSignature("Ed25519");
} catch (NoSuchAlgorithmException e) { } catch (NoSuchAlgorithmException | NoSuchProviderException e) {
throw new SSHRuntimeException(e); throw new SSHRuntimeException("Ed25519 Signatures not supported", e);
} }
} }

View File

@@ -21,9 +21,6 @@ import com.hierynomus.sshj.transport.cipher.BlockCiphers;
import com.hierynomus.sshj.transport.cipher.ChachaPolyCiphers; import com.hierynomus.sshj.transport.cipher.ChachaPolyCiphers;
import com.hierynomus.sshj.transport.cipher.GcmCiphers; import com.hierynomus.sshj.transport.cipher.GcmCiphers;
import com.hierynomus.sshj.userauth.keyprovider.bcrypt.BCrypt; import com.hierynomus.sshj.userauth.keyprovider.bcrypt.BCrypt;
import net.i2p.crypto.eddsa.EdDSAPrivateKey;
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec;
import net.schmizz.sshj.common.*; import net.schmizz.sshj.common.*;
import net.schmizz.sshj.common.Buffer.PlainBuffer; import net.schmizz.sshj.common.Buffer.PlainBuffer;
import net.schmizz.sshj.transport.cipher.Cipher; import net.schmizz.sshj.transport.cipher.Cipher;
@@ -351,8 +348,14 @@ public class OpenSSHKeyV1KeyFile extends BaseFileKeyProvider {
keyBuffer.readUInt32(); // length of privatekey+publickey keyBuffer.readUInt32(); // length of privatekey+publickey
byte[] privKey = new byte[32]; byte[] privKey = new byte[32];
keyBuffer.readRawBytes(privKey); // string privatekey keyBuffer.readRawBytes(privKey); // string privatekey
keyBuffer.readRawBytes(new byte[32]); // string publickey (again...)
kp = new KeyPair(publicKey, new EdDSAPrivateKey(new EdDSAPrivateKeySpec(privKey, EdDSANamedCurveTable.getByName("Ed25519")))); final byte[] pubKey = new byte[32];
keyBuffer.readRawBytes(pubKey); // string publickey (again...)
final PrivateKey edPrivateKey = Ed25519KeyFactory.getPrivateKey(privKey);
final PublicKey edPublicKey = Ed25519KeyFactory.getPublicKey(pubKey);
kp = new KeyPair(edPublicKey, edPrivateKey);
break; break;
case RSA: case RSA:
final RSAPrivateCrtKeySpec rsaPrivateCrtKeySpec = readRsaPrivateKeySpec(keyBuffer); final RSAPrivateCrtKeySpec rsaPrivateCrtKeySpec = readRsaPrivateKeySpec(keyBuffer);

View File

@@ -0,0 +1,90 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.schmizz.sshj.common;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Objects;
/**
* Factory for generating Edwards-curve 25519 Public and Private Keys
*/
public class Ed25519KeyFactory {
private static final int KEY_LENGTH = 32;
private static final String KEY_ALGORITHM = "Ed25519";
private static final byte[] ED25519_PKCS8_PRIVATE_KEY_HEADER = Base64.getDecoder().decode("MC4CAQEwBQYDK2VwBCIEIA");
private static final byte[] ED25519_PKCS8_PUBLIC_KEY_HEADER = Base64.getDecoder().decode("MCowBQYDK2VwAyEA");
private static final int PRIVATE_KEY_ENCODED_LENGTH = 48;
private static final int PUBLIC_KEY_ENCODED_LENGTH = 44;
private Ed25519KeyFactory() {
}
/**
* Get Edwards-curve Private Key for private key binary
*
* @param privateKeyBinary Private Key byte array consisting of 32 bytes
* @return Edwards-curve 25519 Private Key
* @throws GeneralSecurityException Thrown on failure to generate Private Key
*/
public static PrivateKey getPrivateKey(final byte[] privateKeyBinary) throws GeneralSecurityException {
Objects.requireNonNull(privateKeyBinary, "Private Key byte array required");
if (privateKeyBinary.length == KEY_LENGTH) {
final byte[] privateKeyEncoded = new byte[PRIVATE_KEY_ENCODED_LENGTH];
System.arraycopy(ED25519_PKCS8_PRIVATE_KEY_HEADER, 0, privateKeyEncoded, 0, ED25519_PKCS8_PRIVATE_KEY_HEADER.length);
System.arraycopy(privateKeyBinary, 0, privateKeyEncoded, ED25519_PKCS8_PRIVATE_KEY_HEADER.length, KEY_LENGTH);
final PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyEncoded);
final KeyFactory keyFactory = SecurityUtils.getKeyFactory(KEY_ALGORITHM);
return keyFactory.generatePrivate(keySpec);
} else {
throw new IllegalArgumentException("Key length of 32 bytes required");
}
}
/**
* Get Edwards-curve Public Key for public key binary
*
* @param publicKeyBinary Public Key byte array consisting of 32 bytes
* @return Edwards-curve 25519 Public Key
* @throws GeneralSecurityException Thrown on failure to generate Public Key
*/
public static PublicKey getPublicKey(final byte[] publicKeyBinary) throws GeneralSecurityException {
Objects.requireNonNull(publicKeyBinary, "Public Key byte array required");
if (publicKeyBinary.length == KEY_LENGTH) {
final byte[] publicKeyEncoded = new byte[PUBLIC_KEY_ENCODED_LENGTH];
System.arraycopy(ED25519_PKCS8_PUBLIC_KEY_HEADER, 0, publicKeyEncoded, 0, ED25519_PKCS8_PUBLIC_KEY_HEADER.length);
System.arraycopy(publicKeyBinary, 0, publicKeyEncoded, ED25519_PKCS8_PUBLIC_KEY_HEADER.length, KEY_LENGTH);
final X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyEncoded);
final KeyFactory keyFactory = SecurityUtils.getKeyFactory(KEY_ALGORITHM);
return keyFactory.generatePublic(keySpec);
} else {
throw new IllegalArgumentException("Key length of 32 bytes required");
}
}
}

View File

@@ -16,13 +16,8 @@
package net.schmizz.sshj.common; package net.schmizz.sshj.common;
import com.hierynomus.sshj.common.KeyAlgorithm; import com.hierynomus.sshj.common.KeyAlgorithm;
import com.hierynomus.sshj.signature.Ed25519PublicKey;
import com.hierynomus.sshj.signature.SignatureEdDSA; import com.hierynomus.sshj.signature.SignatureEdDSA;
import com.hierynomus.sshj.userauth.certificate.Certificate; import com.hierynomus.sshj.userauth.certificate.Certificate;
import net.i2p.crypto.eddsa.EdDSAPublicKey;
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec;
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
import net.schmizz.sshj.common.Buffer.BufferException; import net.schmizz.sshj.common.Buffer.BufferException;
import net.schmizz.sshj.signature.Signature; import net.schmizz.sshj.signature.Signature;
import net.schmizz.sshj.signature.SignatureDSA; import net.schmizz.sshj.signature.SignatureDSA;
@@ -178,20 +173,16 @@ public enum KeyType {
public PublicKey readPubKeyFromBuffer(Buffer<?> buf) throws GeneralSecurityException { public PublicKey readPubKeyFromBuffer(Buffer<?> buf) throws GeneralSecurityException {
try { try {
final int keyLen = buf.readUInt32AsInt(); final int keyLen = buf.readUInt32AsInt();
final byte[] p = new byte[keyLen]; final byte[] publicKeyBinary = new byte[keyLen];
buf.readRawBytes(p); buf.readRawBytes(publicKeyBinary);
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug(String.format("Key algo: %s, Key curve: 25519, Key Len: %s\np: %s", log.debug(String.format("Key algo: %s, Key curve: 25519, Key Len: %s\np: %s",
sType, sType,
keyLen, keyLen,
Arrays.toString(p)) Arrays.toString(publicKeyBinary))
); );
} }
return Ed25519KeyFactory.getPublicKey(publicKeyBinary);
EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName("Ed25519");
EdDSAPublicKeySpec publicSpec = new EdDSAPublicKeySpec(p, ed25519);
return new Ed25519PublicKey(publicSpec);
} catch (Buffer.BufferException be) { } catch (Buffer.BufferException be) {
throw new SSHRuntimeException(be); throw new SSHRuntimeException(be);
} }
@@ -199,13 +190,17 @@ public enum KeyType {
@Override @Override
protected void writePubKeyContentsIntoBuffer(PublicKey pk, Buffer<?> buf) { protected void writePubKeyContentsIntoBuffer(PublicKey pk, Buffer<?> buf) {
EdDSAPublicKey key = (EdDSAPublicKey) pk; final byte[] encoded = pk.getEncoded();
buf.putBytes(key.getAbyte()); final int keyLength = 32;
final int headerLength = encoded.length - keyLength;
final byte[] encodedPublicKey = new byte[keyLength];
System.arraycopy(encoded, headerLength, encodedPublicKey, 0, keyLength);
buf.putBytes(encodedPublicKey);
} }
@Override @Override
protected boolean isMyType(Key key) { protected boolean isMyType(Key key) {
return "EdDSA".equals(key.getAlgorithm()); return "EdDSA".equals(key.getAlgorithm()) || "Ed25519".equals(key.getAlgorithm());
} }
}, },

View File

@@ -16,12 +16,6 @@
package net.schmizz.sshj.userauth.keyprovider; package net.schmizz.sshj.userauth.keyprovider;
import com.hierynomus.sshj.common.KeyAlgorithm; import com.hierynomus.sshj.common.KeyAlgorithm;
import net.i2p.crypto.eddsa.EdDSAPrivateKey;
import net.i2p.crypto.eddsa.EdDSAPublicKey;
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec;
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec;
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
import net.schmizz.sshj.common.*; import net.schmizz.sshj.common.*;
import net.schmizz.sshj.userauth.password.PasswordUtils; import net.schmizz.sshj.userauth.password.PasswordUtils;
import org.bouncycastle.crypto.generators.Argon2BytesGenerator; import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
@@ -165,10 +159,17 @@ public class PuTTYKeyFile extends BaseFileKeyProvider {
} }
} }
if (KeyType.ED25519.equals(keyType)) { if (KeyType.ED25519.equals(keyType)) {
EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName("Ed25519"); try {
EdDSAPublicKeySpec publicSpec = new EdDSAPublicKeySpec(publicKeyReader.readBytes(), ed25519); final byte[] publicKeyEncoded = publicKeyReader.readBytes();
EdDSAPrivateKeySpec privateSpec = new EdDSAPrivateKeySpec(privateKeyReader.readBytes(), ed25519); final PublicKey edPublicKey = Ed25519KeyFactory.getPublicKey(publicKeyEncoded);
return new KeyPair(new EdDSAPublicKey(publicSpec), new EdDSAPrivateKey(privateSpec));
final byte[] privateKeyEncoded = privateKeyReader.readBytes();
final PrivateKey edPrivateKey = Ed25519KeyFactory.getPrivateKey(privateKeyEncoded);
return new KeyPair(edPublicKey, edPrivateKey);
} catch (final GeneralSecurityException e) {
throw new IOException("Reading Ed25519 Keys failed", e);
}
} }
final ECDSACurve ecdsaCurve; final ECDSACurve ecdsaCurve;
switch (keyType) { switch (keyType) {

View File

@@ -222,7 +222,7 @@ public class OpenSSHKeyFileTest {
OpenSSHKeyV1KeyFile keyFile = new OpenSSHKeyV1KeyFile(); OpenSSHKeyV1KeyFile keyFile = new OpenSSHKeyV1KeyFile();
keyFile.init(new File("src/test/resources/keytypes/test_ed25519")); keyFile.init(new File("src/test/resources/keytypes/test_ed25519"));
PrivateKey aPrivate = keyFile.getPrivate(); PrivateKey aPrivate = keyFile.getPrivate();
assertThat(aPrivate.getAlgorithm(), equalTo("EdDSA")); assertThat(aPrivate.getAlgorithm(), equalTo("Ed25519"));
} }
@Test @Test
@@ -343,7 +343,7 @@ public class OpenSSHKeyFileTest {
WipeTrackingPasswordFinder pwf = new WipeTrackingPasswordFinder(password, withRetry); WipeTrackingPasswordFinder pwf = new WipeTrackingPasswordFinder(password, withRetry);
keyFile.init(new File(key), pwf); keyFile.init(new File(key), pwf);
PrivateKey aPrivate = keyFile.getPrivate(); PrivateKey aPrivate = keyFile.getPrivate();
assertThat(aPrivate.getAlgorithm(), equalTo("EdDSA")); assertThat(aPrivate.getAlgorithm(), equalTo("Ed25519"));
pwf.assertWiped(); pwf.assertWiped();
} }