From 347f595913d6e309a3d0e986989d4ef617573891 Mon Sep 17 00:00:00 2001 From: rmartinc Date: Fri, 9 Aug 2024 10:47:50 +0200 Subject: [PATCH] Add ECDH-ES encyption algorithms to the java keystore key provider Closes #32023 Signed-off-by: rmartinc --- .../release_notes/topics/26_0_0.adoc | 8 +++ .../keys/JavaKeystoreKeyProvider.java | 2 + .../keys/JavaKeystoreKeyProviderFactory.java | 3 +- .../keys/JavaKeystoreKeyProviderTest.java | 61 +++++++++++-------- 4 files changed, 46 insertions(+), 28 deletions(-) diff --git a/docs/documentation/release_notes/topics/26_0_0.adoc b/docs/documentation/release_notes/topics/26_0_0.adoc index 74fc26603a..6ca3423328 100644 --- a/docs/documentation/release_notes/topics/26_0_0.adoc +++ b/docs/documentation/release_notes/topics/26_0_0.adoc @@ -103,3 +103,11 @@ no longer holds the list of identity providers. However, they are still availabl when exporting a realm. For information on how to migrate, see the link:{upgradingguide_link}[{upgradingguide_name}]. + += Adding support for ECDH-ES encryption key management algorithms + +Now {project_name} allows configuring ECDH-ES, ECDH-ES+A128KW, ECDH-ES+A192KW or ECDH-ES+A256KW as the encryption key management algorithm for clients. The Key Agreement with Elliptic Curve Diffie-Hellman Ephemeral Static (ECDH-ES) specification introduces three new header parameters for the JWT: `epk`, `apu` and `apv`. Currently {project_name} implementation only manages the compulsory `epk` while the other two (which are optional) are never added to the header. For more information about those algorithms please refer to the link:https://datatracker.ietf.org/doc/html/rfc7518#section-4.6[JSON Web Algorithms (JWA)]. + +ifeval::[{project_community}==true] +Many thanks to https://github.com/justin-tay[Justin Tay] for the contribution. +endif::[] diff --git a/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProvider.java b/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProvider.java index c2eb0ef1ab..ae687c34f0 100644 --- a/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProvider.java +++ b/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProvider.java @@ -107,6 +107,8 @@ public class JavaKeystoreKeyProvider implements KeyProvider { loadRSAKey(keyStore, keyAlias, KeyUse.ENC); case Algorithm.ES256, Algorithm.ES384, Algorithm.ES512 -> loadECKey(keyStore, keyAlias, KeyUse.SIG); + case Algorithm.ECDH_ES, Algorithm.ECDH_ES_A128KW, Algorithm.ECDH_ES_A192KW, Algorithm.ECDH_ES_A256KW -> + loadECKey(keyStore, keyAlias, KeyUse.ENC); case Algorithm.EdDSA -> loadEdDSAKey(keyStore, keyAlias, KeyUse.SIG); case Algorithm.AES -> diff --git a/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProviderFactory.java index 930b16aa21..1accaf57ab 100644 --- a/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProviderFactory.java +++ b/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProviderFactory.java @@ -123,7 +123,8 @@ public class JavaKeystoreKeyProviderFactory implements KeyProviderFactory { List.of(Algorithm.ES256, Algorithm.ES384, Algorithm.ES512), Attributes.HS_ALGORITHM_PROPERTY.getOptions(), Attributes.RS_ALGORITHM_PROPERTY.getOptions(), - Attributes.RS_ENC_ALGORITHM_PROPERTY.getOptions()) + Attributes.RS_ENC_ALGORITHM_PROPERTY.getOptions(), + GeneratedEcdhKeyProviderFactory.ECDH_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/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 0e7a868b84..c662ebf08e 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 @@ -84,54 +84,69 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest { @Test public void createJksRSA() throws Exception { - createSuccess(KeystoreUtil.KeystoreFormat.JKS, AlgorithmType.RSA, false); + createSuccess(KeystoreUtil.KeystoreFormat.JKS, AlgorithmType.RSA, Algorithm.RS256, false); } @Test public void createPkcs12RSA() throws Exception { - createSuccess(KeystoreUtil.KeystoreFormat.PKCS12, AlgorithmType.RSA, true); + createSuccess(KeystoreUtil.KeystoreFormat.PKCS12, AlgorithmType.RSA, Algorithm.RS256, true); } @Test public void createBcfksRSA() throws Exception { - createSuccess(KeystoreUtil.KeystoreFormat.BCFKS, AlgorithmType.RSA, false); + createSuccess(KeystoreUtil.KeystoreFormat.BCFKS, AlgorithmType.RSA, Algorithm.RS256, false); } @Test public void createJksECDSA() throws Exception { - createSuccess(KeystoreUtil.KeystoreFormat.JKS, AlgorithmType.ECDSA, true); + createSuccess(KeystoreUtil.KeystoreFormat.JKS, AlgorithmType.ECDSA, Algorithm.ES256, true); } @Test public void createPkcs12ECDSA() throws Exception { - createSuccess(KeystoreUtil.KeystoreFormat.PKCS12, AlgorithmType.ECDSA, false); + createSuccess(KeystoreUtil.KeystoreFormat.PKCS12, AlgorithmType.ECDSA, Algorithm.ES256, false); } @Test public void createBcfksECDSA() throws Exception { - createSuccess(KeystoreUtil.KeystoreFormat.BCFKS, AlgorithmType.ECDSA, true); + createSuccess(KeystoreUtil.KeystoreFormat.BCFKS, AlgorithmType.ECDSA, Algorithm.ES256, true); + } + + @Test + public void createJksECDSAECDHES() throws Exception { + createSuccess(KeystoreUtil.KeystoreFormat.JKS, AlgorithmType.ECDSA, Algorithm.ECDH_ES, true); + } + + @Test + public void createPkcs12ECDSAECDHESA192KW() throws Exception { + createSuccess(KeystoreUtil.KeystoreFormat.PKCS12, AlgorithmType.ECDSA, Algorithm.ECDH_ES_A192KW, false); + } + + @Test + public void createBcfksECDSAECDHESA256KW() throws Exception { + createSuccess(KeystoreUtil.KeystoreFormat.BCFKS, AlgorithmType.ECDSA, Algorithm.ECDH_ES_A256KW, true); } @Test public void createBcfksAES() throws Exception { - createSuccess(KeystoreUtil.KeystoreFormat.BCFKS, AlgorithmType.AES, false); + createSuccess(KeystoreUtil.KeystoreFormat.BCFKS, AlgorithmType.AES, Algorithm.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); + createSuccess(isFips()? KeystoreUtil.KeystoreFormat.BCFKS : KeystoreUtil.KeystoreFormat.PKCS12, AlgorithmType.HMAC, Algorithm.HS256, 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); + createSuccess(KeystoreUtil.KeystoreFormat.JKS, AlgorithmType.EDDSA, Algorithm.EdDSA, true); } - private void createSuccess(KeystoreUtil.KeystoreFormat keystoreType, AlgorithmType algorithmType, boolean vault) throws Exception { + private void createSuccess(KeystoreUtil.KeystoreFormat keystoreType, AlgorithmType algorithmType, String keyAlgorithm, boolean vault) throws Exception { KeystoreUtils.assumeKeystoreTypeSupported(keystoreType); - generateKeystore(keystoreType, algorithmType); + generateKeystore(keystoreType, algorithmType, keyAlgorithm); long priority = System.currentTimeMillis(); @@ -182,7 +197,7 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest { @Test public void invalidKeystore() throws Exception { - generateKeystore(KeystoreUtils.getPreferredKeystoreType()); + generateKeystore(KeystoreUtils.getPreferredKeystoreType(), AlgorithmType.RSA, Algorithm.RS256); ComponentRepresentation rep = createRep("valid", System.currentTimeMillis(), keyAlgorithm); rep.getConfig().putSingle("keystore", "/nosuchfile"); @@ -192,7 +207,7 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest { @Test public void invalidKeystorePassword() throws Exception { - generateKeystore(KeystoreUtils.getPreferredKeystoreType()); + generateKeystore(KeystoreUtils.getPreferredKeystoreType(), AlgorithmType.RSA, Algorithm.RS256); ComponentRepresentation rep = createRep("valid", System.currentTimeMillis(), keyAlgorithm); rep.getConfig().putSingle("keystore", "invalid"); @@ -202,7 +217,7 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest { @Test public void invalidKeyAlias() throws Exception { - generateKeystore(KeystoreUtils.getPreferredKeystoreType()); + generateKeystore(KeystoreUtils.getPreferredKeystoreType(), AlgorithmType.RSA, Algorithm.RS256); ComponentRepresentation rep = createRep("valid", System.currentTimeMillis(), keyAlgorithm); rep.getConfig().putSingle("keyAlias", "invalid"); @@ -222,7 +237,7 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest { keystoreType = Enum.valueOf(KeystoreUtil.KeystoreFormat.class, supportedKsTypes[1]); log.infof("Fallback to keystore type '%s' for the invalidKeyPassword() test", keystoreType); } - generateKeystore(keystoreType); + generateKeystore(keystoreType, AlgorithmType.RSA, Algorithm.RS256); ComponentRepresentation rep = createRep("valid", System.currentTimeMillis(), keyAlgorithm); rep.getConfig().putSingle("keyPassword", "invalid"); @@ -233,7 +248,7 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest { @Test public void invalidKeyAlgorithmCreatedECButRegisteredRSA() throws Exception { - generateKeystore(KeystoreUtils.getPreferredKeystoreType(), AlgorithmType.ECDSA); + generateKeystore(KeystoreUtils.getPreferredKeystoreType(), AlgorithmType.ECDSA, Algorithm.RS256); ComponentRepresentation rep = createRep("valid", System.currentTimeMillis(), Algorithm.RS256); Response response = adminClient.realm("test").components().add(rep); @@ -242,7 +257,7 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest { @Test public void invalidKeyUsageForRS256() throws Exception { - generateKeystore(KeystoreUtils.getPreferredKeystoreType(), AlgorithmType.RSA); + generateKeystore(KeystoreUtils.getPreferredKeystoreType(), AlgorithmType.RSA, Algorithm.RS256); ComponentRepresentation rep = createRep("valid", System.currentTimeMillis(), Algorithm.RS256); rep.getConfig().putSingle(Attributes.KEY_USE, "enc"); @@ -280,35 +295,27 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest { return rep; } - private void generateKeystore(KeystoreUtil.KeystoreFormat keystoreType) throws Exception { - generateKeystore(keystoreType, AlgorithmType.RSA); - } - - private void generateKeystore(KeystoreUtil.KeystoreFormat keystoreType, AlgorithmType algorithmType) throws Exception { + private void generateKeystore(KeystoreUtil.KeystoreFormat keystoreType, AlgorithmType algorithmType, String keyAlgorithm) throws Exception { + this.keyAlgorithm = keyAlgorithm; switch (algorithmType) { case RSA -> { this.generatedKeystore = KeystoreUtils.generateKeystore(folder, keystoreType, "keyalias", "password", "password"); - this.keyAlgorithm = Algorithm.RS256; } 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; } } }