diff --git a/crypto/default/src/main/java/org/keycloak/crypto/def/BCCertificateUtilsProvider.java b/crypto/default/src/main/java/org/keycloak/crypto/def/BCCertificateUtilsProvider.java index 311a1110d1e..b9aa926633d 100755 --- a/crypto/default/src/main/java/org/keycloak/crypto/def/BCCertificateUtilsProvider.java +++ b/crypto/default/src/main/java/org/keycloak/crypto/def/BCCertificateUtilsProvider.java @@ -49,6 +49,7 @@ import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.keycloak.common.util.BouncyIntegration; import org.keycloak.common.crypto.CertificateUtilsProvider; +import org.keycloak.crypto.JavaAlgorithm; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -188,7 +189,12 @@ public class BCCertificateUtilsProvider implements CertificateUtilsProvider { signerBuilder = new JcaContentSignerBuilder("SHA256WithECDSA") .setProvider(BouncyIntegration.PROVIDER); break; - + } + case JavaAlgorithm.Ed25519: + case JavaAlgorithm.Ed448: { + signerBuilder = new JcaContentSignerBuilder(privateKey.getAlgorithm()) + .setProvider(BouncyIntegration.PROVIDER); + break; } default: { throw new RuntimeException(String.format("Keytype %s is not supported.", privateKey.getAlgorithm())); diff --git a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSCertificateUtilsProvider.java b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSCertificateUtilsProvider.java index 2f5fe596bfb..e9de665cf9e 100755 --- a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSCertificateUtilsProvider.java +++ b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSCertificateUtilsProvider.java @@ -49,6 +49,7 @@ import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.keycloak.common.util.BouncyIntegration; import org.keycloak.common.crypto.CertificateUtilsProvider; +import org.keycloak.crypto.JavaAlgorithm; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -193,7 +194,12 @@ public class BCFIPSCertificateUtilsProvider implements CertificateUtilsProvider{ signerBuilder = new JcaContentSignerBuilder("SHA256WithECDSA") .setProvider(BouncyIntegration.PROVIDER); break; - + } + case JavaAlgorithm.Ed25519: + case JavaAlgorithm.Ed448: { + signerBuilder = new JcaContentSignerBuilder(privateKey.getAlgorithm()) + .setProvider(BouncyIntegration.PROVIDER); + break; } default: { throw new RuntimeException(String.format("Keytype %s is not supported.", privateKey.getAlgorithm())); diff --git a/docs/documentation/server_admin/topics/realms/keys.adoc b/docs/documentation/server_admin/topics/realms/keys.adoc index 4d613117cee..2e16d826d76 100644 --- a/docs/documentation/server_admin/topics/realms/keys.adoc +++ b/docs/documentation/server_admin/topics/realms/keys.adoc @@ -127,12 +127,20 @@ For the associated certificate chain to be loaded it must be imported to the Jav . Click the *Providers* tab. . Click *Add provider* and select *java-keystore*. . Enter a number in the *Priority* field. This number determines if the new key pair becomes the active key pair. -. Enter a value for *Keystore*. -. Enter a value for *Keystore Password*. -. Enter a value for *Key Alias*. -. Enter a value for *Key Password*. +. Enter the desired *Algorithm*. Note that the algorithm should match the key type (for example `RS256` requires a RSA private key, `ES256` a EC private key or `AES` an AES secret key). +. Enter a value for *Keystore*. Path to the keystore file. +. Enter the *Keystore Password*. The option can refer a value from an external <<_vault-administration,vault>>. +. Enter a value for *Keystore Type* (`JKS`, `PKCS12` or `BCFKS`). +. Enter a value for the *Key Alias* to load from the keystore. +. Enter the *Key Password*. The option can refer a value from an external <<_vault-administration,vault>>. +. Enter a value for *Key Use* (`sig` for signing or `enc` for encryption). Note that the use should match the algorithm type (for example `RS256` is `sig` but `RSA-OAEP` is `enc`) . Click *Save*. +[WARNING] +==== +Not all the keystore types support all types of keys. `JKS` and `PKCS12` in fips modes (provider `BCFIPS`) cannot store secret key entries. +==== + ==== Making keys passive .Procedure diff --git a/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProvider.java b/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProvider.java index 35b6eb1a449..c2eb0ef1ab7 100644 --- a/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProvider.java +++ b/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProvider.java @@ -17,38 +17,42 @@ package org.keycloak.keys; -import org.keycloak.common.crypto.CryptoIntegration; -import org.keycloak.common.util.CertificateUtils; import org.keycloak.common.util.KeyUtils; import org.keycloak.common.util.KeystoreUtil; import org.keycloak.component.ComponentModel; import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.JavaAlgorithm; import org.keycloak.crypto.KeyStatus; import org.keycloak.crypto.KeyType; import org.keycloak.crypto.KeyUse; import org.keycloak.crypto.KeyWrapper; import org.keycloak.jose.jwe.JWEConstants; import org.keycloak.models.RealmModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.vault.VaultTranscriber; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.security.GeneralSecurityException; +import java.security.Key; import java.security.KeyPair; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; import java.security.PublicKey; import java.security.UnrecoverableKeyException; import java.security.cert.CertPath; import java.security.cert.CertPathValidator; +import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.PKIXParameters; import java.security.cert.TrustAnchor; import java.security.cert.X509Certificate; import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.EdECPrivateKey; +import java.security.interfaces.RSAPrivateCrtKey; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -57,6 +61,7 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.crypto.SecretKey; /** * @author Stian Thorgersen @@ -67,15 +72,18 @@ public class JavaKeystoreKeyProvider implements KeyProvider { private final ComponentModel model; + private final VaultTranscriber vault; + private final KeyWrapper key; private final String algorithm; - public JavaKeystoreKeyProvider(RealmModel realm, ComponentModel model) { + public JavaKeystoreKeyProvider(RealmModel realm, ComponentModel model, VaultTranscriber vault) { this.model = model; + this.vault = vault; this.status = KeyStatus.from(model.get(Attributes.ACTIVE_KEY, true), model.get(Attributes.ENABLED_KEY, true)); - String defaultAlgorithmKey = KeyUse.ENC.name().equals(model.get(Attributes.KEY_USE)) ? JWEConstants.RSA_OAEP : Algorithm.RS256; + String defaultAlgorithmKey = KeyUse.ENC.name().equalsIgnoreCase(model.get(Attributes.KEY_USE)) ? JWEConstants.RSA_OAEP : Algorithm.RS256; this.algorithm = model.get(Attributes.ALGORITHM_KEY, defaultAlgorithmKey); if (model.hasNote(KeyWrapper.class.getName())) { @@ -93,12 +101,19 @@ public class JavaKeystoreKeyProvider implements KeyProvider { String keyAlias = model.get(JavaKeystoreKeyProviderFactory.KEY_ALIAS_KEY); return switch (algorithm) { - case Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, - Algorithm.RSA_OAEP, Algorithm.RSA1_5, Algorithm.RSA_OAEP_256 -> - loadRSAKey(realm, model, keyStore, keyAlias); - case Algorithm.ES256, Algorithm.ES384, Algorithm.ES512 -> loadECKey(realm, model, keyStore, keyAlias); - default -> - throw new RuntimeException(String.format("Keys for algorithm %s are not supported.", algorithm)); + case Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512 -> + loadRSAKey(keyStore, keyAlias, KeyUse.SIG); + case Algorithm.RSA_OAEP, Algorithm.RSA1_5, Algorithm.RSA_OAEP_256 -> + loadRSAKey(keyStore, keyAlias, KeyUse.ENC); + case Algorithm.ES256, Algorithm.ES384, Algorithm.ES512 -> + loadECKey(keyStore, keyAlias, KeyUse.SIG); + case Algorithm.EdDSA -> + loadEdDSAKey(keyStore, keyAlias, KeyUse.SIG); + case Algorithm.AES -> + loadOctKey(keyStore, keyAlias, JavaAlgorithm.getJavaAlgorithm(algorithm), KeyUse.ENC); + case Algorithm.HS256, Algorithm.HS384, Algorithm.HS512 -> + loadOctKey(keyStore, keyAlias, JavaAlgorithm.getJavaAlgorithm(algorithm), KeyUse.SIG); + default -> throw new RuntimeException(String.format("Keys for algorithm %s are not supported.", algorithm)); }; } catch (KeyStoreException kse) { throw new RuntimeException("KeyStore error on server. " + kse.getMessage(), kse); @@ -111,7 +126,7 @@ public class JavaKeystoreKeyProvider implements KeyProvider { } catch (CertificateException ce) { throw new RuntimeException("Certificate error on server. " + ce.getMessage(), ce); } catch (UnrecoverableKeyException uke) { - throw new RuntimeException("Keystore on server can not be recovered. " + uke.getMessage(), uke); + throw new RuntimeException("Key in the keystore cannot be recovered. " + uke.getMessage(), uke); } catch (GeneralSecurityException gse) { throw new RuntimeException("Invalid certificate chain. Check the order of certificates.", gse); } @@ -121,42 +136,90 @@ public class JavaKeystoreKeyProvider implements KeyProvider { // Use "JKS" as default type for backwards compatibility String keystoreType = KeystoreUtil.getKeystoreType(model.get(JavaKeystoreKeyProviderFactory.KEYSTORE_TYPE_KEY), keystorePath, "JKS"); KeyStore keyStore = KeyStore.getInstance(keystoreType); - keyStore.load(inputStream, model.get(JavaKeystoreKeyProviderFactory.KEYSTORE_PASSWORD_KEY).toCharArray()); + String keystorePwd = model.get(JavaKeystoreKeyProviderFactory.KEYSTORE_PASSWORD_KEY); + keystorePwd = vault.getStringSecret(keystorePwd).get().orElse(keystorePwd); + keyStore.load(inputStream, keystorePwd.toCharArray()); return keyStore; } - - private KeyWrapper loadECKey(RealmModel realm, ComponentModel model, KeyStore keyStore, String keyAlias) throws GeneralSecurityException { - ECPrivateKey privateKey = (ECPrivateKey) keyStore.getKey(keyAlias, model.get(JavaKeystoreKeyProviderFactory.KEY_PASSWORD_KEY).toCharArray()); - String curve = AbstractEcdsaKeyProviderFactory.convertECDomainParmNistRepToSecRep(AbstractEcdsaKeyProviderFactory.convertAlgorithmToECDomainParmNistRep(algorithm)); - - PublicKey publicKey = CryptoIntegration.getProvider().getEcdsaCryptoProvider().getPublicFromPrivate(privateKey); - - KeyPair keyPair = new KeyPair(publicKey, privateKey); - - return createKeyWrapper(keyPair, getCertificate(keyStore, keyPair, keyAlias, realm.getName()), loadCertificateChain(keyStore, keyAlias), KeyType.EC); - - } - - private X509Certificate getCertificate(KeyStore keyStore, KeyPair keyPair, String keyAlias, String realmName) throws KeyStoreException { - X509Certificate certificate = (X509Certificate) keyStore.getCertificate(keyAlias); - if (certificate == null) { - certificate = CertificateUtils.generateV1SelfSignedCertificate(keyPair, realmName); + private void checkUsage(KeyUse keyUse) throws GeneralSecurityException { + String use = model.get(Attributes.KEY_USE); + if (use != null && !keyUse.name().equalsIgnoreCase(use)) { + throw new UnrecoverableKeyException(String.format("Invalid use %s for algorithm %s.", use, algorithm)); } - return certificate; } - private KeyWrapper loadRSAKey(RealmModel realm, ComponentModel model, KeyStore keyStore, String keyAlias) throws GeneralSecurityException { - PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyAlias, model.get(JavaKeystoreKeyProviderFactory.KEY_PASSWORD_KEY).toCharArray()); - PublicKey publicKey = KeyUtils.extractPublicKey(privateKey); + private X509Certificate checkCertificate(Certificate cert) throws GeneralSecurityException { + if (cert instanceof X509Certificate x509Cert) { + return x509Cert; + } + throw new UnrecoverableKeyException(String.format("Invalid %s certificate in the entry.", (cert != null? cert.getType() : null))); + } + private K checkKeyEntry(KeyStore keyStore, String keyAlias, Class clazz, KeyUse use) throws GeneralSecurityException { + checkUsage(use); + String keyPwd = model.get(JavaKeystoreKeyProviderFactory.KEY_PASSWORD_KEY); + keyPwd = vault.getStringSecret(keyPwd).get().orElse(keyPwd); + KeyStore.Entry keyEntry = keyStore.getEntry(keyAlias, new KeyStore.PasswordProtection(keyPwd.toCharArray())); + if (keyEntry == null) { + throw new UnrecoverableKeyException(String.format("Alias %s does not exists in the keystore.", keyAlias)); + } + if (!clazz.isInstance(keyEntry)) { + throw new UnrecoverableKeyException(String.format("Invalid %s key for alias %s. Key is not %s.", algorithm, keyAlias, clazz.getSimpleName())); + } + return clazz.cast(keyEntry); + } + + private K checkKey(Key key, String keyAlias, Class clazz, String javaAlgorithm) throws GeneralSecurityException { + if (!clazz.isInstance(key) || (javaAlgorithm != null && !javaAlgorithm.equalsIgnoreCase(key.getAlgorithm()))) { + throw new NoSuchAlgorithmException(String.format("Invalid %s key for alias %s. Algorithm is %s.", algorithm, keyAlias, key.getAlgorithm())); + } + return clazz.cast(key); + } + + private KeyWrapper loadOctKey(KeyStore keyStore, String keyAlias, String javaAlgorithm, KeyUse keyUse) throws GeneralSecurityException { + KeyStore.SecretKeyEntry secretKeyEntry = checkKeyEntry(keyStore, keyAlias, KeyStore.SecretKeyEntry.class, keyUse); + SecretKey secretKey = checkKey(secretKeyEntry.getSecretKey(), keyAlias, SecretKey.class, javaAlgorithm); + + return createKeyWrapper(secretKey, keyUse); + } + + private KeyWrapper loadEdDSAKey(KeyStore keyStore, String keyAlias, KeyUse keyUse) throws GeneralSecurityException { + KeyStore.PrivateKeyEntry privateKeyEntry = checkKeyEntry(keyStore, keyAlias, KeyStore.PrivateKeyEntry.class, keyUse); + EdECPrivateKey privateKey = checkKey(privateKeyEntry.getPrivateKey(), keyAlias, EdECPrivateKey.class, null); + X509Certificate x509Cert = checkCertificate(privateKeyEntry.getCertificate()); + try { + JavaAlgorithm.getJavaAlgorithmForHash(Algorithm.EdDSA, privateKey.getParams().getName()); + } catch (RuntimeException e) { + throw new UnrecoverableKeyException(String.format("Invalid EdDSA curve for alias %s. Curve algorithm is %s.", + keyAlias, privateKey.getParams().getName())); + } + + PublicKey publicKey = x509Cert.getPublicKey(); KeyPair keyPair = new KeyPair(publicKey, privateKey); - - return createKeyWrapper(keyPair, getCertificate(keyStore, keyPair, keyAlias, realm.getName()), loadCertificateChain(keyStore, keyAlias), KeyType.RSA); + return createKeyWrapper(keyPair, x509Cert, loadCertificateChain(privateKeyEntry), KeyType.OKP, keyUse, privateKey.getParams().getName()); } - private List loadCertificateChain(KeyStore keyStore, String keyAlias) throws GeneralSecurityException { - List chain = Optional.ofNullable(keyStore.getCertificateChain(keyAlias)) + private KeyWrapper loadECKey(KeyStore keyStore, String keyAlias, KeyUse keyUse) throws GeneralSecurityException { + KeyStore.PrivateKeyEntry privateKeyEntry = checkKeyEntry(keyStore, keyAlias, KeyStore.PrivateKeyEntry.class, keyUse); + ECPrivateKey privateKey = checkKey(privateKeyEntry.getPrivateKey(), keyAlias, ECPrivateKey.class, null); + X509Certificate x509Cert = checkCertificate(privateKeyEntry.getCertificate()); + PublicKey publicKey = x509Cert.getPublicKey(); + KeyPair keyPair = new KeyPair(publicKey, privateKey); + return createKeyWrapper(keyPair, x509Cert, loadCertificateChain(privateKeyEntry), KeyType.EC, keyUse, null); + } + + private KeyWrapper loadRSAKey(KeyStore keyStore, String keyAlias, KeyUse keyUse) throws GeneralSecurityException { + KeyStore.PrivateKeyEntry privateKeyEntry = checkKeyEntry(keyStore, keyAlias, KeyStore.PrivateKeyEntry.class, keyUse); + RSAPrivateCrtKey privateKey = checkKey(privateKeyEntry.getPrivateKey(), keyAlias, RSAPrivateCrtKey.class, null); + X509Certificate x509Cert = checkCertificate(privateKeyEntry.getCertificate()); + PublicKey publicKey = x509Cert.getPublicKey(); + KeyPair keyPair = new KeyPair(publicKey, privateKey); + return createKeyWrapper(keyPair, x509Cert, loadCertificateChain(privateKeyEntry), KeyType.RSA, keyUse, null); + } + + private List loadCertificateChain(KeyStore.PrivateKeyEntry privateKeyEntry) throws GeneralSecurityException { + List chain = Optional.ofNullable(privateKeyEntry.getCertificateChain()) .map(certificates -> Arrays.stream(certificates) .map(X509Certificate.class::cast) .collect(Collectors.toList())) @@ -167,9 +230,8 @@ public class JavaKeystoreKeyProvider implements KeyProvider { return chain; } - private KeyWrapper createKeyWrapper(KeyPair keyPair, X509Certificate certificate, List certificateChain, String type) { - KeyUse keyUse = KeyUse.valueOf(model.get(Attributes.KEY_USE, KeyUse.SIG.getSpecName()).toUpperCase()); - + private KeyWrapper createKeyWrapper(KeyPair keyPair, X509Certificate certificate, List certificateChain, + String type, KeyUse keyUse, String curve) { KeyWrapper key = new KeyWrapper(); key.setProviderId(model.getId()); @@ -179,6 +241,7 @@ public class JavaKeystoreKeyProvider implements KeyProvider { key.setUse(keyUse); key.setType(type); key.setAlgorithm(algorithm); + key.setCurve(curve); key.setStatus(status); key.setPrivateKey(keyPair.getPrivate()); key.setPublicKey(keyPair.getPublic()); @@ -195,6 +258,21 @@ public class JavaKeystoreKeyProvider implements KeyProvider { return key; } + private KeyWrapper createKeyWrapper(SecretKey secretKey, KeyUse use) { + KeyWrapper keyWrapper = new KeyWrapper(); + + keyWrapper.setProviderId(model.getId()); + keyWrapper.setProviderPriority(model.get("priority", 0l)); + + keyWrapper.setKid(model.get(Attributes.KID_KEY, KeycloakModelUtils.generateId())); + keyWrapper.setUse(use); + keyWrapper.setType(KeyType.OCT); + keyWrapper.setAlgorithm(algorithm); + keyWrapper.setStatus(status); + keyWrapper.setSecretKey(secretKey); + return keyWrapper; + } + /** *

Validates the giving certificate chain represented by {@code certificates}. If the list of certificates is empty * or does not have at least 2 certificates (end-user certificate plus intermediary/root CAs) this method does nothing. @@ -224,7 +302,6 @@ public class JavaKeystoreKeyProvider implements KeyProvider { validator.validate(certPath, params); } - @Override public Stream getKeysStream() { return Stream.of(key); diff --git a/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProviderFactory.java index ff2408bcd76..930b16aa217 100644 --- a/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProviderFactory.java +++ b/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProviderFactory.java @@ -92,7 +92,7 @@ public class JavaKeystoreKeyProviderFactory implements KeyProviderFactory { @Override public KeyProvider create(KeycloakSession session, ComponentModel model) { - return new JavaKeystoreKeyProvider(session.getContext().getRealm(), model); + return new JavaKeystoreKeyProvider(session.getContext().getRealm(), model, session.vault()); } @Override @@ -109,7 +109,7 @@ public class JavaKeystoreKeyProviderFactory implements KeyProviderFactory { .checkSingle(KEY_PASSWORD_PROPERTY, true); try { - new JavaKeystoreKeyProvider(realm, model).loadKey(realm, model); + new JavaKeystoreKeyProvider(realm, model, session.vault()).loadKey(realm, model); } catch (Throwable t) { logger.error("Failed to load keys.", t); throw new ComponentValidationException("Failed to load keys. " + t.getMessage(), t); @@ -118,9 +118,12 @@ public class JavaKeystoreKeyProviderFactory implements KeyProviderFactory { // merge the algorithms supported for RSA and EC keys and provide them as one configuration property private static ProviderConfigProperty mergedAlgorithmProperties() { - List ecAlgorithms = List.of(Algorithm.ES256, Algorithm.ES384, Algorithm.ES512); - List algorithms = Stream.of(Attributes.RS_ALGORITHM_PROPERTY.getOptions(), - ecAlgorithms, Attributes.RS_ENC_ALGORITHM_PROPERTY.getOptions()) + List algorithms = Stream.of( + List.of(Algorithm.AES, Algorithm.EdDSA), + List.of(Algorithm.ES256, Algorithm.ES384, Algorithm.ES512), + Attributes.HS_ALGORITHM_PROPERTY.getOptions(), + Attributes.RS_ALGORITHM_PROPERTY.getOptions(), + Attributes.RS_ENC_ALGORITHM_PROPERTY.getOptions()) .flatMap(Collection::stream) .toList(); return new ProviderConfigProperty(Attributes.RS_ALGORITHM_PROPERTY.getName(), Attributes.RS_ALGORITHM_PROPERTY.getLabel(), diff --git a/testsuite/integration-arquillian/servers/auth-server/common/vault/test_keystore__password b/testsuite/integration-arquillian/servers/auth-server/common/vault/test_keystore__password new file mode 100644 index 00000000000..7aa311adf93 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/common/vault/test_keystore__password @@ -0,0 +1 @@ +password \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeyUtils.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeyUtils.java index b88b3461d3a..143c0b490bc 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeyUtils.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeyUtils.java @@ -3,7 +3,9 @@ package org.keycloak.testsuite.util; import jakarta.ws.rs.core.Response; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.common.crypto.CryptoIntegration; +import org.keycloak.common.util.BouncyIntegration; import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.crypto.JavaAlgorithm; import org.keycloak.crypto.KeyStatus; import org.keycloak.crypto.KeyType; import org.keycloak.crypto.KeyUse; @@ -28,6 +30,8 @@ import java.security.spec.X509EncodedKeySpec; import java.util.Base64; import java.util.function.Predicate; import java.util.stream.Stream; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; @@ -50,6 +54,16 @@ public class KeyUtils { } } + public static KeyPair generateEdDSAKey(String curve) throws NoSuchAlgorithmException, NoSuchProviderException { + KeyPairGenerator kpg = CryptoIntegration.getProvider().getKeyPairGen(curve); + return kpg.generateKeyPair(); + } + + public static SecretKey generateSecretKey(String algorithm, int keySize) throws NoSuchAlgorithmException, NoSuchProviderException { + KeyGenerator keyGen = KeyGenerator.getInstance(JavaAlgorithm.getJavaAlgorithm(algorithm), BouncyIntegration.PROVIDER); + keyGen.init(keySize); + return keyGen.generateKey(); + } public static PublicKey publicKeyFromString(String key) { try { diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeystoreUtils.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeystoreUtils.java index 240b780861a..a15d56d6184 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeystoreUtils.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeystoreUtils.java @@ -32,10 +32,12 @@ import java.io.File; import java.io.FileOutputStream; import java.security.KeyPair; import java.security.KeyStore; +import java.security.PrivateKey; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.stream.Stream; +import javax.crypto.SecretKey; import static org.junit.Assert.fail; @@ -64,26 +66,46 @@ public class KeystoreUtils { } public static KeystoreInfo generateKeystore(TemporaryFolder folder, KeystoreUtil.KeystoreFormat keystoreType, String subject, String keystorePassword, String keyPassword, KeyPair keyPair) throws Exception { - String fileName = "keystore." + keystoreType.getPrimaryExtension(); - X509Certificate certificate = CertificateUtils.generateV1SelfSignedCertificate(keyPair, subject); + return generateKeystore(folder, keystoreType, subject, keystorePassword, keyPassword, keyPair.getPrivate(), certificate); + } + + public static KeystoreInfo generateKeystore(TemporaryFolder folder, KeystoreUtil.KeystoreFormat keystoreType, + String subject, String keystorePassword, String keyPassword, PrivateKey privKey, Certificate certificate) throws Exception { + String fileName = "keystore." + keystoreType.getPrimaryExtension(); KeyStore keyStore = CryptoIntegration.getProvider().getKeyStore(keystoreType); keyStore.load(null, null); - Certificate[] chain = {certificate}; - keyStore.setKeyEntry(subject, keyPair.getPrivate(), keyPassword.trim().toCharArray(), chain); + keyStore.setKeyEntry(subject, privKey, keyPassword.trim().toCharArray(), chain); File file = folder.newFile(fileName); keyStore.store(new FileOutputStream(file), keystorePassword.trim().toCharArray()); CertificateRepresentation certRep = new CertificateRepresentation(); - certRep.setPrivateKey(PemUtils.encodeKey(keyPair.getPrivate())); - certRep.setPublicKey(PemUtils.encodeKey(keyPair.getPublic())); + certRep.setPrivateKey(PemUtils.encodeKey(privKey)); + certRep.setPublicKey(PemUtils.encodeKey(certificate.getPublicKey())); certRep.setCertificate(PemUtils.encodeCertificate(certificate)); return new KeystoreInfo(certRep, file); } + public static KeystoreInfo generateKeystore(TemporaryFolder folder, KeystoreUtil.KeystoreFormat keystoreType, String alias, + String keystorePassword, String keyPassword, SecretKey secretKey) throws Exception { + String fileName = "keystore." + keystoreType.getPrimaryExtension(); + + KeyStore keyStore = KeyStore.getInstance(keystoreType.name()); + keyStore.load(null, null); + + KeyStore.SecretKeyEntry secretKeyEntry = new KeyStore.SecretKeyEntry(secretKey); + KeyStore.ProtectionParameter protection = new KeyStore.PasswordProtection(keyPassword.trim().toCharArray()); + keyStore.setEntry(alias, secretKeyEntry, protection); + + File file = folder.newFile(fileName); + keyStore.store(new FileOutputStream(file), keystorePassword.trim().toCharArray()); + + return new KeystoreInfo(null, file); + } + public static KeystoreInfo generateKeystore(TemporaryFolder folder, KeystoreUtil.KeystoreFormat keystoreType, String subject, String keystorePassword, String keyPassword) throws Exception { return generateKeystore(folder, keystoreType, subject, keystorePassword, keyPassword, KeyUtils.generateRsaKeyPair(2048)); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/JavaKeystoreKeyProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/JavaKeystoreKeyProviderTest.java index 4ed7b31d360..0e7a868b84a 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/JavaKeystoreKeyProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/JavaKeystoreKeyProviderTest.java @@ -18,15 +18,20 @@ package org.keycloak.testsuite.keys; import jakarta.ws.rs.core.Response; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; import org.jboss.arquillian.graphene.page.Page; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; +import org.keycloak.common.crypto.FipsMode; import org.keycloak.common.util.KeystoreUtil; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.common.util.PemUtils; import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.KeyType; import org.keycloak.jose.jws.AlgorithmType; +import org.keycloak.keys.Attributes; import org.keycloak.keys.JavaKeystoreKeyProviderFactory; import org.keycloak.keys.KeyProvider; import org.keycloak.representations.idm.ComponentRepresentation; @@ -37,6 +42,8 @@ import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.arquillian.AuthServerTestEnricher; +import org.keycloak.testsuite.arquillian.annotation.EnableVault; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.util.KeyUtils; @@ -46,14 +53,13 @@ import java.security.PublicKey; import java.util.List; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import static org.keycloak.common.util.KeystoreUtil.KeystoreFormat.PKCS12; import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson; /** * @author Stian Thorgersen */ +@EnableVault public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest { @Rule @@ -78,41 +84,58 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest { @Test public void createJksRSA() throws Exception { - createSuccess(KeystoreUtil.KeystoreFormat.JKS, AlgorithmType.RSA); + createSuccess(KeystoreUtil.KeystoreFormat.JKS, AlgorithmType.RSA, false); } @Test public void createPkcs12RSA() throws Exception { - createSuccess(PKCS12, AlgorithmType.RSA); + createSuccess(KeystoreUtil.KeystoreFormat.PKCS12, AlgorithmType.RSA, true); } @Test public void createBcfksRSA() throws Exception { - createSuccess(KeystoreUtil.KeystoreFormat.BCFKS, AlgorithmType.RSA); + createSuccess(KeystoreUtil.KeystoreFormat.BCFKS, AlgorithmType.RSA, false); } @Test public void createJksECDSA() throws Exception { - createSuccess(KeystoreUtil.KeystoreFormat.JKS, AlgorithmType.ECDSA); + createSuccess(KeystoreUtil.KeystoreFormat.JKS, AlgorithmType.ECDSA, true); } @Test public void createPkcs12ECDSA() throws Exception { - createSuccess(KeystoreUtil.KeystoreFormat.PKCS12, AlgorithmType.ECDSA); + createSuccess(KeystoreUtil.KeystoreFormat.PKCS12, AlgorithmType.ECDSA, false); } @Test public void createBcfksECDSA() throws Exception { - createSuccess(KeystoreUtil.KeystoreFormat.BCFKS, AlgorithmType.ECDSA); + createSuccess(KeystoreUtil.KeystoreFormat.BCFKS, AlgorithmType.ECDSA, true); } - private void createSuccess(KeystoreUtil.KeystoreFormat keystoreType, AlgorithmType algorithmType) throws Exception { + @Test + public void createBcfksAES() throws Exception { + createSuccess(KeystoreUtil.KeystoreFormat.BCFKS, AlgorithmType.AES, false); + } + + @Test + public void createHMAC() throws Exception { + // BC provider fails storing HMAC in BCFKS (although BCFIPS works) + createSuccess(isFips()? KeystoreUtil.KeystoreFormat.BCFKS : KeystoreUtil.KeystoreFormat.PKCS12, AlgorithmType.HMAC, true); + } + + @Test + public void createJksEdDSA() throws Exception { + // BCFIPS does not support EdEC keys as it does not implement JDK interfaces + createSuccess(KeystoreUtil.KeystoreFormat.JKS, AlgorithmType.EDDSA, true); + } + + private void createSuccess(KeystoreUtil.KeystoreFormat keystoreType, AlgorithmType algorithmType, boolean vault) throws Exception { KeystoreUtils.assumeKeystoreTypeSupported(keystoreType); generateKeystore(keystoreType, algorithmType); long priority = System.currentTimeMillis(); - ComponentRepresentation rep = createRep("valid", priority, keyAlgorithm); + ComponentRepresentation rep = createRep("valid", priority, keyAlgorithm, vault? "${vault.keystore_password}" : "password"); Response response = adminClient.realm("test").components().add(rep); String id = ApiUtil.getCreatedId(response); @@ -121,8 +144,8 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest { ComponentRepresentation createdRep = adminClient.realm("test").components().component(id).toRepresentation(); assertEquals(6, createdRep.getConfig().size()); assertEquals(Long.toString(priority), createdRep.getConfig().getFirst("priority")); - assertEquals(ComponentRepresentation.SECRET_VALUE, createdRep.getConfig().getFirst("keystorePassword")); - assertEquals(ComponentRepresentation.SECRET_VALUE, createdRep.getConfig().getFirst("keyPassword")); + assertEquals(vault? "${vault.keystore_password}" : ComponentRepresentation.SECRET_VALUE, createdRep.getConfig().getFirst("keystorePassword")); + assertEquals(vault? "${vault.keystore_password}" : ComponentRepresentation.SECRET_VALUE, createdRep.getConfig().getFirst("keyPassword")); KeysMetadataRepresentation keys = adminClient.realm("test").keys().getKeyMetadata(); @@ -130,22 +153,31 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest { assertEquals(id, key.getProviderId()); switch (algorithmType) { - case RSA: { + case RSA -> { assertEquals(algorithmType.name(), key.getType()); - PublicKey exp = PemUtils.decodePublicKey(generatedKeystore.getCertificateInfo().getPublicKey(), "RSA"); - PublicKey got = PemUtils.decodePublicKey(key.getPublicKey(), "RSA"); + PublicKey exp = PemUtils.decodePublicKey(generatedKeystore.getCertificateInfo().getPublicKey(), KeyType.RSA); + PublicKey got = PemUtils.decodePublicKey(key.getPublicKey(), KeyType.RSA); assertEquals(exp, got); - break; + assertEquals(generatedKeystore.getCertificateInfo().getCertificate(), key.getCertificate()); } - case ECDSA: + case ECDSA -> { assertEquals("EC", key.getType()); - PublicKey exp = PemUtils.decodePublicKey(generatedKeystore.getCertificateInfo().getPublicKey(), "EC"); - PublicKey got = PemUtils.decodePublicKey(key.getPublicKey(), "EC"); + PublicKey exp = PemUtils.decodePublicKey(generatedKeystore.getCertificateInfo().getPublicKey(), KeyType.EC); + PublicKey got = PemUtils.decodePublicKey(key.getPublicKey(), KeyType.EC); assertEquals(exp, got); + assertEquals(generatedKeystore.getCertificateInfo().getCertificate(), key.getCertificate()); + } + case AES, HMAC -> { + assertEquals(KeyType.OCT, key.getType()); + assertEquals(keyAlgorithm, key.getAlgorithm()); + } + case EDDSA -> { + assertEquals(KeyType.OKP, key.getType()); + assertEquals(keyAlgorithm, key.getAlgorithm()); + } } assertEquals(priority, key.getProviderPriority()); - assertEquals(generatedKeystore.getCertificateInfo().getCertificate(), key.getCertificate()); } @Test @@ -155,7 +187,7 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest { rep.getConfig().putSingle("keystore", "/nosuchfile"); Response response = adminClient.realm("test").components().add(rep); - assertErrror(response, "Failed to load keys. File not found on server."); + assertError(response, "Failed to load keys. File not found on server."); } @Test @@ -165,7 +197,7 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest { rep.getConfig().putSingle("keystore", "invalid"); Response response = adminClient.realm("test").components().add(rep); - assertErrror(response, "Failed to load keys. File not found on server."); + assertError(response, "Failed to load keys. File not found on server."); } @Test @@ -175,13 +207,13 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest { rep.getConfig().putSingle("keyAlias", "invalid"); Response response = adminClient.realm("test").components().add(rep); - assertErrror(response, "Failed to load keys. Error creating X509v1Certificate."); + assertError(response, "Alias invalid does not exists in the keystore."); } @Test public void invalidKeyPassword() throws Exception { KeystoreUtil.KeystoreFormat keystoreType = KeystoreUtils.getPreferredKeystoreType(); - if (keystoreType == PKCS12) { + if (keystoreType == KeystoreUtil.KeystoreFormat.PKCS12) { // only the keyStore password is significant with PKCS12. Hence we need to test with different keystore type String[] supportedKsTypes = KeystoreUtils.getSupportedKeystoreTypes(); if (supportedKsTypes.length <= 1) { @@ -196,20 +228,43 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest { Response response = adminClient.realm("test").components().add(rep); Assert.assertEquals(400, response.getStatus()); - assertErrror(response, "Failed to load keys. Keystore on server can not be recovered."); + assertError(response, "Failed to load keys. Key in the keystore cannot be recovered."); } - protected void assertErrror(Response response, String error) { + @Test + public void invalidKeyAlgorithmCreatedECButRegisteredRSA() throws Exception { + generateKeystore(KeystoreUtils.getPreferredKeystoreType(), AlgorithmType.ECDSA); + ComponentRepresentation rep = createRep("valid", System.currentTimeMillis(), Algorithm.RS256); + + Response response = adminClient.realm("test").components().add(rep); + assertError(response, "Invalid RS256 key for alias keyalias. Algorithm is EC."); + } + + @Test + public void invalidKeyUsageForRS256() throws Exception { + generateKeystore(KeystoreUtils.getPreferredKeystoreType(), AlgorithmType.RSA); + ComponentRepresentation rep = createRep("valid", System.currentTimeMillis(), Algorithm.RS256); + rep.getConfig().putSingle(Attributes.KEY_USE, "enc"); + + Response response = adminClient.realm("test").components().add(rep); + assertError(response, "Invalid use enc for algorithm RS256."); + } + + protected void assertError(Response response, String error) { if (!response.hasEntity()) { fail("No error message set"); } ErrorRepresentation errorRepresentation = response.readEntity(ErrorRepresentation.class); - assertTrue(errorRepresentation.getErrorMessage().startsWith(error)); + MatcherAssert.assertThat(errorRepresentation.getErrorMessage(), Matchers.containsString(error)); response.close(); } protected ComponentRepresentation createRep(String name, long priority, String algorithm) { + return createRep(name, priority, algorithm, "password"); + } + + protected ComponentRepresentation createRep(String name, long priority, String algorithm, String password) { ComponentRepresentation rep = new ComponentRepresentation(); rep.setName(name); rep.setParentId(adminClient.realm("test").toRepresentation().getId()); @@ -218,9 +273,9 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest { rep.setConfig(new MultivaluedHashMap<>()); rep.getConfig().putSingle("priority", Long.toString(priority)); rep.getConfig().putSingle("keystore", generatedKeystore.getKeystoreFile().getAbsolutePath()); - rep.getConfig().putSingle("keystorePassword", "password"); - rep.getConfig().putSingle("keyAlias", "selfsigned"); - rep.getConfig().putSingle("keyPassword", "password"); + rep.getConfig().putSingle("keystorePassword", password); + rep.getConfig().putSingle("keyAlias", "keyalias"); + rep.getConfig().putSingle("keyPassword", password); rep.getConfig().putSingle("algorithm", algorithm); return rep; } @@ -231,16 +286,35 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest { private void generateKeystore(KeystoreUtil.KeystoreFormat keystoreType, AlgorithmType algorithmType) throws Exception { switch (algorithmType) { - case RSA: { - this.generatedKeystore = KeystoreUtils.generateKeystore(folder, keystoreType, "selfsigned", "password", "password"); + case RSA -> { + this.generatedKeystore = KeystoreUtils.generateKeystore(folder, keystoreType, "keyalias", "password", "password"); this.keyAlgorithm = Algorithm.RS256; - return; } - case ECDSA: - this.generatedKeystore = KeystoreUtils.generateKeystore(folder, keystoreType, "selfsigned", "password", "password", KeyUtils.generateECKey(Algorithm.ES256)); + case ECDSA -> { + this.generatedKeystore = KeystoreUtils.generateKeystore(folder, keystoreType, "keyalias", "password", "password", + KeyUtils.generateECKey(Algorithm.ES256)); this.keyAlgorithm = Algorithm.ES256; + } + case AES -> { + this.generatedKeystore = KeystoreUtils.generateKeystore(folder, keystoreType, "keyalias", "password", "password", + KeyUtils.generateSecretKey(Algorithm.AES, 256)); + this.keyAlgorithm = Algorithm.AES; + } + case HMAC -> { + this.generatedKeystore = KeystoreUtils.generateKeystore(folder, keystoreType, "keyalias", "password", "password", + KeyUtils.generateSecretKey(Algorithm.HS256, 256)); + this.keyAlgorithm = Algorithm.HS256; + } + case EDDSA -> { + this.generatedKeystore = KeystoreUtils.generateKeystore(folder, keystoreType, "keyalias", "password", "password", + KeyUtils.generateEdDSAKey(Algorithm.Ed25519)); + this.keyAlgorithm = Algorithm.EdDSA; + } } } + private static boolean isFips() { + return AuthServerTestEnricher.AUTH_SERVER_FIPS_MODE != FipsMode.DISABLED; + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/vault/test_keystore__password b/testsuite/integration-arquillian/tests/base/src/test/resources/vault/test_keystore__password new file mode 100644 index 00000000000..7aa311adf93 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/vault/test_keystore__password @@ -0,0 +1 @@ +password \ No newline at end of file