Skip to content

Commit 1ef64dc

Browse files
committed
GH-856: Fix using BC-FIPS
BC-FIPS is quite different from normal BC when it comes to ed25519 keys. FIPS 140-3 includes ed25519. (140-2 didn't.) Back-port generic ed25519 operations from the 3.0.0 milestone branch and use them to get raw key bytes, or to construct keys from raw bytes. This avoids the need to use BC-specific classes that differ between BC-FIPS and normal BC. Add a test run that executes a test using BC-FIPS instead of plain BC. The test exercises all kinds of public key types.
1 parent e82a9d3 commit 1ef64dc

5 files changed

Lines changed: 263 additions & 19 deletions

File tree

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
* [GH-809](https://github.com/apache/mina-sshd/pull/809) Fix server-side authentication for FIDO/U2F sk-* keys with flags in `authorized_keys`
3434
* [GH-827](https://github.com/apache/mina-sshd/issues/827) Don't fail on invalid `known_hosts` lines; log and skip them
3535
* [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
36+
* [GH-856](https://github.com/apache/mina-sshd/issues/856) FIX using ed25519 with BC-FIPS
3637

3738
## New Features
3839

sshd-common/src/main/java/org/apache/sshd/common/util/security/SecurityUtils.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -619,9 +619,6 @@ public static boolean isNetI2pCryptoEdDSARegistered() {
619619
}
620620

621621
public static Optional<EdDSASupport<?, ?>> getEdDSASupport() {
622-
if (isFipsMode()) {
623-
return Optional.empty();
624-
}
625622
register();
626623

627624
synchronized (REGISTERED_PROVIDERS) {

sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/bouncycastle/BouncyCastleEdDSASupport.java

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.security.PublicKey;
2929
import java.security.spec.KeySpec;
3030
import java.security.spec.PKCS8EncodedKeySpec;
31+
import java.security.spec.X509EncodedKeySpec;
3132

3233
import org.apache.sshd.common.config.keys.PrivateKeyEntryDecoder;
3334
import org.apache.sshd.common.config.keys.PublicKeyEntryDecoder;
@@ -36,17 +37,13 @@
3637
import org.apache.sshd.common.util.buffer.Buffer;
3738
import org.apache.sshd.common.util.security.SecurityUtils;
3839
import org.apache.sshd.common.util.security.eddsa.generic.EdDSASupport;
40+
import org.apache.sshd.common.util.security.eddsa.generic.EdDSAUtils;
3941
import org.apache.sshd.common.util.security.eddsa.generic.GenericEd25519PublicKeyDecoder;
4042
import org.apache.sshd.common.util.security.eddsa.generic.GenericOpenSSHEd25519PrivateKeyEntryDecoder;
4143
import org.apache.sshd.common.util.security.eddsa.generic.GenericSignatureEd25519;
42-
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
43-
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
44-
import org.bouncycastle.crypto.util.PrivateKeyFactory;
45-
import org.bouncycastle.crypto.util.PrivateKeyInfoFactory;
4644
import org.bouncycastle.jcajce.interfaces.EdDSAKey;
4745
import org.bouncycastle.jcajce.interfaces.EdDSAPrivateKey;
4846
import org.bouncycastle.jcajce.interfaces.EdDSAPublicKey;
49-
import org.bouncycastle.jcajce.spec.RawEncodedKeySpec;
5047

5148
public class BouncyCastleEdDSASupport implements EdDSASupport<EdDSAPublicKey, EdDSAPrivateKey> {
5249

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

114111
@Override
115112
public EdDSAPublicKey generateEDDSAPublicKey(byte[] seed) throws GeneralSecurityException {
116-
RawEncodedKeySpec keySpec = new RawEncodedKeySpec(seed);
117113
KeyFactory factory = SecurityUtils.getKeyFactory(getKeyFactoryAlgorithm());
118-
return (EdDSAPublicKey) factory.generatePublic(keySpec);
114+
return (EdDSAPublicKey) factory.generatePublic(EdDSAUtils.createPublicKeySpec(seed));
119115
}
120116

121117
@Override
122118
public EdDSAPrivateKey generateEDDSAPrivateKey(byte[] seed) throws GeneralSecurityException, IOException {
123-
Ed25519PrivateKeyParameters parameters = new Ed25519PrivateKeyParameters(seed);
124-
PrivateKeyInfo info = PrivateKeyInfoFactory.createPrivateKeyInfo(parameters);
125119
KeyFactory factory = SecurityUtils.getKeyFactory(getKeyFactoryAlgorithm());
126-
return (EdDSAPrivateKey) factory.generatePrivate(new PKCS8EncodedKeySpec(info.getEncoded()));
120+
return (EdDSAPrivateKey) factory.generatePrivate(EdDSAUtils.createPrivateKeySpec(seed));
127121
}
128122

129123
@Override
130124
public <B extends Buffer> B putRawEDDSAPublicKey(B buffer, PublicKey key) {
131125
EdDSAPublicKey edKey = ValidateUtils.checkInstanceOf(key, EdDSAPublicKey.class, "Not an EDDSA public key: %s", key);
132-
buffer.putBytes(edKey.getPointEncoding());
126+
try {
127+
buffer.putBytes(EdDSAUtils.getBytes(edKey));
128+
} catch (InvalidKeyException e) {
129+
throw new IllegalArgumentException(e.getMessage(), e);
130+
}
133131
return buffer;
134132
}
135133

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

143141
@Override
144142
public KeySpec createPublicKeySpec(EdDSAPublicKey publicKey) {
145-
return new RawEncodedKeySpec(publicKey.getPointEncoding());
143+
return new X509EncodedKeySpec(publicKey.getEncoded());
146144
}
147145

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

153151
@Override
154152
public byte[] getPublicKeyData(EdDSAPublicKey publicKey) {
155-
return publicKey == null ? null : publicKey.getPointEncoding();
153+
try {
154+
return publicKey == null ? null : EdDSAUtils.getBytes(publicKey);
155+
} catch (InvalidKeyException e) {
156+
throw new IllegalArgumentException(e.getMessage(), e);
157+
}
156158
}
157159

158160
@Override
159161
public byte[] getPrivateKeyData(EdDSAPrivateKey privateKey) throws IOException {
160-
Ed25519PrivateKeyParameters parameters
161-
= (Ed25519PrivateKeyParameters) PrivateKeyFactory.createKey(privateKey.getEncoded());
162-
return parameters.getEncoded();
162+
try {
163+
return EdDSAUtils.getBytes(privateKey);
164+
} catch (InvalidKeyException e) {
165+
throw new IOException(e.getMessage(), e);
166+
}
163167
}
164168

165169
@Override

sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/generic/EdDSAUtils.java

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,17 @@
1818
*/
1919
package org.apache.sshd.common.util.security.eddsa.generic;
2020

21+
import java.io.IOException;
2122
import java.security.InvalidKeyException;
23+
import java.security.PrivateKey;
2224
import java.security.PublicKey;
25+
import java.security.spec.KeySpec;
26+
import java.security.spec.PKCS8EncodedKeySpec;
27+
import java.security.spec.X509EncodedKeySpec;
2328
import java.util.Arrays;
2429

30+
import org.apache.sshd.common.util.io.der.DERParser;
31+
2532
/**
2633
* Utilities to extract the raw key bytes from ed25519 or ed448 public keys, in a manner that is independent of the
2734
* actual concrete key implementation classes.
@@ -44,10 +51,43 @@ public final class EdDSAUtils {
4451
private static final byte[] ED448_X509_PREFIX = {
4552
0x30, 0x43, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x71, 0x03, 0x3a, 0x00 };
4653

54+
// For reconstructing private keys from raw bytes we construct a minimal PKCS#8 encoding, using RFC 5208 (version 0)
55+
// without the public key and without attributes. This is allowed by RFC 5958 (Asymmetric key packages).
56+
57+
// Sequence, length 46, (3 bytes: Version 0), Sequence, length 5, OID, length 3, O, I, D, Octet String, length 34,
58+
// Octet String, length 32
59+
private static final byte[] ED25519_PKCS8_PREFIX = {
60+
0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70,
61+
0x04, 0x22, 0x04, 0x20 };
62+
// Sequence, length 71, (3 bytes: Version 0), Sequence, length 5, OID, length 3, O, I, D, Octet String, length 59,
63+
// Octet String, length 57
64+
private static final byte[] ED448_PKCS8_PREFIX = {
65+
0x30, 0x47, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x71,
66+
0x04, 0x3b, 0x04, 0x39 };
67+
68+
// The first two numbers of the dotted notation are combined into one byte: (1 * 40 + 3) = 43 = 0x2b
69+
private static final byte[] ED25519_OID = { 0x2b, 0x65, 0x70 }; // 1.3.101.112
70+
private static final byte[] ED448_OID = { 0x2b, 0x65, 0x71 }; // 1.3.101.113
71+
4772
private EdDSAUtils() {
4873
throw new IllegalStateException("No instantiation");
4974
}
5075

76+
private static boolean arrayEq(byte[] a, byte[] b) {
77+
if (a == null && b == null) {
78+
return true;
79+
}
80+
if (a == null || b == null) {
81+
return false;
82+
}
83+
int unequal = a.length ^ b.length;
84+
int length = Math.min(a.length, b.length);
85+
for (int i = 0; i < length; i++) {
86+
unequal |= a[i] ^ b[i];
87+
}
88+
return unequal == 0;
89+
}
90+
5191
private static boolean startsWith(byte[] data, byte[] prefix) {
5292
if (data == null || prefix == null || prefix.length == 0 || data.length < prefix.length) {
5393
return false;
@@ -86,4 +126,143 @@ public static byte[] getBytes(PublicKey key) throws InvalidKeyException {
86126
}
87127
return Arrays.copyOfRange(encoded, encoded.length - n, encoded.length);
88128
}
129+
130+
/**
131+
* Retrieves the raw key bytes from an ed25519 or ed448 {@link PrivateKey}.
132+
*
133+
* @param key {@link PrivateKey} to get the bytes of
134+
* @return the raw key bytes
135+
* @throws InvalidKeyException if the key is not an ed25519 or ed448 key, or if it doesn't use PKCS#8 encoding
136+
*/
137+
public static byte[] getBytes(PrivateKey key) throws InvalidKeyException {
138+
// Extract the private key bytes from the PKCS#8 encoding.
139+
if (!"PKCS#8".equalsIgnoreCase(key.getFormat())) {
140+
throw new InvalidKeyException("Cannot extract private key bytes from a non-PKCS#8 encoding");
141+
}
142+
byte[] encoded = key.getEncoded();
143+
if (encoded == null) {
144+
throw new InvalidKeyException("Private key " + key.getClass().getCanonicalName() + " does not support encoding");
145+
}
146+
try {
147+
return asn1Parse(encoded);
148+
} finally {
149+
Arrays.fill(encoded, (byte) 0);
150+
}
151+
}
152+
153+
/**
154+
* Extracts the private key bytes from an encoded EdDSA private key by parsing the bytes as ASN.1 according to RFC
155+
* 5958 (PKCS #8 encoding):
156+
*
157+
* <pre>
158+
* OneAsymmetricKey ::= SEQUENCE {
159+
* version Version,
160+
* privateKeyAlgorithm PrivateKeyAlgorithmIdentifier,
161+
* privateKey PrivateKey,
162+
* ...
163+
* }
164+
*
165+
* Version ::= INTEGER
166+
* PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier
167+
* PrivateKey ::= OCTET STRING
168+
*
169+
* AlgorithmIdentifier ::= SEQUENCE {
170+
* algorithm OBJECT IDENTIFIER,
171+
* parameters ANY DEFINED BY algorithm OPTIONAL
172+
* }
173+
* </pre>
174+
* <p>
175+
* and RFC 8410: "... when encoding a OneAsymmetricKey object, the private key is wrapped in a CurvePrivateKey
176+
* object and wrapped by the OCTET STRING of the 'privateKey' field."
177+
* </p>
178+
*
179+
* <pre>
180+
* CurvePrivateKey ::= OCTET STRING
181+
* </pre>
182+
*
183+
* @param encoded encoded private key to extract the private key bytes from
184+
* @return the extracted private key bytes
185+
* @throws InvalidKeyException if the private key cannot be extracted
186+
* @see <a href="https://tools.ietf.org/html/rfc5958">RFC 5958</a>
187+
* @see <a href="https://tools.ietf.org/html/rfc8410">RFC 8410</a>
188+
*/
189+
private static byte[] asn1Parse(byte[] encoded) throws InvalidKeyException {
190+
byte[] privateKey = null;
191+
try (DERParser byteParser = new DERParser(encoded);
192+
DERParser oneAsymmetricKey = byteParser.readObject().createParser()) {
193+
oneAsymmetricKey.readObject(); // Skip version
194+
int n;
195+
try (DERParser algorithmIdentifier = oneAsymmetricKey.readObject().createParser()) {
196+
byte[] oid = algorithmIdentifier.readObject().getValue();
197+
if (arrayEq(ED25519_OID, oid)) {
198+
n = ED25519_LENGTH;
199+
} else if (arrayEq(ED448_OID, oid)) {
200+
n = ED448_LENGTH;
201+
} else {
202+
throw new InvalidKeyException("Private key is neither ed25519 nor ed448");
203+
}
204+
}
205+
privateKey = oneAsymmetricKey.readObject().getValue();
206+
// The last n bytes of this must be the private key bytes.
207+
return Arrays.copyOfRange(privateKey, privateKey.length - n, privateKey.length);
208+
// Depending on the version there may be optional stuff following, but we don't care about that.
209+
} catch (IOException e) {
210+
throw new InvalidKeyException("Cannot parse EdDSA private key", e);
211+
} finally {
212+
if (privateKey != null) {
213+
Arrays.fill(privateKey, (byte) 0);
214+
}
215+
}
216+
}
217+
218+
/**
219+
* Creates a {@link KeySpec} for re-creating an ed25519 or ed448 public key from the raw key bytes.
220+
*
221+
* @param keyData the raw key bytes
222+
* @return the {@link KeySpec}
223+
* @throws InvalidKeyException if the key bytes do not have the appropriate length for an ed25519 or ed448 key
224+
*/
225+
public static KeySpec createPublicKeySpec(byte[] keyData) throws InvalidKeyException {
226+
// Create an X.509 encoding for ed25519 or ed448, depending on the length of keyData.
227+
if (keyData.length == ED25519_LENGTH) {
228+
byte[] x509 = Arrays.copyOf(ED25519_X509_PREFIX, ED25519_X509_PREFIX.length + ED25519_LENGTH);
229+
System.arraycopy(keyData, 0, x509, ED25519_X509_PREFIX.length, ED25519_LENGTH);
230+
return new X509EncodedKeySpec(x509);
231+
} else if (keyData.length == ED448_LENGTH) {
232+
byte[] x509 = Arrays.copyOf(ED448_X509_PREFIX, ED448_X509_PREFIX.length + ED448_LENGTH);
233+
System.arraycopy(keyData, 0, x509, ED448_X509_PREFIX.length, ED448_LENGTH);
234+
return new X509EncodedKeySpec(x509);
235+
}
236+
throw new InvalidKeyException("Public key data is neither ed25519 nor ed448");
237+
}
238+
239+
/**
240+
* Creates a {@link KeySpec} for re-creating an ed25519 or ed448 public key from the raw key bytes.
241+
*
242+
* @param keyData the raw key bytes
243+
* @return the {@link KeySpec}
244+
* @throws InvalidKeyException if the key bytes do not have the appropriate length for an ed25519 or ed448 key
245+
*/
246+
public static KeySpec createPrivateKeySpec(byte[] keyData) throws InvalidKeyException {
247+
// Create a PKCS#8 encoding for ed25519 or ed448, depending on the length of keyData.
248+
if (keyData.length == ED25519_LENGTH) {
249+
byte[] pkcs8 = Arrays.copyOf(ED25519_PKCS8_PREFIX, ED25519_PKCS8_PREFIX.length + ED25519_LENGTH);
250+
try {
251+
System.arraycopy(keyData, 0, pkcs8, ED25519_PKCS8_PREFIX.length, ED25519_LENGTH);
252+
return new PKCS8EncodedKeySpec(pkcs8);
253+
} finally {
254+
Arrays.fill(pkcs8, (byte) 0);
255+
}
256+
} else if (keyData.length == ED448_LENGTH) {
257+
byte[] pkcs8 = Arrays.copyOf(ED448_PKCS8_PREFIX, ED448_PKCS8_PREFIX.length + ED448_LENGTH);
258+
try {
259+
System.arraycopy(keyData, 0, pkcs8, ED448_PKCS8_PREFIX.length, ED448_LENGTH);
260+
return new PKCS8EncodedKeySpec(pkcs8);
261+
} finally {
262+
Arrays.fill(pkcs8, (byte) 0);
263+
}
264+
}
265+
throw new InvalidKeyException("Private key data is neither ed25519 nor ed448");
266+
}
267+
89268
}

sshd-core/pom.xml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,69 @@
229229
</build>
230230

231231
<profiles>
232+
<profile>
233+
<id>test-bcfips</id>
234+
<activation>
235+
<property>
236+
<name>test.bcfips</name>
237+
<value>!disable</value>
238+
</property>
239+
</activation>
240+
<build>
241+
<plugins>
242+
<plugin>
243+
<groupId>org.apache.maven.plugins</groupId>
244+
<artifactId>maven-surefire-plugin</artifactId>
245+
<executions>
246+
<execution>
247+
<id>bcfips</id>
248+
<goals>
249+
<goal>test</goal>
250+
</goals>
251+
<configuration>
252+
<reportsDirectory>${project.build.directory}/surefire-reports-bcfips</reportsDirectory>
253+
<systemPropertyVariables>
254+
<org.apache.sshd.security.fipsEnabled>true</org.apache.sshd.security.fipsEnabled>
255+
</systemPropertyVariables>
256+
<includes>
257+
<include>**/SignatureFactoriesTest.java</include>
258+
</includes>
259+
<classpathDependencyExcludes>
260+
<classpathDependencyExclude>net.i2p.crypto:eddsa</classpathDependencyExclude>
261+
<classpathDependencyExclude>org.bouncycastle:bcprov-jdk18on</classpathDependencyExclude>
262+
<classpathDependencyExclude>org.bouncycastle:bcpkix-jdk18on</classpathDependencyExclude>
263+
<classpathDependencyExclude>org.bouncycastle:bcpg-jdk18on</classpathDependencyExclude>
264+
<classpathDependencyExclude>org.bouncycastle:bcutil-jdk18on</classpathDependencyExclude>
265+
</classpathDependencyExcludes>
266+
<additionalClasspathDependencies>
267+
<additionalClasspathDependency>
268+
<groupId>org.bouncycastle</groupId>
269+
<artifactId>bc-fips</artifactId>
270+
<version>2.0.1</version>
271+
</additionalClasspathDependency>
272+
<additionalClasspathDependency>
273+
<groupId>org.bouncycastle</groupId>
274+
<artifactId>bcpkix-fips</artifactId>
275+
<version>2.0.10</version>
276+
</additionalClasspathDependency>
277+
<additionalClasspathDependency>
278+
<groupId>org.bouncycastle</groupId>
279+
<artifactId>bcpg-fips</artifactId>
280+
<version>2.0.12</version>
281+
</additionalClasspathDependency>
282+
<additionalClasspathDependency>
283+
<groupId>org.bouncycastle</groupId>
284+
<artifactId>bcutil-fips</artifactId>
285+
<version>2.0.5</version>
286+
</additionalClasspathDependency>
287+
</additionalClasspathDependencies>
288+
</configuration>
289+
</execution>
290+
</executions>
291+
</plugin>
292+
</plugins>
293+
</build>
294+
</profile>
232295
<profile>
233296
<id>test-jce</id>
234297
<activation>

0 commit comments

Comments
 (0)