diff --git a/CHANGES.md b/CHANGES.md index e4b23cf47..33518b0c1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -41,17 +41,39 @@ * [GH-524](https://github.com/apache/mina-sshd/issues/524) Performance improvements * [GH-533](https://github.com/apache/mina-sshd/issues/533) Fix multi-step authentication * [GH-582](https://github.com/apache/mina-sshd/issues/582) Fix filtering in `NamedFactory` +* [GH-590](https://github.com/apache/mina-sshd/issues/590) Better support for FIPS ## New Features * New utility methods `SftpClient.put(Path localFile, String remoteFileName)` and `SftpClient.put(InputStream in, String remoteFileName)` facilitate SFTP file uploading. +### [GH-590](https://github.com/apache/mina-sshd/issues/590) Better support for FIPS + +Besides fixing a bug with bc-fips (the `RandomGenerator` class exists in normal Bouncy Castle, +but not in the FIPS version, but Apache MINA sshd referenced it even if only bc-fips was present), +support was improved for running in an environment restricted by FIPS. + +There is a new system property `org.apache.sshd.security.fipsEnabled`. If set to `true`, a number +of crypto-algorithms not approved by FIPS 140 are disabled: + +* key exchange methods sntrup761x25519-sha512, sntrup761x25519-sha512@openssh.com, curve25519-sha256, curve25519-sha256@libssh.org, curve448-sha512. +* the chacha20-poly1305 cipher. +* the bcrypt KDF used in encrypted private key files in [OpenSSH format](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key). +* all ed25519 keys and signatures. + +Additionally, the new "SunJCEWrapper" `SecurityProviderRegistrar` (see below) and the +`EdDSASecurityProviderRegistrar` are disabled, and the `BouncyCastleScurityProviderRegistrar` +looks only for the "BCFIPS" security provider, not for the normal "BC" provider. + +If the system property is _not_ set to `true`, FIPS mode can be enabled programmatically +by calling `SeurityUtils.setFipsMode()` before any other call to Apache MINA sshd. + ## Potential compatibility issues ### New security provider registrar There is a new `SecurityProviderRegistrar` that is registered by default -if there is a `SunJCE` security provider and that uses the AES and +if there is a `SunJCE` security provider. It uses the AES and HmacSHA* implementations from `SunJCE` even if Bouncy Castle is also registered. `SunJCE` has native implementations, whereas Bouncy Castle may not. diff --git a/sshd-common/src/main/java/org/apache/sshd/common/cipher/BuiltinCiphers.java b/sshd-common/src/main/java/org/apache/sshd/common/cipher/BuiltinCiphers.java index 671bc53a1..71fbc7a69 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/cipher/BuiltinCiphers.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/cipher/BuiltinCiphers.java @@ -31,6 +31,7 @@ import java.util.Set; import java.util.SortedSet; import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -39,6 +40,7 @@ import org.apache.sshd.common.config.NamedFactoriesListParseResult; import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.ValidateUtils; +import org.apache.sshd.common.util.security.SecurityUtils; /** * Provides easy access to the currently implemented ciphers @@ -51,6 +53,11 @@ public enum BuiltinCiphers implements CipherFactory { public Cipher create() { return new CipherNone(); } + + @Override + public boolean isSupported() { + return !SecurityUtils.isFipsMode(); + } }, aes128cbc(Constants.AES128_CBC, 16, 0, 16, "AES", 128, "AES/CBC/NoPadding", 16) { @Override @@ -149,6 +156,11 @@ public Cipher create() { public Cipher create() { return new ChaCha20Cipher(); } + + @Override + public boolean isSupported() { + return !SecurityUtils.isFipsMode(); + } }, /** * @deprecated @@ -175,7 +187,7 @@ public Cipher create() { private final int blkSize; private final String algorithm; private final String transformation; - private final boolean supported; + private final AtomicReference supported = new AtomicReference<>(); BuiltinCiphers( String factoryName, int ivsize, int authSize, int kdfSize, @@ -188,13 +200,6 @@ public Cipher create() { this.algorithm = algorithm; this.transformation = transformation; this.blkSize = blkSize; - /* - * This can be done once since in order to change the support the JVM needs to be stopped, some - * unlimited-strength files need be installed and then the JVM re-started. Therefore, the answer is not going to - * change while the JVM is running - */ - this.supported = Constants.NONE.equals(factoryName) || Constants.CC20P1305_OPENSSH.equals(factoryName) - || Cipher.checkSupported(this.transformation, this.keysize); } @Override @@ -213,7 +218,14 @@ public final String toString() { */ @Override public boolean isSupported() { - return supported; + Boolean value = supported.get(); + if (value == null) { + value = Cipher.checkSupported(this.transformation, this.keysize); + if (!supported.compareAndSet(null, value)) { + value = supported.get(); + } + } + return value.booleanValue(); } @Override diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParser.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParser.java index 01c8fd35d..3fd1c1d5a 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParser.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParser.java @@ -196,6 +196,9 @@ protected OpenSSHKdfOptions resolveKdfOptions( OpenSSHKdfOptions options; // TODO define a factory class where users can register extra KDF options if (BCryptKdfOptions.NAME.equalsIgnoreCase(kdfName)) { + if (SecurityUtils.isFipsMode()) { + throw new NoSuchAlgorithmException(BCryptKdfOptions.NAME + " is disabled in FIPS mode"); + } options = new BCryptKdfOptions(); } else { options = new RawKdfOptions(); diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/writer/openssh/OpenSSHKeyPairResourceWriter.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/writer/openssh/OpenSSHKeyPairResourceWriter.java index f9402c45e..8a9653add 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/writer/openssh/OpenSSHKeyPairResourceWriter.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/writer/openssh/OpenSSHKeyPairResourceWriter.java @@ -27,6 +27,7 @@ import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.SecureRandom; @@ -50,8 +51,11 @@ import org.apache.sshd.common.config.keys.loader.openssh.kdf.BCrypt; import org.apache.sshd.common.config.keys.loader.openssh.kdf.BCryptKdfOptions; import org.apache.sshd.common.config.keys.writer.KeyPairResourceWriter; +import org.apache.sshd.common.random.JceRandomFactory; +import org.apache.sshd.common.random.Random; import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.io.output.SecureByteArrayOutputStream; +import org.apache.sshd.common.util.security.SecurityUtils; /** * A {@link KeyPairResourceWriter} for writing keys in the modern OpenSSH format, using the OpenBSD bcrypt KDF for @@ -272,14 +276,18 @@ public byte[] getKdfOptions() { @Override protected byte[] deriveEncryptionKey(PrivateKeyEncryptionContext context, int keyLength) throws IOException, GeneralSecurityException { + if (SecurityUtils.isFipsMode()) { + throw new NoSuchAlgorithmException(BCryptKdfOptions.NAME + " is disabled in FIPS mode"); + } + byte[] iv = context.getInitVector(); if (iv == null) { iv = generateInitializationVector(context); } byte[] salt = new byte[BCRYPT_SALT_LENGTH]; - SecureRandom random = new SecureRandom(); - random.nextBytes(salt); + Random random = JceRandomFactory.INSTANCE.create(); + random.fill(salt); byte[] kdfOutput = new byte[keyLength + iv.length]; BCrypt bcrypt = new BCrypt(); diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/security/SecurityUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/util/security/SecurityUtils.java index 5b59273d2..c45510c7d 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/util/security/SecurityUtils.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/security/SecurityUtils.java @@ -165,6 +165,15 @@ public final class SecurityUtils { public static final String PROP_DEFAULT_SECURITY_PROVIDER = "org.apache.sshd.security.defaultProvider"; + /** + * A boolean system property that can be set to {@code "true"} to enable FIPS mode. In FIPS mode, crypto-algorithms + * not approved in FIPS-140 will not be available. + *

+ * Note: if this system property is not {@code "true"}, it can be overridden via {@link #setFipsMode()}. + *

+ */ + public static final String FIPS_ENABLED = "org.apache.sshd.security.fipsEnabled"; + private static final AtomicInteger MIN_DHG_KEY_SIZE_HOLDER = new AtomicInteger(0); private static final AtomicInteger MAX_DHG_KEY_SIZE_HOLDER = new AtomicInteger(0); @@ -181,12 +190,44 @@ public final class SecurityUtils { private static final AtomicReference DEFAULT_PROVIDER_HOLDER = new AtomicReference<>(); + private static final AtomicReference FIPS_MODE = new AtomicReference<>(); + private static Boolean hasEcc; private SecurityUtils() { throw new UnsupportedOperationException("No instance"); } + /** + * Unconditionally set FIPS mode, overriding the {@link #FIPS_ENABLED} system property. + * + * @throws IllegalStateException if a call to {@link #isFipsMode()} had already occurred and returned {@code false}. + */ + public static void setFipsMode() { + if (!FIPS_MODE.compareAndSet(null, Boolean.TRUE)) { + if (!Boolean.TRUE.equals(FIPS_MODE.get())) { + throw new IllegalStateException("FIPS mode was already set to FALSE"); + } + } + } + + /** + * Tells whether FIPS mode is enabled, either through the system property {@link #FIPS_ENABLED} or via + * {@link #setFipsMode()}. + * + * @return {@code true} if FIPS mode is enabled, {@code false} otherwise. + */ + public static boolean isFipsMode() { + Boolean value = FIPS_MODE.get(); + if (FIPS_MODE.get() == null) { + value = Boolean.getBoolean(FIPS_ENABLED); + if (!FIPS_MODE.compareAndSet(null, value)) { + value = FIPS_MODE.get(); + } + } + return value; + } + /** * @param name The provider's name - never {@code null}/empty * @return {@code true} if the provider is marked as disabled a-priori @@ -565,6 +606,9 @@ public static RandomFactory getRandomFactory() { * @return {@code true} if EDDSA curves (e.g., {@code ed25519}) are supported */ public static boolean isEDDSACurveSupported() { + if (isFipsMode()) { + return false; + } register(); SecurityProviderRegistrar r = getRegisteredProvider(EDDSA); diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/security/SunJCESecurityProviderRegistrar.java b/sshd-common/src/main/java/org/apache/sshd/common/util/security/SunJCESecurityProviderRegistrar.java index 5c4a33ec0..b17c2df40 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/util/security/SunJCESecurityProviderRegistrar.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/security/SunJCESecurityProviderRegistrar.java @@ -61,7 +61,7 @@ public SunJCESecurityProviderRegistrar() { @Override public boolean isEnabled() { - if (!super.isEnabled()) { + if (SecurityUtils.isFipsMode() || !super.isEnabled()) { return false; } return isSupported(); diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/security/bouncycastle/BouncyCastleSecurityProviderRegistrar.java b/sshd-common/src/main/java/org/apache/sshd/common/util/security/bouncycastle/BouncyCastleSecurityProviderRegistrar.java index d60d3f4e7..f5cf4c549 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/util/security/bouncycastle/BouncyCastleSecurityProviderRegistrar.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/security/bouncycastle/BouncyCastleSecurityProviderRegistrar.java @@ -132,8 +132,11 @@ public boolean isSupported() { if (supported != null) { return supported.booleanValue(); } - - Class clazz = ThreadUtils.resolveDefaultClass(getClass(), PROVIDER_CLASS); + boolean requireFips = SecurityUtils.isFipsMode(); + Class clazz = null; + if (!requireFips) { + clazz = ThreadUtils.resolveDefaultClass(getClass(), PROVIDER_CLASS); + } if (clazz == null) { clazz = ThreadUtils.resolveDefaultClass(getClass(), FIPS_PROVIDER_CLASS); } @@ -145,7 +148,7 @@ public boolean isSupported() { Provider provider = Security.getProvider(BCFIPS_PROVIDER_NAME); if (provider != null) { providerName = BCFIPS_PROVIDER_NAME; - } else { + } else if (!requireFips) { provider = Security.getProvider(BC_PROVIDER_NAME); if (provider != null) { providerName = BC_PROVIDER_NAME; diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/EdDSASecurityProviderRegistrar.java b/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/EdDSASecurityProviderRegistrar.java index 3aada0722..8fbd28c7d 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/EdDSASecurityProviderRegistrar.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/EdDSASecurityProviderRegistrar.java @@ -44,7 +44,7 @@ public EdDSASecurityProviderRegistrar() { @Override public boolean isEnabled() { - if (!super.isEnabled()) { + if (SecurityUtils.isFipsMode() || !super.isEnabled()) { return false; } diff --git a/sshd-core/src/main/java/org/apache/sshd/common/kex/MontgomeryCurve.java b/sshd-core/src/main/java/org/apache/sshd/common/kex/MontgomeryCurve.java index 3f6904f47..2d3bb8a32 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/kex/MontgomeryCurve.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/kex/MontgomeryCurve.java @@ -136,7 +136,7 @@ public int getKeySize() { @Override public boolean isSupported() { - return supported; + return supported && !SecurityUtils.isFipsMode(); } public KeyAgreement createKeyAgreement() throws GeneralSecurityException { diff --git a/sshd-core/src/main/java/org/apache/sshd/common/kex/SNTRUP761.java b/sshd-core/src/main/java/org/apache/sshd/common/kex/SNTRUP761.java index 313aa3dc7..d423d882b 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/kex/SNTRUP761.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/kex/SNTRUP761.java @@ -21,6 +21,7 @@ import java.security.SecureRandom; import java.util.Arrays; +import org.apache.sshd.common.util.security.SecurityUtils; import org.bouncycastle.crypto.AsymmetricCipherKeyPair; import org.bouncycastle.crypto.SecretWithEncapsulation; import org.bouncycastle.pqc.crypto.ntruprime.SNTRUPrimeKEMExtractor; @@ -41,6 +42,9 @@ private SNTRUP761() { } static boolean isSupported() { + if (SecurityUtils.isFipsMode()) { + return false; + } try { return SNTRUPrimeParameters.sntrup761.getSessionKeySize() == 256; // BC < 1.78 had only 128 } catch (Throwable e) {