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
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
* [GH-809](https://github.com/apache/mina-sshd/pull/809) Fix server-side authentication for FIDO/U2F sk-* keys with flags in `authorized_keys`
* [GH-827](https://github.com/apache/mina-sshd/issues/827) Don't fail on invalid `known_hosts` lines; log and skip them
* [GH-830](https://github.com/apache/mina-sshd/issues/830) EC public keys: let Bouncy Castle generate X.509 encodings with the curve OID as algorithm parameter
* [GH-856](https://github.com/apache/mina-sshd/issues/856) FIX using ed25519 with BC-FIPS

## New Features

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -619,9 +619,6 @@ public static boolean isNetI2pCryptoEdDSARegistered() {
}

public static Optional<EdDSASupport<?, ?>> getEdDSASupport() {
if (isFipsMode()) {
return Optional.empty();
}
register();

synchronized (REGISTERED_PROVIDERS) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import java.security.PublicKey;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

import org.apache.sshd.common.config.keys.PrivateKeyEntryDecoder;
import org.apache.sshd.common.config.keys.PublicKeyEntryDecoder;
Expand All @@ -36,17 +37,13 @@
import org.apache.sshd.common.util.buffer.Buffer;
import org.apache.sshd.common.util.security.SecurityUtils;
import org.apache.sshd.common.util.security.eddsa.generic.EdDSASupport;
import org.apache.sshd.common.util.security.eddsa.generic.EdDSAUtils;
import org.apache.sshd.common.util.security.eddsa.generic.GenericEd25519PublicKeyDecoder;
import org.apache.sshd.common.util.security.eddsa.generic.GenericOpenSSHEd25519PrivateKeyEntryDecoder;
import org.apache.sshd.common.util.security.eddsa.generic.GenericSignatureEd25519;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
import org.bouncycastle.crypto.util.PrivateKeyFactory;
import org.bouncycastle.crypto.util.PrivateKeyInfoFactory;
import org.bouncycastle.jcajce.interfaces.EdDSAKey;
import org.bouncycastle.jcajce.interfaces.EdDSAPrivateKey;
import org.bouncycastle.jcajce.interfaces.EdDSAPublicKey;
import org.bouncycastle.jcajce.spec.RawEncodedKeySpec;

public class BouncyCastleEdDSASupport implements EdDSASupport<EdDSAPublicKey, EdDSAPrivateKey> {

Expand Down Expand Up @@ -113,23 +110,24 @@ public EdDSAPublicKey recoverEDDSAPublicKey(PrivateKey key) throws GeneralSecuri

@Override
public EdDSAPublicKey generateEDDSAPublicKey(byte[] seed) throws GeneralSecurityException {
RawEncodedKeySpec keySpec = new RawEncodedKeySpec(seed);
KeyFactory factory = SecurityUtils.getKeyFactory(getKeyFactoryAlgorithm());
return (EdDSAPublicKey) factory.generatePublic(keySpec);
return (EdDSAPublicKey) factory.generatePublic(EdDSAUtils.createPublicKeySpec(seed));
}

@Override
public EdDSAPrivateKey generateEDDSAPrivateKey(byte[] seed) throws GeneralSecurityException, IOException {
Ed25519PrivateKeyParameters parameters = new Ed25519PrivateKeyParameters(seed);
PrivateKeyInfo info = PrivateKeyInfoFactory.createPrivateKeyInfo(parameters);
KeyFactory factory = SecurityUtils.getKeyFactory(getKeyFactoryAlgorithm());
return (EdDSAPrivateKey) factory.generatePrivate(new PKCS8EncodedKeySpec(info.getEncoded()));
return (EdDSAPrivateKey) factory.generatePrivate(EdDSAUtils.createPrivateKeySpec(seed));
}

@Override
public <B extends Buffer> B putRawEDDSAPublicKey(B buffer, PublicKey key) {
EdDSAPublicKey edKey = ValidateUtils.checkInstanceOf(key, EdDSAPublicKey.class, "Not an EDDSA public key: %s", key);
buffer.putBytes(edKey.getPointEncoding());
try {
buffer.putBytes(EdDSAUtils.getBytes(edKey));
} catch (InvalidKeyException e) {
throw new IllegalArgumentException(e.getMessage(), e);
}
return buffer;
}

Expand All @@ -142,7 +140,7 @@ public <B extends Buffer> B putEDDSAKeyPair(B buffer, PublicKey pubKey, PrivateK

@Override
public KeySpec createPublicKeySpec(EdDSAPublicKey publicKey) {
return new RawEncodedKeySpec(publicKey.getPointEncoding());
return new X509EncodedKeySpec(publicKey.getEncoded());
}

@Override
Expand All @@ -152,14 +150,20 @@ public KeySpec createPrivateKeySpec(EdDSAPrivateKey privateKey) {

@Override
public byte[] getPublicKeyData(EdDSAPublicKey publicKey) {
return publicKey == null ? null : publicKey.getPointEncoding();
try {
return publicKey == null ? null : EdDSAUtils.getBytes(publicKey);
} catch (InvalidKeyException e) {
throw new IllegalArgumentException(e.getMessage(), e);
}
}

@Override
public byte[] getPrivateKeyData(EdDSAPrivateKey privateKey) throws IOException {
Ed25519PrivateKeyParameters parameters
= (Ed25519PrivateKeyParameters) PrivateKeyFactory.createKey(privateKey.getEncoded());
return parameters.getEncoded();
try {
return EdDSAUtils.getBytes(privateKey);
} catch (InvalidKeyException e) {
throw new IOException(e.getMessage(), e);
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,17 @@
*/
package org.apache.sshd.common.util.security.eddsa.generic;

import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;

import org.apache.sshd.common.util.io.der.DERParser;

/**
* Utilities to extract the raw key bytes from ed25519 or ed448 public keys, in a manner that is independent of the
* actual concrete key implementation classes.
Expand All @@ -44,10 +51,43 @@ public final class EdDSAUtils {
private static final byte[] ED448_X509_PREFIX = {
0x30, 0x43, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x71, 0x03, 0x3a, 0x00 };

// For reconstructing private keys from raw bytes we construct a minimal PKCS#8 encoding, using RFC 5208 (version 0)
// without the public key and without attributes. This is allowed by RFC 5958 (Asymmetric key packages).

// Sequence, length 46, (3 bytes: Version 0), Sequence, length 5, OID, length 3, O, I, D, Octet String, length 34,
// Octet String, length 32
private static final byte[] ED25519_PKCS8_PREFIX = {
0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70,
0x04, 0x22, 0x04, 0x20 };
// Sequence, length 71, (3 bytes: Version 0), Sequence, length 5, OID, length 3, O, I, D, Octet String, length 59,
// Octet String, length 57
private static final byte[] ED448_PKCS8_PREFIX = {
0x30, 0x47, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x71,
0x04, 0x3b, 0x04, 0x39 };

// The first two numbers of the dotted notation are combined into one byte: (1 * 40 + 3) = 43 = 0x2b
private static final byte[] ED25519_OID = { 0x2b, 0x65, 0x70 }; // 1.3.101.112
private static final byte[] ED448_OID = { 0x2b, 0x65, 0x71 }; // 1.3.101.113

private EdDSAUtils() {
throw new IllegalStateException("No instantiation");
}

private static boolean arrayEq(byte[] a, byte[] b) {
if (a == null && b == null) {
return true;
}
if (a == null || b == null) {
return false;
}
int unequal = a.length ^ b.length;
int length = Math.min(a.length, b.length);
for (int i = 0; i < length; i++) {
unequal |= a[i] ^ b[i];
}
return unequal == 0;
}

private static boolean startsWith(byte[] data, byte[] prefix) {
if (data == null || prefix == null || prefix.length == 0 || data.length < prefix.length) {
return false;
Expand Down Expand Up @@ -86,4 +126,143 @@ public static byte[] getBytes(PublicKey key) throws InvalidKeyException {
}
return Arrays.copyOfRange(encoded, encoded.length - n, encoded.length);
}

/**
* Retrieves the raw key bytes from an ed25519 or ed448 {@link PrivateKey}.
*
* @param key {@link PrivateKey} to get the bytes of
* @return the raw key bytes
* @throws InvalidKeyException if the key is not an ed25519 or ed448 key, or if it doesn't use PKCS#8 encoding
*/
public static byte[] getBytes(PrivateKey key) throws InvalidKeyException {
// Extract the private key bytes from the PKCS#8 encoding.
if (!"PKCS#8".equalsIgnoreCase(key.getFormat())) {
throw new InvalidKeyException("Cannot extract private key bytes from a non-PKCS#8 encoding");
}
byte[] encoded = key.getEncoded();
if (encoded == null) {
throw new InvalidKeyException("Private key " + key.getClass().getCanonicalName() + " does not support encoding");
}
try {
return asn1Parse(encoded);
} finally {
Arrays.fill(encoded, (byte) 0);
}
}

/**
* Extracts the private key bytes from an encoded EdDSA private key by parsing the bytes as ASN.1 according to RFC
* 5958 (PKCS #8 encoding):
*
* <pre>
* OneAsymmetricKey ::= SEQUENCE {
* version Version,
* privateKeyAlgorithm PrivateKeyAlgorithmIdentifier,
* privateKey PrivateKey,
* ...
* }
*
* Version ::= INTEGER
* PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier
* PrivateKey ::= OCTET STRING
*
* AlgorithmIdentifier ::= SEQUENCE {
* algorithm OBJECT IDENTIFIER,
* parameters ANY DEFINED BY algorithm OPTIONAL
* }
* </pre>
* <p>
* and RFC 8410: "... when encoding a OneAsymmetricKey object, the private key is wrapped in a CurvePrivateKey
* object and wrapped by the OCTET STRING of the 'privateKey' field."
* </p>
*
* <pre>
* CurvePrivateKey ::= OCTET STRING
* </pre>
*
* @param encoded encoded private key to extract the private key bytes from
* @return the extracted private key bytes
* @throws InvalidKeyException if the private key cannot be extracted
* @see <a href="https://tools.ietf.org/html/rfc5958">RFC 5958</a>
* @see <a href="https://tools.ietf.org/html/rfc8410">RFC 8410</a>
*/
private static byte[] asn1Parse(byte[] encoded) throws InvalidKeyException {
byte[] privateKey = null;
try (DERParser byteParser = new DERParser(encoded);
DERParser oneAsymmetricKey = byteParser.readObject().createParser()) {
oneAsymmetricKey.readObject(); // Skip version
int n;
try (DERParser algorithmIdentifier = oneAsymmetricKey.readObject().createParser()) {
byte[] oid = algorithmIdentifier.readObject().getValue();
if (arrayEq(ED25519_OID, oid)) {
n = ED25519_LENGTH;
} else if (arrayEq(ED448_OID, oid)) {
n = ED448_LENGTH;
} else {
throw new InvalidKeyException("Private key is neither ed25519 nor ed448");
}
}
privateKey = oneAsymmetricKey.readObject().getValue();
// The last n bytes of this must be the private key bytes.
return Arrays.copyOfRange(privateKey, privateKey.length - n, privateKey.length);
// Depending on the version there may be optional stuff following, but we don't care about that.
} catch (IOException e) {
throw new InvalidKeyException("Cannot parse EdDSA private key", e);
} finally {
if (privateKey != null) {
Arrays.fill(privateKey, (byte) 0);
}
}
}

/**
* Creates a {@link KeySpec} for re-creating an ed25519 or ed448 public key from the raw key bytes.
*
* @param keyData the raw key bytes
* @return the {@link KeySpec}
* @throws InvalidKeyException if the key bytes do not have the appropriate length for an ed25519 or ed448 key
*/
public static KeySpec createPublicKeySpec(byte[] keyData) throws InvalidKeyException {
// Create an X.509 encoding for ed25519 or ed448, depending on the length of keyData.
if (keyData.length == ED25519_LENGTH) {
byte[] x509 = Arrays.copyOf(ED25519_X509_PREFIX, ED25519_X509_PREFIX.length + ED25519_LENGTH);
System.arraycopy(keyData, 0, x509, ED25519_X509_PREFIX.length, ED25519_LENGTH);
return new X509EncodedKeySpec(x509);
} else if (keyData.length == ED448_LENGTH) {
byte[] x509 = Arrays.copyOf(ED448_X509_PREFIX, ED448_X509_PREFIX.length + ED448_LENGTH);
System.arraycopy(keyData, 0, x509, ED448_X509_PREFIX.length, ED448_LENGTH);
return new X509EncodedKeySpec(x509);
}
throw new InvalidKeyException("Public key data is neither ed25519 nor ed448");
}

/**
* Creates a {@link KeySpec} for re-creating an ed25519 or ed448 public key from the raw key bytes.
*
* @param keyData the raw key bytes
* @return the {@link KeySpec}
* @throws InvalidKeyException if the key bytes do not have the appropriate length for an ed25519 or ed448 key
*/
public static KeySpec createPrivateKeySpec(byte[] keyData) throws InvalidKeyException {
// Create a PKCS#8 encoding for ed25519 or ed448, depending on the length of keyData.
if (keyData.length == ED25519_LENGTH) {
byte[] pkcs8 = Arrays.copyOf(ED25519_PKCS8_PREFIX, ED25519_PKCS8_PREFIX.length + ED25519_LENGTH);
try {
System.arraycopy(keyData, 0, pkcs8, ED25519_PKCS8_PREFIX.length, ED25519_LENGTH);
return new PKCS8EncodedKeySpec(pkcs8);
} finally {
Arrays.fill(pkcs8, (byte) 0);
}
} else if (keyData.length == ED448_LENGTH) {
byte[] pkcs8 = Arrays.copyOf(ED448_PKCS8_PREFIX, ED448_PKCS8_PREFIX.length + ED448_LENGTH);
try {
System.arraycopy(keyData, 0, pkcs8, ED448_PKCS8_PREFIX.length, ED448_LENGTH);
return new PKCS8EncodedKeySpec(pkcs8);
} finally {
Arrays.fill(pkcs8, (byte) 0);
}
}
throw new InvalidKeyException("Private key data is neither ed25519 nor ed448");
}

}
63 changes: 63 additions & 0 deletions sshd-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,69 @@
</build>

<profiles>
<profile>
<id>test-bcfips</id>
<activation>
<property>
<name>test.bcfips</name>
<value>!disable</value>
</property>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<executions>
<execution>
<id>bcfips</id>
<goals>
<goal>test</goal>
</goals>
<configuration>
<reportsDirectory>${project.build.directory}/surefire-reports-bcfips</reportsDirectory>
<systemPropertyVariables>
<org.apache.sshd.security.fipsEnabled>true</org.apache.sshd.security.fipsEnabled>
</systemPropertyVariables>
<includes>
<include>**/SignatureFactoriesTest.java</include>
</includes>
<classpathDependencyExcludes>
<classpathDependencyExclude>net.i2p.crypto:eddsa</classpathDependencyExclude>
<classpathDependencyExclude>org.bouncycastle:bcprov-jdk18on</classpathDependencyExclude>
<classpathDependencyExclude>org.bouncycastle:bcpkix-jdk18on</classpathDependencyExclude>
<classpathDependencyExclude>org.bouncycastle:bcpg-jdk18on</classpathDependencyExclude>
<classpathDependencyExclude>org.bouncycastle:bcutil-jdk18on</classpathDependencyExclude>
</classpathDependencyExcludes>
<additionalClasspathDependencies>
<additionalClasspathDependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bc-fips</artifactId>
<version>2.0.1</version>
</additionalClasspathDependency>
<additionalClasspathDependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-fips</artifactId>
<version>2.0.10</version>
</additionalClasspathDependency>
<additionalClasspathDependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpg-fips</artifactId>
<version>2.0.12</version>
</additionalClasspathDependency>
<additionalClasspathDependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcutil-fips</artifactId>
<version>2.0.5</version>
</additionalClasspathDependency>
</additionalClasspathDependencies>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>test-jce</id>
<activation>
Expand Down