From 56571c6a2b21f9a546ea31f76575b9f41450424e Mon Sep 17 00:00:00 2001 From: Thomas Wolf Date: Tue, 3 Sep 2024 21:52:30 +0200 Subject: [PATCH] GH-590: provide a single flag to enable FIPS mode Add a flag in SecurityUtils to enable FIPS mode. In FIPS mode, algorithms known to be not FIPS-compliant are had disabled and not available. The BouncyCastleSecurityRegistrar only considers bc-fips, and the SunJCESecurityRegistrar and the EdDSASecurityRegistrar are disabled. The ChaCha20-Poly1305 cipher is disabled, ed25519 signatures are disabled, the bcrypt KDF used in OpenSSH-format encrypted private keys[1] is disabled, and the curve25519 and curve448 key exchange methods are disabled. Also disabled is the post-quantum sntrup761x25519-sha512 key exchange method. These disabled algorithms are not approved in FIPS 140. The flag can be set via a system property or by calling SecurityUtils.setFipsMode(). The system property is "org.apache.sshd.security.fipsEnabled" and takes the boolean value "true". Any other value does not enable FIPS mode. [1] https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key --- .../sshd/common/cipher/BuiltinCiphers.java | 30 +++++++++---- .../openssh/OpenSSHKeyPairResourceParser.java | 3 ++ .../openssh/OpenSSHKeyPairResourceWriter.java | 12 ++++- .../common/util/security/SecurityUtils.java | 44 +++++++++++++++++++ .../SunJCESecurityProviderRegistrar.java | 2 +- ...BouncyCastleSecurityProviderRegistrar.java | 9 ++-- .../eddsa/EdDSASecurityProviderRegistrar.java | 2 +- .../sshd/common/kex/MontgomeryCurve.java | 2 +- .../org/apache/sshd/common/kex/SNTRUP761.java | 4 ++ 9 files changed, 91 insertions(+), 17 deletions(-) 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..5ab819ec3 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..541b5adad 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) {