From 966a4545487c9f752d0c4b14cb106c968f5a4abd Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Thu, 8 Aug 2024 23:29:35 +0800 Subject: [PATCH] Add ECDH-ES JWE Algorithm Provider, Add generated ECDH key provider (#23928) Closes #23596 Closes #23597 Signed-off-by: Justin Tay <49700559+justin-tay@users.noreply.github.com> --- .../common/crypto/CryptoConstants.java | 4 + .../java/org/keycloak/crypto/Algorithm.java | 5 + .../main/java/org/keycloak/jose/jwe/JWE.java | 7 +- .../org/keycloak/jose/jwe/JWEConstants.java | 4 + .../java/org/keycloak/jose/jwe/JWEHeader.java | 114 +++++- .../org/keycloak/jose/jwe/JWERegistry.java | 2 + .../jose/jwe/alg/DirectAlgorithmProvider.java | 6 +- .../jose/jwe/alg/JWEAlgorithmProvider.java | 6 +- .../def/AesKeyWrapAlgorithmProvider.java | 6 +- .../crypto/def/BCEcdhEsAlgorithmProvider.java | 278 +++++++++++++++ .../crypto/def/DefaultCryptoProvider.java | 4 + ...tRsaKeyEncryptionJWEAlgorithmProvider.java | 6 +- .../test/BCEcdhEsAlgorithmProviderTest.java | 142 ++++++++ .../elytron/AesKeyWrapAlgorithmProvider.java | 6 +- .../ElytronEcdhEsAlgorithmProvider.java | 252 +++++++++++++ ...nRsaKeyEncryptionJWEAlgorithmProvider.java | 6 +- .../elytron/WildFlyElytronProvider.java | 4 + .../ElytronEcdhEsAlgorithmProviderTest.java | 104 ++++++ .../fips/BCFIPSEcdhEsAlgorithmProvider.java | 262 ++++++++++++++ .../crypto/fips/FIPS1402Provider.java | 4 + .../fips/FIPSAesKeyWrapAlgorithmProvider.java | 6 +- ...SRsaKeyEncryptionJWEAlgorithmProvider.java | 6 +- .../test/BCFIPSECDSACryptoProviderTest.java | 11 +- .../BCFIPSEcdhEsAlgorithmProviderTest.java | 117 ++++++ ...hEsA128KwCekManagementProviderFactory.java | 37 ++ ...hEsA192KwCekManagementProviderFactory.java | 37 ++ ...hEsA256KwCekManagementProviderFactory.java | 37 ++ .../crypto/EcdhEsCekManagementProvider.java | 46 +++ .../EcdhEsCekManagementProviderFactory.java | 37 ++ ...ovider.java => AbstractEcKeyProvider.java} | 10 +- ...java => AbstractEcKeyProviderFactory.java} | 50 +-- ...AbstractGeneratedEcKeyProviderFactory.java | 123 +++++++ .../keys/GeneratedEcdhKeyProvider.java | 64 ++++ .../keys/GeneratedEcdhKeyProviderFactory.java | 155 ++++++++ .../keys/GeneratedEcdsaKeyProvider.java | 11 +- .../GeneratedEcdsaKeyProviderFactory.java | 146 ++++---- ...ycloak.crypto.CekManagementProviderFactory | 4 + .../org.keycloak.keys.KeyProviderFactory | 1 + .../org/keycloak/testsuite/util/KeyUtils.java | 13 +- .../AbstractKcOidcBrokerJWEEcdhEsTest.java | 62 ++++ ...erJWEEcdhEsA128KwP256A128CbcHs256Test.java | 26 ++ ...cBrokerJWEEcdhEsA128KwP256A128GcmTest.java | 26 ++ ...erJWEEcdhEsA192KwP384A192CbcHs384Test.java | 26 ++ ...cBrokerJWEEcdhEsA192KwP384A192GcmTest.java | 26 ++ ...erJWEEcdhEsA256KwP521A256CbcHs512Test.java | 26 ++ ...cBrokerJWEEcdhEsA256KwP521A256GcmTest.java | 26 ++ ...dcBrokerJWEEcdhEsP384A192CbcHs384Test.java | 26 ++ .../KcOidcBrokerJWEEcdhEsP384A192GcmTest.java | 26 ++ .../testsuite/broker/KcOidcBrokerJWETest.java | 61 +++- .../keys/GeneratedEcdhKeyProviderTest.java | 337 ++++++++++++++++++ .../keys/GeneratedEcdsaKeyProviderTest.java | 12 +- .../oidc/AbstractWellKnownProviderTest.java | 9 +- .../tests/base/testsuites/fips-suite | 8 + 53 files changed, 2650 insertions(+), 180 deletions(-) create mode 100644 crypto/default/src/main/java/org/keycloak/crypto/def/BCEcdhEsAlgorithmProvider.java create mode 100644 crypto/default/src/test/java/org/keycloak/crypto/def/test/BCEcdhEsAlgorithmProviderTest.java create mode 100644 crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronEcdhEsAlgorithmProvider.java create mode 100644 crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/ElytronEcdhEsAlgorithmProviderTest.java create mode 100644 crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSEcdhEsAlgorithmProvider.java create mode 100644 crypto/fips1402/src/test/java/org/keycloak/crypto/fips/test/BCFIPSEcdhEsAlgorithmProviderTest.java create mode 100644 services/src/main/java/org/keycloak/crypto/EcdhEsA128KwCekManagementProviderFactory.java create mode 100644 services/src/main/java/org/keycloak/crypto/EcdhEsA192KwCekManagementProviderFactory.java create mode 100644 services/src/main/java/org/keycloak/crypto/EcdhEsA256KwCekManagementProviderFactory.java create mode 100644 services/src/main/java/org/keycloak/crypto/EcdhEsCekManagementProvider.java create mode 100644 services/src/main/java/org/keycloak/crypto/EcdhEsCekManagementProviderFactory.java rename services/src/main/java/org/keycloak/keys/{AbstractEcdsaKeyProvider.java => AbstractEcKeyProvider.java} (87%) rename services/src/main/java/org/keycloak/keys/{AbstractEcdsaKeyProviderFactory.java => AbstractEcKeyProviderFactory.java} (60%) create mode 100644 services/src/main/java/org/keycloak/keys/AbstractGeneratedEcKeyProviderFactory.java create mode 100644 services/src/main/java/org/keycloak/keys/GeneratedEcdhKeyProvider.java create mode 100644 services/src/main/java/org/keycloak/keys/GeneratedEcdhKeyProviderFactory.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractKcOidcBrokerJWEEcdhEsTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsA128KwP256A128CbcHs256Test.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsA128KwP256A128GcmTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsA192KwP384A192CbcHs384Test.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsA192KwP384A192GcmTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsA256KwP521A256CbcHs512Test.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsA256KwP521A256GcmTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsP384A192CbcHs384Test.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsP384A192GcmTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/GeneratedEcdhKeyProviderTest.java diff --git a/common/src/main/java/org/keycloak/common/crypto/CryptoConstants.java b/common/src/main/java/org/keycloak/common/crypto/CryptoConstants.java index 90a06e2e661..1c53e6186cd 100644 --- a/common/src/main/java/org/keycloak/common/crypto/CryptoConstants.java +++ b/common/src/main/java/org/keycloak/common/crypto/CryptoConstants.java @@ -10,6 +10,10 @@ public class CryptoConstants { public static final String RSA1_5 = "RSA1_5"; public static final String RSA_OAEP = "RSA-OAEP"; public static final String RSA_OAEP_256 = "RSA-OAEP-256"; + public static final String ECDH_ES = "ECDH-ES"; + public static final String ECDH_ES_A128KW = "ECDH-ES+A128KW"; + public static final String ECDH_ES_A192KW = "ECDH-ES+A192KW"; + public static final String ECDH_ES_A256KW = "ECDH-ES+A256KW"; // Constant for the OCSP provider // public static final String OCSP = "OCSP"; diff --git a/core/src/main/java/org/keycloak/crypto/Algorithm.java b/core/src/main/java/org/keycloak/crypto/Algorithm.java index 7aa20a2c5cc..ab6efb6e2a0 100755 --- a/core/src/main/java/org/keycloak/crypto/Algorithm.java +++ b/core/src/main/java/org/keycloak/crypto/Algorithm.java @@ -49,4 +49,9 @@ public interface Algorithm { /* AES */ String AES = "AES"; + + String ECDH_ES = CryptoConstants.ECDH_ES; + String ECDH_ES_A128KW = CryptoConstants.ECDH_ES_A128KW; + String ECDH_ES_A192KW = CryptoConstants.ECDH_ES_A192KW; + String ECDH_ES_A256KW = CryptoConstants.ECDH_ES_A256KW; } diff --git a/core/src/main/java/org/keycloak/jose/jwe/JWE.java b/core/src/main/java/org/keycloak/jose/jwe/JWE.java index e3f7eba873a..99d3d109fe3 100644 --- a/core/src/main/java/org/keycloak/jose/jwe/JWE.java +++ b/core/src/main/java/org/keycloak/jose/jwe/JWE.java @@ -20,6 +20,7 @@ package org.keycloak.jose.jwe; import org.keycloak.common.util.Base64Url; import org.keycloak.jose.JOSE; import org.keycloak.jose.JOSEHeader; +import org.keycloak.jose.jwe.JWEHeader.JWEHeaderBuilder; import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider; import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; import org.keycloak.util.JsonSerialization; @@ -143,8 +144,10 @@ public class JWE implements JOSE { keyStorage.setEncryptionProvider(encryptionProvider); keyStorage.getCEKKey(JWEKeyStorage.KeyUse.ENCRYPTION, true); // Will generate CEK if it's not already present - byte[] encodedCEK = algorithmProvider.encodeCek(encryptionProvider, keyStorage, keyStorage.getEncryptionKey()); + JWEHeaderBuilder headerBuilder = header.toBuilder(); + byte[] encodedCEK = algorithmProvider.encodeCek(encryptionProvider, keyStorage, keyStorage.getEncryptionKey(), headerBuilder); base64Cek = Base64Url.encode(encodedCEK); + header = headerBuilder.build(); encryptionProvider.encodeJwe(this); @@ -191,7 +194,7 @@ public class JWE implements JOSE { keyStorage.setEncryptionProvider(encryptionProvider); - byte[] decodedCek = algorithmProvider.decodeCek(Base64Url.decode(base64Cek), keyStorage.getDecryptionKey()); + byte[] decodedCek = algorithmProvider.decodeCek(Base64Url.decode(base64Cek), keyStorage.getDecryptionKey(), this.header, encryptionProvider); keyStorage.setCEKBytes(decodedCek); encryptionProvider.verifyAndDecodeJwe(this); diff --git a/core/src/main/java/org/keycloak/jose/jwe/JWEConstants.java b/core/src/main/java/org/keycloak/jose/jwe/JWEConstants.java index ad33e73f7bf..89406e44290 100644 --- a/core/src/main/java/org/keycloak/jose/jwe/JWEConstants.java +++ b/core/src/main/java/org/keycloak/jose/jwe/JWEConstants.java @@ -29,6 +29,10 @@ public class JWEConstants { public static final String RSA1_5 = CryptoConstants.RSA1_5; public static final String RSA_OAEP = CryptoConstants.RSA_OAEP; public static final String RSA_OAEP_256 = CryptoConstants.RSA_OAEP_256; + public static final String ECDH_ES = CryptoConstants.ECDH_ES; + public static final String ECDH_ES_A128KW = CryptoConstants.ECDH_ES_A128KW; + public static final String ECDH_ES_A192KW = CryptoConstants.ECDH_ES_A192KW; + public static final String ECDH_ES_A256KW = CryptoConstants.ECDH_ES_A256KW; public static final String A128CBC_HS256 = "A128CBC-HS256"; public static final String A192CBC_HS384 = "A192CBC-HS384"; diff --git a/core/src/main/java/org/keycloak/jose/jwe/JWEHeader.java b/core/src/main/java/org/keycloak/jose/jwe/JWEHeader.java index 55cb782080f..c3b84011b86 100644 --- a/core/src/main/java/org/keycloak/jose/jwe/JWEHeader.java +++ b/core/src/main/java/org/keycloak/jose/jwe/JWEHeader.java @@ -18,6 +18,7 @@ package org.keycloak.jose.jwe; import java.io.IOException; +import java.io.UncheckedIOException; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @@ -25,6 +26,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; import org.keycloak.jose.JOSEHeader; +import org.keycloak.jose.jwk.ECPublicJWK; /** * @author Marek Posolda @@ -41,7 +43,6 @@ public class JWEHeader implements JOSEHeader { @JsonProperty("zip") private String compressionAlgorithm; - @JsonProperty("typ") private String type; @@ -51,6 +52,15 @@ public class JWEHeader implements JOSEHeader { @JsonProperty("kid") private String keyId; + @JsonProperty("epk") + private ECPublicJWK ephemeralPublicKey; + + @JsonProperty("apu") + private String agreementPartyUInfo; + + @JsonProperty("apv") + private String agreementPartyVInfo; + public JWEHeader() { } @@ -75,6 +85,19 @@ public class JWEHeader implements JOSEHeader { this.contentType = contentType; } + public JWEHeader(String algorithm, String encryptionAlgorithm, String compressionAlgorithm, String keyId, String contentType, + String type, ECPublicJWK ephemeralPublicKey, String agreementPartyUInfo, String agreementPartyVInfo) { + this.algorithm = algorithm; + this.encryptionAlgorithm = encryptionAlgorithm; + this.compressionAlgorithm = compressionAlgorithm; + this.keyId = keyId; + this.type = type; + this.contentType = contentType; + this.ephemeralPublicKey = ephemeralPublicKey; + this.agreementPartyUInfo = agreementPartyUInfo; + this.agreementPartyVInfo = agreementPartyVInfo; + } + public String getAlgorithm() { return algorithm; } @@ -105,21 +128,102 @@ public class JWEHeader implements JOSEHeader { return keyId; } + public ECPublicJWK getEphemeralPublicKey() { + return ephemeralPublicKey; + } + + public String getAgreementPartyUInfo() { + return agreementPartyUInfo; + } + + public String getAgreementPartyVInfo() { + return agreementPartyVInfo; + } + private static final ObjectMapper mapper = new ObjectMapper(); static { mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - } public String toString() { try { return mapper.writeValueAsString(this); } catch (IOException e) { - throw new RuntimeException(e); + throw new UncheckedIOException(e); } - - } + public JWEHeaderBuilder toBuilder() { + return builder().algorithm(algorithm).encryptionAlgorithm(encryptionAlgorithm) + .compressionAlgorithm(compressionAlgorithm).type(type).contentType(contentType) + .keyId(keyId).ephemeralPublicKey(ephemeralPublicKey).agreementPartyUInfo(agreementPartyUInfo) + .agreementPartyVInfo(agreementPartyVInfo); + } + + public static JWEHeaderBuilder builder() { + return new JWEHeaderBuilder(); + } + + public static class JWEHeaderBuilder { + private String algorithm = null; + private String encryptionAlgorithm = null; + private String compressionAlgorithm = null; + private String type = null; + private String contentType = null; + private String keyId = null; + private ECPublicJWK ephemeralPublicKey = null; + private String agreementPartyUInfo = null; + private String agreementPartyVInfo = null; + + public JWEHeaderBuilder algorithm(String algorithm) { + this.algorithm = algorithm; + return this; + } + + public JWEHeaderBuilder encryptionAlgorithm(String encryptionAlgorithm) { + this.encryptionAlgorithm = encryptionAlgorithm; + return this; + } + + public JWEHeaderBuilder compressionAlgorithm(String compressionAlgorithm) { + this.compressionAlgorithm = compressionAlgorithm; + return this; + } + + public JWEHeaderBuilder type(String type) { + this.type = type; + return this; + } + + public JWEHeaderBuilder contentType(String contentType) { + this.contentType = contentType; + return this; + } + + public JWEHeaderBuilder keyId(String keyId) { + this.keyId = keyId; + return this; + } + + public JWEHeaderBuilder ephemeralPublicKey(ECPublicJWK ephemeralPublicKey) { + this.ephemeralPublicKey = ephemeralPublicKey; + return this; + } + + public JWEHeaderBuilder agreementPartyUInfo(String agreementPartyUInfo) { + this.agreementPartyUInfo = agreementPartyUInfo; + return this; + } + + public JWEHeaderBuilder agreementPartyVInfo(String agreementPartyVInfo) { + this.agreementPartyVInfo = agreementPartyVInfo; + return this; + } + + public JWEHeader build() { + return new JWEHeader(algorithm, encryptionAlgorithm, compressionAlgorithm, keyId, contentType, + type, ephemeralPublicKey, agreementPartyUInfo, agreementPartyVInfo); + } + } } diff --git a/core/src/main/java/org/keycloak/jose/jwe/JWERegistry.java b/core/src/main/java/org/keycloak/jose/jwe/JWERegistry.java index dc9aa1f80ea..2785f33910f 100644 --- a/core/src/main/java/org/keycloak/jose/jwe/JWERegistry.java +++ b/core/src/main/java/org/keycloak/jose/jwe/JWERegistry.java @@ -38,6 +38,8 @@ class JWERegistry { private static final Map ENC_PROVIDERS = new HashMap<>(); static { + ENC_PROVIDERS.put(JWEConstants.A128GCM, new AesGcmJWEEncryptionProvider(JWEConstants.A128GCM)); + ENC_PROVIDERS.put(JWEConstants.A192GCM, new AesGcmJWEEncryptionProvider(JWEConstants.A192GCM)); ENC_PROVIDERS.put(JWEConstants.A256GCM, new AesGcmJWEEncryptionProvider(JWEConstants.A256GCM)); ENC_PROVIDERS.put(JWEConstants.A128CBC_HS256, new AesCbcHmacShaEncryptionProvider.Aes128CbcHmacSha256Provider()); ENC_PROVIDERS.put(JWEConstants.A192CBC_HS384, new AesCbcHmacShaEncryptionProvider.Aes192CbcHmacSha384Provider()); diff --git a/core/src/main/java/org/keycloak/jose/jwe/alg/DirectAlgorithmProvider.java b/core/src/main/java/org/keycloak/jose/jwe/alg/DirectAlgorithmProvider.java index ef3ca0242ab..ed949bbb02a 100644 --- a/core/src/main/java/org/keycloak/jose/jwe/alg/DirectAlgorithmProvider.java +++ b/core/src/main/java/org/keycloak/jose/jwe/alg/DirectAlgorithmProvider.java @@ -19,6 +19,8 @@ package org.keycloak.jose.jwe.alg; import java.security.Key; +import org.keycloak.jose.jwe.JWEHeader; +import org.keycloak.jose.jwe.JWEHeader.JWEHeaderBuilder; import org.keycloak.jose.jwe.JWEKeyStorage; import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; @@ -28,12 +30,12 @@ import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; public class DirectAlgorithmProvider implements JWEAlgorithmProvider { @Override - public byte[] decodeCek(byte[] encodedCek, Key encryptionKey) { + public byte[] decodeCek(byte[] encodedCek, Key encryptionKey, JWEHeader header, JWEEncryptionProvider encryptionProvider) { return new byte[0]; } @Override - public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey) { + public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey, JWEHeaderBuilder headerBuilder) { return new byte[0]; } } diff --git a/core/src/main/java/org/keycloak/jose/jwe/alg/JWEAlgorithmProvider.java b/core/src/main/java/org/keycloak/jose/jwe/alg/JWEAlgorithmProvider.java index 80347f8e686..4ad5653764b 100644 --- a/core/src/main/java/org/keycloak/jose/jwe/alg/JWEAlgorithmProvider.java +++ b/core/src/main/java/org/keycloak/jose/jwe/alg/JWEAlgorithmProvider.java @@ -19,7 +19,9 @@ package org.keycloak.jose.jwe.alg; import java.security.Key; +import org.keycloak.jose.jwe.JWEHeader; import org.keycloak.jose.jwe.JWEKeyStorage; +import org.keycloak.jose.jwe.JWEHeader.JWEHeaderBuilder; import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; /** @@ -27,8 +29,8 @@ import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; */ public interface JWEAlgorithmProvider { - byte[] decodeCek(byte[] encodedCek, Key encryptionKey) throws Exception; + byte[] decodeCek(byte[] encodedCek, Key encryptionKey, JWEHeader header, JWEEncryptionProvider encryptionProvider) throws Exception; - byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey) throws Exception; + byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey, JWEHeaderBuilder headerBuilder) throws Exception; } diff --git a/crypto/default/src/main/java/org/keycloak/crypto/def/AesKeyWrapAlgorithmProvider.java b/crypto/default/src/main/java/org/keycloak/crypto/def/AesKeyWrapAlgorithmProvider.java index 36fb50b8caa..f623de86276 100644 --- a/crypto/default/src/main/java/org/keycloak/crypto/def/AesKeyWrapAlgorithmProvider.java +++ b/crypto/default/src/main/java/org/keycloak/crypto/def/AesKeyWrapAlgorithmProvider.java @@ -22,6 +22,8 @@ import java.security.Key; import org.bouncycastle.crypto.Wrapper; import org.bouncycastle.crypto.engines.AESWrapEngine; import org.bouncycastle.crypto.params.KeyParameter; +import org.keycloak.jose.jwe.JWEHeader; +import org.keycloak.jose.jwe.JWEHeader.JWEHeaderBuilder; import org.keycloak.jose.jwe.JWEKeyStorage; import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider; import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; @@ -32,14 +34,14 @@ import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; public class AesKeyWrapAlgorithmProvider implements JWEAlgorithmProvider { @Override - public byte[] decodeCek(byte[] encodedCek, Key encryptionKey) throws Exception { + public byte[] decodeCek(byte[] encodedCek, Key encryptionKey, JWEHeader header, JWEEncryptionProvider encryptionProvider) throws Exception { Wrapper encrypter = new AESWrapEngine(); encrypter.init(false, new KeyParameter(encryptionKey.getEncoded())); return encrypter.unwrap(encodedCek, 0, encodedCek.length); } @Override - public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey) throws Exception { + public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey, JWEHeaderBuilder headerBuilder) throws Exception { Wrapper encrypter = new AESWrapEngine(); encrypter.init(true, new KeyParameter(encryptionKey.getEncoded())); byte[] cekBytes = keyStorage.getCekBytes(); diff --git a/crypto/default/src/main/java/org/keycloak/crypto/def/BCEcdhEsAlgorithmProvider.java b/crypto/default/src/main/java/org/keycloak/crypto/def/BCEcdhEsAlgorithmProvider.java new file mode 100644 index 00000000000..d9a1db967c7 --- /dev/null +++ b/crypto/default/src/main/java/org/keycloak/crypto/def/BCEcdhEsAlgorithmProvider.java @@ -0,0 +1,278 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.crypto.def; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; + +import javax.crypto.KeyAgreement; + +import org.bouncycastle.crypto.Wrapper; +import org.bouncycastle.crypto.agreement.kdf.ConcatenationKDFGenerator; +import org.bouncycastle.crypto.engines.AESWrapEngine; +import org.bouncycastle.crypto.params.KDFParameters; +import org.bouncycastle.crypto.params.KeyParameter; +import org.bouncycastle.crypto.util.DigestFactory; +import org.bouncycastle.jce.ECNamedCurveTable; +import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec; +import org.bouncycastle.jce.spec.ECNamedCurveSpec; +import org.keycloak.common.util.Base64Url; +import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.KeyType; +import org.keycloak.jose.jwe.JWEHeader; +import org.keycloak.jose.jwe.JWEHeader.JWEHeaderBuilder; +import org.keycloak.jose.jwe.JWEKeyStorage; +import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider; +import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; +import org.keycloak.jose.jwk.ECPublicJWK; +import org.keycloak.jose.jwk.JWKUtil; + +/** + * ECDH Ephemeral Static Algorithm Provider. + * + * @author Justin Tay + * @see Key + * Derivation for ECDH Key Agreement + */ +public class BCEcdhEsAlgorithmProvider implements JWEAlgorithmProvider { + + @Override + public byte[] decodeCek(byte[] encodedCek, Key encryptionKey, JWEHeader header, + JWEEncryptionProvider encryptionProvider) throws Exception { + int keyDataLength = getKeyDataLength(header.getAlgorithm(), encryptionProvider); + PublicKey sharedPublicKey = toPublicKey(header.getEphemeralPublicKey()); + + String algorithmID = getAlgorithmID(header.getAlgorithm(), header.getEncryptionAlgorithm()); + byte[] derivedKey = deriveKey(sharedPublicKey, encryptionKey, keyDataLength, algorithmID, + base64UrlDecode(header.getAgreementPartyUInfo()), base64UrlDecode(header.getAgreementPartyVInfo())); + + if (Algorithm.ECDH_ES.equals(header.getAlgorithm())) { + return derivedKey; + } else { + Wrapper encrypter = new AESWrapEngine(); + encrypter.init(false, new KeyParameter(derivedKey)); + return encrypter.unwrap(encodedCek, 0, encodedCek.length); + } + } + + @Override + public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey, + JWEHeaderBuilder headerBuilder) throws Exception { + JWEHeader header = headerBuilder.build(); + int keyDataLength = getKeyDataLength(header.getAlgorithm(), encryptionProvider); + ECParameterSpec params = ((ECPublicKey) encryptionKey).getParams(); + KeyPair ephemeralKeyPair = generateEcKeyPair(params); + ECPublicKey ephemeralPublicKey = (ECPublicKey) ephemeralKeyPair.getPublic(); + ECPrivateKey ephemeralPrivateKey = (ECPrivateKey) ephemeralKeyPair.getPrivate(); + + byte[] agreementPartyUInfo = header.getAgreementPartyUInfo() != null + ? base64UrlDecode(header.getAgreementPartyUInfo()) + : new byte[0]; + byte[] agreementPartyVInfo = header.getAgreementPartyVInfo() != null + ? base64UrlDecode(header.getAgreementPartyVInfo()) + : new byte[0]; + + headerBuilder.ephemeralPublicKey(toECPublicJWK(ephemeralPublicKey)); + + String algorithmID = getAlgorithmID(header.getAlgorithm(), header.getEncryptionAlgorithm()); + byte[] derivedKey = deriveKey(encryptionKey, ephemeralPrivateKey, keyDataLength, algorithmID, + agreementPartyUInfo, agreementPartyVInfo); + if (Algorithm.ECDH_ES.equals(header.getAlgorithm())) { + keyStorage.setCEKBytes(derivedKey); + encryptionProvider.deserializeCEK(keyStorage); + return new byte[0]; + } else { + Wrapper encrypter = new AESWrapEngine(); + encrypter.init(true, new KeyParameter(derivedKey)); + byte[] cekBytes = keyStorage.getCekBytes(); + return encrypter.wrap(cekBytes, 0, cekBytes.length); + } + } + + private byte[] base64UrlDecode(String encoded) { + return Base64Url.decode(encoded == null ? "" : encoded); + } + + private static KeyPair generateEcKeyPair(ECParameterSpec params) { + try { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC"); + SecureRandom randomGen = SecureRandom.getInstance("SHA1PRNG"); + keyGen.initialize(params, randomGen); + return keyGen.generateKeyPair(); + } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException e) { + throw new IllegalArgumentException(e); + } + } + + private static byte[] deriveOtherInfo(int keyDataLength, String algorithmID, byte[] agreementPartyUInfo, + byte[] agreementPartyVInfo) { + byte[] algorithmId = encodeDataLengthData(algorithmID.getBytes(Charset.forName("ASCII"))); + byte[] partyUInfo = encodeDataLengthData(agreementPartyUInfo); + byte[] partyVInfo = encodeDataLengthData(agreementPartyVInfo); + byte[] suppPubInfo = toByteArray(keyDataLength); + byte[] suppPrivInfo = emptyBytes(); + return concat(algorithmId, partyUInfo, partyVInfo, suppPubInfo, suppPrivInfo); + } + + public static byte[] deriveKey(Key publicKey, Key privateKey, int keyDataLength, String algorithmID, + byte[] agreementPartyUInfo, byte[] agreementPartyVInfo) + throws InvalidKeyException, NoSuchAlgorithmException, IllegalStateException { + byte[] z = deriveSharedSecret(publicKey, privateKey); + byte[] otherInfo = deriveOtherInfo(keyDataLength, algorithmID, agreementPartyUInfo, agreementPartyVInfo); + KDFParameters param = new KDFParameters(z, otherInfo); + ConcatenationKDFGenerator concatKdf = new ConcatenationKDFGenerator(DigestFactory.createSHA256()); + concatKdf.init(param); + int derivedKeyLength = keyDataLength / 8; + byte[] derivedKeyBytes = new byte[derivedKeyLength]; + concatKdf.generateBytes(derivedKeyBytes, 0, derivedKeyLength); + return derivedKeyBytes; + } + + private static ECPublicJWK toECPublicJWK(ECPublicKey ecKey) { + ECPublicJWK k = new ECPublicJWK(); + int fieldSize = ecKey.getParams().getCurve().getField().getFieldSize(); + k.setCrv("P-" + fieldSize); + k.setKeyType(KeyType.EC); + k.setX(Base64Url.encode(JWKUtil.toIntegerBytes(ecKey.getW().getAffineX(), fieldSize))); + k.setY(Base64Url.encode(JWKUtil.toIntegerBytes(ecKey.getW().getAffineY(), fieldSize))); + return k; + } + + private static PublicKey toPublicKey(ECPublicJWK jwk) { + String crv = jwk.getCrv(); + String xStr = jwk.getX(); + String yStr = jwk.getY(); + + if (crv == null) { + throw new IllegalArgumentException("JWK crv must be set"); + } + if (xStr == null) { + throw new IllegalArgumentException("JWK x must be set"); + } + if (yStr == null) { + throw new IllegalArgumentException("JWK y must be set"); + } + + BigInteger x = new BigInteger(1, Base64Url.decode(xStr)); + BigInteger y = new BigInteger(1, Base64Url.decode(yStr)); + + String name = nistToSecCurveName(crv); + try { + ECPoint point = new ECPoint(x, y); + ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec(name); + ECParameterSpec params = new ECNamedCurveSpec(name, spec.getCurve(), spec.getG(), spec.getN()); + ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(point, params); + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + return keyFactory.generatePublic(pubKeySpec); + } catch (InvalidKeySpecException | NoSuchAlgorithmException e) { + throw new IllegalArgumentException(e); + } + } + + private static byte[] deriveSharedSecret(Key publicKey, Key privateKey) + throws NoSuchAlgorithmException, InvalidKeyException, IllegalStateException { + KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH"); + keyAgreement.init(privateKey); + keyAgreement.doPhase(publicKey, true); + return keyAgreement.generateSecret(); + } + + private static String getAlgorithmID(String alg, String enc) { + if (Algorithm.ECDH_ES_A128KW.equals(alg) || Algorithm.ECDH_ES_A192KW.equals(alg) + || Algorithm.ECDH_ES_A256KW.equals(alg)) { + return alg; + } else if (Algorithm.ECDH_ES.equals(alg)) { + return enc; + } else { + throw new IllegalArgumentException("Unsupported algorithm"); + } + } + + private static String nistToSecCurveName(String nistCurveName) { + switch (nistCurveName) { + case "P-256": + return "secp256r1"; + case "P-384": + return "secp384r1"; + case "P-521": + return "secp521r1"; + default: + throw new IllegalArgumentException("Unsupported curve"); + } + } + + private static int getKeyDataLength(String alg, JWEEncryptionProvider encryptionProvider) { + if (Algorithm.ECDH_ES_A128KW.equals(alg)) { + return 128; + } else if (Algorithm.ECDH_ES_A192KW.equals(alg)) { + return 192; + } else if (Algorithm.ECDH_ES_A256KW.equals(alg)) { + return 256; + } else if (Algorithm.ECDH_ES.equals(alg)) { + return encryptionProvider.getExpectedCEKLength() * 8; + } else { + throw new IllegalArgumentException("Unsupported algorithm"); + } + } + + private static byte[] encodeDataLengthData(final byte[] data) { + byte[] databytes = data != null ? data : new byte[0]; + byte[] datalen = toByteArray(databytes.length); + return concat(datalen, databytes); + } + + private static byte[] emptyBytes() { + return new byte[0]; + } + + private static byte[] toByteArray(int intValue) { + return new byte[] { (byte) (intValue >> 24), (byte) (intValue >> 16), (byte) (intValue >> 8), (byte) intValue }; + } + + private static byte[] concat(byte[]... byteArrays) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + for (byte[] bytes : byteArrays) { + if (bytes != null) { + baos.write(bytes); + } + } + return baos.toByteArray(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/crypto/default/src/main/java/org/keycloak/crypto/def/DefaultCryptoProvider.java b/crypto/default/src/main/java/org/keycloak/crypto/def/DefaultCryptoProvider.java index bd0d0c9c925..b28bac33fd3 100644 --- a/crypto/default/src/main/java/org/keycloak/crypto/def/DefaultCryptoProvider.java +++ b/crypto/default/src/main/java/org/keycloak/crypto/def/DefaultCryptoProvider.java @@ -60,6 +60,10 @@ public class DefaultCryptoProvider implements CryptoProvider { providers.put(CryptoConstants.RSA1_5, new DefaultRsaKeyEncryptionJWEAlgorithmProvider("RSA/ECB/PKCS1Padding")); providers.put(CryptoConstants.RSA_OAEP, new DefaultRsaKeyEncryptionJWEAlgorithmProvider("RSA/ECB/OAEPWithSHA-1AndMGF1Padding")); providers.put(CryptoConstants.RSA_OAEP_256, new DefaultRsaKeyEncryption256JWEAlgorithmProvider("RSA/ECB/OAEPWithSHA-256AndMGF1Padding")); + providers.put(CryptoConstants.ECDH_ES, new BCEcdhEsAlgorithmProvider()); + providers.put(CryptoConstants.ECDH_ES_A128KW, new BCEcdhEsAlgorithmProvider()); + providers.put(CryptoConstants.ECDH_ES_A192KW, new BCEcdhEsAlgorithmProvider()); + providers.put(CryptoConstants.ECDH_ES_A256KW, new BCEcdhEsAlgorithmProvider()); if (existingBc == null) { Security.addProvider(this.bcProvider); diff --git a/crypto/default/src/main/java/org/keycloak/crypto/def/DefaultRsaKeyEncryptionJWEAlgorithmProvider.java b/crypto/default/src/main/java/org/keycloak/crypto/def/DefaultRsaKeyEncryptionJWEAlgorithmProvider.java index b0bc523e7ca..f8875e2277c 100644 --- a/crypto/default/src/main/java/org/keycloak/crypto/def/DefaultRsaKeyEncryptionJWEAlgorithmProvider.java +++ b/crypto/default/src/main/java/org/keycloak/crypto/def/DefaultRsaKeyEncryptionJWEAlgorithmProvider.java @@ -3,7 +3,9 @@ package org.keycloak.crypto.def; import java.security.Key; import javax.crypto.Cipher; +import org.keycloak.jose.jwe.JWEHeader; import org.keycloak.jose.jwe.JWEKeyStorage; +import org.keycloak.jose.jwe.JWEHeader.JWEHeaderBuilder; import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider; import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; @@ -16,14 +18,14 @@ public class DefaultRsaKeyEncryptionJWEAlgorithmProvider implements JWEAlgorithm } @Override - public byte[] decodeCek(byte[] encodedCek, Key privateKey) throws Exception { + public byte[] decodeCek(byte[] encodedCek, Key privateKey, JWEHeader header, JWEEncryptionProvider encryptionProvider) throws Exception { Cipher cipher = getCipherProvider(); initCipher(cipher, Cipher.DECRYPT_MODE, privateKey); return cipher.doFinal(encodedCek); } @Override - public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key publicKey) throws Exception { + public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key publicKey, JWEHeaderBuilder headerBuilder) throws Exception { Cipher cipher = getCipherProvider(); initCipher(cipher, Cipher.ENCRYPT_MODE, publicKey); byte[] cekBytes = keyStorage.getCekBytes(); diff --git a/crypto/default/src/test/java/org/keycloak/crypto/def/test/BCEcdhEsAlgorithmProviderTest.java b/crypto/default/src/test/java/org/keycloak/crypto/def/test/BCEcdhEsAlgorithmProviderTest.java new file mode 100644 index 00000000000..d57c5faa2f1 --- /dev/null +++ b/crypto/default/src/test/java/org/keycloak/crypto/def/test/BCEcdhEsAlgorithmProviderTest.java @@ -0,0 +1,142 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.crypto.def.test; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPrivateKeySpec; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; + +import org.bouncycastle.jce.ECNamedCurveTable; +import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec; +import org.bouncycastle.jce.spec.ECNamedCurveSpec; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.keycloak.common.util.Base64Url; +import org.keycloak.common.util.Environment; +import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.def.BCEcdhEsAlgorithmProvider; +import org.keycloak.jose.jwe.JWE; +import org.keycloak.jose.jwe.JWEConstants; +import org.keycloak.jose.jwe.JWEException; +import org.keycloak.jose.jwe.JWEHeader; +import org.keycloak.rule.CryptoInitRule; + +public class BCEcdhEsAlgorithmProviderTest { + @Before + public void before() { + // Run this test just if java is not in FIPS mode + Assume.assumeFalse("Java is in FIPS mode. Skipping the test.", Environment.isJavaInFipsMode()); + } + + @ClassRule + public static CryptoInitRule cryptoInitRule = new CryptoInitRule(); + + /** + * Test ECDH-ES Key Agreement Computation. + * + * @see Example + * ECDH-ES Key Agreement Computation + * @throws InvalidKeySpecException exception + * @throws NoSuchAlgorithmException exception + * @throws NoSuchProviderException exception + * @throws IllegalStateException exception + * @throws InvalidKeyException exception + */ + @Test + public void deriveKey() throws InvalidKeySpecException, NoSuchAlgorithmException, NoSuchProviderException, + InvalidKeyException, IllegalStateException { + PrivateKey ephemeralPrivateKey = getPrivateKey("P-256", "0_NxaRPUMQoAJt50Gz8YiTr8gRTwyEaCumd-MToTmIo"); + PublicKey encryptionPublicKey = getPublicKey("P-256", "weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", + "e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck"); + byte[] derivedKey = BCEcdhEsAlgorithmProvider.deriveKey(encryptionPublicKey, ephemeralPrivateKey, 128, + "A128GCM", Base64Url.decode("QWxpY2U"), Base64Url.decode("Qm9i")); + Assert.assertEquals("VqqN6vgjbSBcIijNcacQGg", Base64Url.encode(derivedKey)); + } + + @Test + public void encodeDecode() + throws JWEException, InvalidKeySpecException, NoSuchAlgorithmException, NoSuchProviderException { + PublicKey encryptionPublicKey = getPublicKey("P-256", "weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", + "e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck"); + String content = "plaintext"; + JWE jweEncode = new JWE() + .header(JWEHeader.builder().algorithm(Algorithm.ECDH_ES_A128KW) + .encryptionAlgorithm(JWEConstants.A128CBC_HS256) + .build()) + .content(content.getBytes(StandardCharsets.UTF_8)); + jweEncode.getKeyStorage().setEncryptionKey(encryptionPublicKey); + String encodedJwe = jweEncode.encodeJwe(); + + PrivateKey decryptionPrivateKey = getPrivateKey("P-256", "VEmDZpDXXK8p8N0Cndsxs924q6nS1RXFASRl6BfUqdw"); + JWE jweDecode = new JWE(); + jweDecode.getKeyStorage().setDecryptionKey(decryptionPrivateKey); + jweDecode = jweDecode.verifyAndDecodeJwe(encodedJwe); + Assert.assertArrayEquals(jweEncode.getContent(), jweDecode.getContent()); + } + + private PublicKey getPublicKey(String crv, String xStr, String yStr) + throws InvalidKeySpecException, NoSuchAlgorithmException, NoSuchProviderException { + BigInteger x = new BigInteger(1, Base64Url.decode(xStr)); + BigInteger y = new BigInteger(1, Base64Url.decode(yStr)); + ECPoint point = new ECPoint(x, y); + String name = nistToSecCurveName(crv); + ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec(name); + ECParameterSpec params = new ECNamedCurveSpec(name, spec.getCurve(), spec.getG(), spec.getN()); + ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(point, params); + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + return keyFactory.generatePublic(pubKeySpec); + } + + private PrivateKey getPrivateKey(String crv, String dStr) + throws InvalidKeySpecException, NoSuchAlgorithmException, NoSuchProviderException { + BigInteger d = new BigInteger(1, Base64Url.decode(dStr)); + String name = nistToSecCurveName(crv); + ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec(name); + ECParameterSpec params = new ECNamedCurveSpec(name, spec.getCurve(), spec.getG(), spec.getN()); + ECPrivateKeySpec privKeySpec = new ECPrivateKeySpec(d, params); + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + return keyFactory.generatePrivate(privKeySpec); + } + + private static String nistToSecCurveName(String nistCurveName) { + switch (nistCurveName) { + case "P-256": + return "secp256r1"; + case "P-384": + return "secp384r1"; + case "P-521": + return "secp521r1"; + default: + throw new IllegalArgumentException("Unsupported curve"); + } + } +} \ No newline at end of file diff --git a/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/AesKeyWrapAlgorithmProvider.java b/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/AesKeyWrapAlgorithmProvider.java index ffc16e19610..0a5a3839588 100644 --- a/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/AesKeyWrapAlgorithmProvider.java +++ b/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/AesKeyWrapAlgorithmProvider.java @@ -20,7 +20,9 @@ import java.security.Key; import javax.crypto.Cipher; +import org.keycloak.jose.jwe.JWEHeader; import org.keycloak.jose.jwe.JWEKeyStorage; +import org.keycloak.jose.jwe.JWEHeader.JWEHeaderBuilder; import org.keycloak.jose.jwe.JWEKeyStorage.KeyUse; import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider; import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; @@ -31,14 +33,14 @@ import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; public class AesKeyWrapAlgorithmProvider implements JWEAlgorithmProvider { @Override - public byte[] decodeCek(byte[] encodedCek, Key encryptionKey) throws Exception { + public byte[] decodeCek(byte[] encodedCek, Key encryptionKey, JWEHeader header, JWEEncryptionProvider encryptionProvider) throws Exception { Cipher cipher = Cipher.getInstance("AESWrap_128"); cipher.init(Cipher.UNWRAP_MODE, encryptionKey); return cipher.unwrap(encodedCek, "AES", Cipher.SECRET_KEY).getEncoded(); } @Override - public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey) throws Exception { + public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey, JWEHeaderBuilder headerBuilder) throws Exception { Cipher cipher = Cipher.getInstance("AESWrap_128"); cipher.init(Cipher.WRAP_MODE, encryptionKey); return cipher.wrap(keyStorage.getCEKKey(KeyUse.ENCRYPTION, false)); diff --git a/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronEcdhEsAlgorithmProvider.java b/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronEcdhEsAlgorithmProvider.java new file mode 100644 index 00000000000..436d7c8125d --- /dev/null +++ b/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronEcdhEsAlgorithmProvider.java @@ -0,0 +1,252 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.crypto.elytron; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECParameterSpec; + +import javax.crypto.Cipher; +import javax.crypto.KeyAgreement; +import javax.crypto.spec.SecretKeySpec; + +import org.jose4j.jwe.kdf.ConcatKeyDerivationFunction; +import org.jose4j.keys.EcKeyUtil; +import org.jose4j.keys.EllipticCurves; +import org.jose4j.lang.JoseException; +import org.keycloak.common.util.Base64Url; +import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.KeyType; +import org.keycloak.jose.jwe.JWEHeader; +import org.keycloak.jose.jwe.JWEHeader.JWEHeaderBuilder; +import org.keycloak.jose.jwe.JWEKeyStorage; +import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider; +import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; +import org.keycloak.jose.jwk.ECPublicJWK; +import org.keycloak.jose.jwk.JWKUtil; + +/** + * ECDH Ephemeral Static Algorithm Provider. + * + * @author Justin Tay + * @see Key + * Derivation for ECDH Key Agreement + */ +public class ElytronEcdhEsAlgorithmProvider implements JWEAlgorithmProvider { + + @Override + public byte[] decodeCek(byte[] encodedCek, Key encryptionKey, JWEHeader header, + JWEEncryptionProvider encryptionProvider) throws Exception { + int keyDataLength = getKeyDataLength(header.getAlgorithm(), encryptionProvider); + PublicKey sharedPublicKey = toPublicKey(header.getEphemeralPublicKey()); + + String algorithmID = getAlgorithmID(header.getAlgorithm(), header.getEncryptionAlgorithm()); + byte[] derivedKey = deriveKey(sharedPublicKey, encryptionKey, keyDataLength, algorithmID, + base64UrlDecode(header.getAgreementPartyUInfo()), base64UrlDecode(header.getAgreementPartyVInfo())); + + if (Algorithm.ECDH_ES.equals(header.getAlgorithm())) { + return derivedKey; + } else { + Cipher cipher = Cipher.getInstance(getAesWrapAlgorithm(header.getAlgorithm())); + cipher.init(Cipher.UNWRAP_MODE, new SecretKeySpec(derivedKey, "AES")); + return cipher.unwrap(encodedCek, "AES", Cipher.SECRET_KEY).getEncoded(); + } + } + + @Override + public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey, + JWEHeaderBuilder headerBuilder) throws Exception { + JWEHeader header = headerBuilder.build(); + int keyDataLength = getKeyDataLength(header.getAlgorithm(), encryptionProvider); + ECParameterSpec params = ((ECPublicKey) encryptionKey).getParams(); + KeyPair ephemeralKeyPair = generateEcKeyPair(params); + ECPublicKey ephemeralPublicKey = (ECPublicKey) ephemeralKeyPair.getPublic(); + ECPrivateKey ephemeralPrivateKey = (ECPrivateKey) ephemeralKeyPair.getPrivate(); + + byte[] agreementPartyUInfo = header.getAgreementPartyUInfo() != null + ? base64UrlDecode(header.getAgreementPartyUInfo()) + : new byte[0]; + byte[] agreementPartyVInfo = header.getAgreementPartyVInfo() != null + ? base64UrlDecode(header.getAgreementPartyVInfo()) + : new byte[0]; + + headerBuilder.ephemeralPublicKey(toECPublicJWK(ephemeralPublicKey)); + + String algorithmID = getAlgorithmID(header.getAlgorithm(), header.getEncryptionAlgorithm()); + byte[] derivedKey = deriveKey(encryptionKey, ephemeralPrivateKey, keyDataLength, algorithmID, + agreementPartyUInfo, agreementPartyVInfo); + if (Algorithm.ECDH_ES.equals(header.getAlgorithm())) { + keyStorage.setCEKBytes(derivedKey); + encryptionProvider.deserializeCEK(keyStorage); + return new byte[0]; + } else { + Cipher cipher = Cipher.getInstance(getAesWrapAlgorithm(header.getAlgorithm())); + cipher.init(Cipher.WRAP_MODE, new SecretKeySpec(derivedKey, "AES")); + byte[] cekBytes = keyStorage.getCekBytes(); + return cipher.wrap(new SecretKeySpec(cekBytes, "AES")); + } + } + + private byte[] base64UrlDecode(String encoded) { + return Base64Url.decode(encoded == null ? "" : encoded); + } + + private static KeyPair generateEcKeyPair(ECParameterSpec params) { + try { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC"); + SecureRandom randomGen = SecureRandom.getInstance("SHA1PRNG"); + keyGen.initialize(params, randomGen); + return keyGen.generateKeyPair(); + } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException e) { + throw new IllegalArgumentException(e); + } + } + + public static byte[] deriveKey(Key publicKey, Key privateKey, int keyDataLength, String algorithmID, + byte[] agreementPartyUInfo, byte[] agreementPartyVInfo) + throws InvalidKeyException, NoSuchAlgorithmException, IllegalStateException { + byte[] z = deriveSharedSecret(publicKey, privateKey); + byte[] suppPrivInfo = emptyBytes(); + ConcatKeyDerivationFunction concatKdf = new ConcatKeyDerivationFunction("SHA-256"); + return concatKdf.kdf(z, keyDataLength, encodeDataLengthData(algorithmID.getBytes(Charset.forName("ASCII"))), + encodeDataLengthData(agreementPartyUInfo), encodeDataLengthData(agreementPartyVInfo), + toByteArray(keyDataLength), suppPrivInfo); + } + + private static ECPublicJWK toECPublicJWK(ECPublicKey ecKey) { + ECPublicJWK k = new ECPublicJWK(); + int fieldSize = ecKey.getParams().getCurve().getField().getFieldSize(); + k.setCrv("P-" + fieldSize); + k.setKeyType(KeyType.EC); + k.setX(Base64Url.encode(JWKUtil.toIntegerBytes(ecKey.getW().getAffineX(), fieldSize))); + k.setY(Base64Url.encode(JWKUtil.toIntegerBytes(ecKey.getW().getAffineY(), fieldSize))); + return k; + } + + private static PublicKey toPublicKey(ECPublicJWK jwk) { + String crv = jwk.getCrv(); + String xStr = jwk.getX(); + String yStr = jwk.getY(); + + if (crv == null) { + throw new IllegalArgumentException("JWK crv must be set"); + } + if (xStr == null) { + throw new IllegalArgumentException("JWK x must be set"); + } + if (yStr == null) { + throw new IllegalArgumentException("JWK y must be set"); + } + + BigInteger x = new BigInteger(1, Base64Url.decode(xStr)); + BigInteger y = new BigInteger(1, Base64Url.decode(yStr)); + + EcKeyUtil ecKeyUtil = new EcKeyUtil(); + try { + return ecKeyUtil.publicKey(x, y, EllipticCurves.getSpec(crv)); + } catch (JoseException e) { + throw new IllegalArgumentException(e); + } + } + + private static byte[] deriveSharedSecret(Key publicKey, Key privateKey) + throws NoSuchAlgorithmException, InvalidKeyException, IllegalStateException { + KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH"); + keyAgreement.init(privateKey); + keyAgreement.doPhase(publicKey, true); + return keyAgreement.generateSecret(); + } + + private static String getAlgorithmID(String alg, String enc) { + if (Algorithm.ECDH_ES_A128KW.equals(alg) || Algorithm.ECDH_ES_A192KW.equals(alg) + || Algorithm.ECDH_ES_A256KW.equals(alg)) { + return alg; + } else if (Algorithm.ECDH_ES.equals(alg)) { + return enc; + } else { + throw new IllegalArgumentException("Unsupported algorithm"); + } + } + + private static int getKeyDataLength(String alg, JWEEncryptionProvider encryptionProvider) { + if (Algorithm.ECDH_ES_A128KW.equals(alg)) { + return 128; + } else if (Algorithm.ECDH_ES_A192KW.equals(alg)) { + return 192; + } else if (Algorithm.ECDH_ES_A256KW.equals(alg)) { + return 256; + } else if (Algorithm.ECDH_ES.equals(alg)) { + return encryptionProvider.getExpectedCEKLength() * 8; + } else { + throw new IllegalArgumentException("Unsupported algorithm"); + } + } + + private static byte[] encodeDataLengthData(final byte[] data) { + byte[] databytes = data != null ? data : new byte[0]; + byte[] datalen = toByteArray(databytes.length); + return concat(datalen, databytes); + } + + private static byte[] emptyBytes() { + return new byte[0]; + } + + private static byte[] toByteArray(int intValue) { + return new byte[] { (byte) (intValue >> 24), (byte) (intValue >> 16), (byte) (intValue >> 8), (byte) intValue }; + } + + private static byte[] concat(byte[]... byteArrays) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + for (byte[] bytes : byteArrays) { + if (bytes != null) { + baos.write(bytes); + } + } + return baos.toByteArray(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private static String getAesWrapAlgorithm(String alg) { + if (Algorithm.ECDH_ES_A128KW.equals(alg)) { + return "AESWrap_128"; + } else if (Algorithm.ECDH_ES_A192KW.equals(alg)) { + return "AESWrap_192"; + } else if (Algorithm.ECDH_ES_A256KW.equals(alg)) { + return "AESWrap_256"; + } else { + throw new IllegalArgumentException("Unsupported algorithm"); + } + } +} diff --git a/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronRsaKeyEncryptionJWEAlgorithmProvider.java b/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronRsaKeyEncryptionJWEAlgorithmProvider.java index 42431bc48ac..94e0b6f1525 100644 --- a/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronRsaKeyEncryptionJWEAlgorithmProvider.java +++ b/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronRsaKeyEncryptionJWEAlgorithmProvider.java @@ -20,7 +20,9 @@ import java.security.Key; import javax.crypto.Cipher; +import org.keycloak.jose.jwe.JWEHeader; import org.keycloak.jose.jwe.JWEKeyStorage; +import org.keycloak.jose.jwe.JWEHeader.JWEHeaderBuilder; import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider; import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; @@ -36,14 +38,14 @@ public class ElytronRsaKeyEncryptionJWEAlgorithmProvider implements JWEAlgorithm } @Override - public byte[] decodeCek(byte[] encodedCek, Key privateKey) throws Exception { + public byte[] decodeCek(byte[] encodedCek, Key privateKey, JWEHeader header, JWEEncryptionProvider encryptionProvider) throws Exception { Cipher cipher = getCipherProvider(); initCipher(cipher, Cipher.DECRYPT_MODE, privateKey); return cipher.doFinal(encodedCek); } @Override - public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key publicKey) throws Exception { + public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key publicKey, JWEHeaderBuilder headerBuilder) throws Exception { Cipher cipher = getCipherProvider(); initCipher(cipher, Cipher.ENCRYPT_MODE, publicKey); byte[] cekBytes = keyStorage.getCekBytes(); diff --git a/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/WildFlyElytronProvider.java b/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/WildFlyElytronProvider.java index 9a61e553a44..9cd84f291a2 100644 --- a/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/WildFlyElytronProvider.java +++ b/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/WildFlyElytronProvider.java @@ -61,6 +61,10 @@ public class WildFlyElytronProvider implements CryptoProvider { providers.put(CryptoConstants.RSA1_5, new ElytronRsaKeyEncryptionJWEAlgorithmProvider("RSA/ECB/PKCS1Padding")); providers.put(CryptoConstants.RSA_OAEP, new ElytronRsaKeyEncryptionJWEAlgorithmProvider("RSA/ECB/OAEPWithSHA-1AndMGF1Padding")); providers.put(CryptoConstants.RSA_OAEP_256, new ElytronRsaKeyEncryption256JWEAlgorithmProvider("RSA/ECB/OAEPWithSHA-256AndMGF1Padding")); + providers.put(CryptoConstants.ECDH_ES, new ElytronEcdhEsAlgorithmProvider()); + providers.put(CryptoConstants.ECDH_ES_A128KW, new ElytronEcdhEsAlgorithmProvider()); + providers.put(CryptoConstants.ECDH_ES_A192KW, new ElytronEcdhEsAlgorithmProvider()); + providers.put(CryptoConstants.ECDH_ES_A256KW, new ElytronEcdhEsAlgorithmProvider()); } @Override diff --git a/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/ElytronEcdhEsAlgorithmProviderTest.java b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/ElytronEcdhEsAlgorithmProviderTest.java new file mode 100644 index 00000000000..b07eeceb162 --- /dev/null +++ b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/ElytronEcdhEsAlgorithmProviderTest.java @@ -0,0 +1,104 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.crypto.elytron.test; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; + +import org.jose4j.keys.EcKeyUtil; +import org.jose4j.keys.EllipticCurves; +import org.jose4j.lang.JoseException; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; +import org.keycloak.common.util.Base64Url; +import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.elytron.ElytronEcdhEsAlgorithmProvider; +import org.keycloak.jose.jwe.JWE; +import org.keycloak.jose.jwe.JWEConstants; +import org.keycloak.jose.jwe.JWEException; +import org.keycloak.jose.jwe.JWEHeader; +import org.keycloak.rule.CryptoInitRule; + +public class ElytronEcdhEsAlgorithmProviderTest { + @ClassRule + public static CryptoInitRule cryptoInitRule = new CryptoInitRule(); + + /** + * Test ECDH-ES Key Agreement Computation. + * + * @see Example + * ECDH-ES Key Agreement Computation + * @throws InvalidKeySpecException exception + * @throws NoSuchAlgorithmException exception + * @throws NoSuchProviderException exception + * @throws IllegalStateException exception + * @throws InvalidKeyException exception + */ + @Test + public void deriveKey() throws JoseException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException { + PrivateKey ephemeralPrivateKey = getPrivateKey("P-256", "0_NxaRPUMQoAJt50Gz8YiTr8gRTwyEaCumd-MToTmIo"); + PublicKey encryptionPublicKey = getPublicKey("P-256", "weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", + "e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck"); + byte[] derivedKey = ElytronEcdhEsAlgorithmProvider.deriveKey(encryptionPublicKey, ephemeralPrivateKey, 128, + "A128GCM", Base64Url.decode("QWxpY2U"), Base64Url.decode("Qm9i")); + Assert.assertEquals("VqqN6vgjbSBcIijNcacQGg", Base64Url.encode(derivedKey)); + } + + @Test + public void encodeDecode() + throws JWEException, InvalidKeySpecException, NoSuchAlgorithmException, NoSuchProviderException, + JoseException { + PublicKey encryptionPublicKey = getPublicKey("P-256", "weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", + "e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck"); + String content = "plaintext"; + JWE jweEncode = new JWE() + .header(JWEHeader.builder().algorithm(Algorithm.ECDH_ES_A128KW) + .encryptionAlgorithm(JWEConstants.A128CBC_HS256) + .build()) + .content(content.getBytes(StandardCharsets.UTF_8)); + jweEncode.getKeyStorage().setEncryptionKey(encryptionPublicKey); + String encodedJwe = jweEncode.encodeJwe(); + + PrivateKey decryptionPrivateKey = getPrivateKey("P-256", "VEmDZpDXXK8p8N0Cndsxs924q6nS1RXFASRl6BfUqdw"); + JWE jweDecode = new JWE(); + jweDecode.getKeyStorage().setDecryptionKey(decryptionPrivateKey); + jweDecode = jweDecode.verifyAndDecodeJwe(encodedJwe); + Assert.assertArrayEquals(jweEncode.getContent(), jweDecode.getContent()); + } + + private PublicKey getPublicKey(String crv, String xStr, String yStr) throws JoseException { + BigInteger x = new BigInteger(1, Base64Url.decode(xStr)); + BigInteger y = new BigInteger(1, Base64Url.decode(yStr)); + EcKeyUtil ecKeyUtil = new EcKeyUtil(); + return ecKeyUtil.publicKey(x, y, EllipticCurves.getSpec(crv)); + } + + private PrivateKey getPrivateKey(String crv, String dStr) throws JoseException { + BigInteger d = new BigInteger(1, Base64Url.decode(dStr)); + EcKeyUtil ecKeyUtil = new EcKeyUtil(); + return ecKeyUtil.privateKey(d, EllipticCurves.getSpec(crv)); + } +} \ No newline at end of file diff --git a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSEcdhEsAlgorithmProvider.java b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSEcdhEsAlgorithmProvider.java new file mode 100644 index 00000000000..5506bd14141 --- /dev/null +++ b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSEcdhEsAlgorithmProvider.java @@ -0,0 +1,262 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.crypto.fips; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.security.InvalidAlgorithmParameterException; +import java.security.Key; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; + +import org.bouncycastle.asn1.nist.NISTNamedCurves; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.crypto.KeyUnwrapper; +import org.bouncycastle.crypto.KeyWrapper; +import org.bouncycastle.crypto.SymmetricKey; +import org.bouncycastle.crypto.SymmetricSecretKey; +import org.bouncycastle.crypto.asymmetric.AsymmetricECPrivateKey; +import org.bouncycastle.crypto.asymmetric.AsymmetricECPublicKey; +import org.bouncycastle.crypto.asymmetric.ECDomainParameters; +import org.bouncycastle.crypto.fips.FipsAES; +import org.bouncycastle.crypto.fips.FipsAES.WrapParameters; +import org.bouncycastle.crypto.fips.FipsAgreement; +import org.bouncycastle.crypto.fips.FipsEC; +import org.bouncycastle.crypto.fips.FipsKDF; +import org.bouncycastle.crypto.fips.FipsKDF.AgreementKDFPRF; +import org.bouncycastle.jcajce.spec.ECDomainParameterSpec; +import org.keycloak.common.util.Base64Url; +import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.KeyType; +import org.keycloak.jose.jwe.JWEHeader; +import org.keycloak.jose.jwe.JWEHeader.JWEHeaderBuilder; +import org.keycloak.jose.jwe.JWEKeyStorage; +import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider; +import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; +import org.keycloak.jose.jwk.ECPublicJWK; +import org.keycloak.jose.jwk.JWKUtil; + +/** + * ECDH Ephemeral Static Algorithm Provider. + * + * @author Justin Tay + * @see Key + * Derivation for ECDH Key Agreement + */ +public class BCFIPSEcdhEsAlgorithmProvider implements JWEAlgorithmProvider { + + @Override + public byte[] decodeCek(byte[] encodedCek, Key encryptionKey, JWEHeader header, + JWEEncryptionProvider encryptionProvider) throws Exception { + int keyDataLength = getKeyDataLength(header.getAlgorithm(), encryptionProvider); + PublicKey sharedPublicKey = toPublicKey(header.getEphemeralPublicKey()); + + String algorithmID = getAlgorithmID(header.getAlgorithm(), header.getEncryptionAlgorithm()); + byte[] derivedKey = deriveKey(sharedPublicKey, encryptionKey, keyDataLength, algorithmID, + base64UrlDecode(header.getAgreementPartyUInfo()), base64UrlDecode(header.getAgreementPartyVInfo())); + + if (Algorithm.ECDH_ES.equals(header.getAlgorithm())) { + return derivedKey; + } else { + SymmetricKey aesKey = new SymmetricSecretKey(FipsAES.KW, derivedKey); + FipsAES.KeyWrapOperatorFactory factory = new FipsAES.KeyWrapOperatorFactory(); + KeyUnwrapper unwrapper = factory.createKeyUnwrapper(aesKey, FipsAES.KW); + return unwrapper.unwrap(encodedCek, 0, encodedCek.length); + } + } + + @Override + public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey, + JWEHeaderBuilder headerBuilder) throws Exception { + JWEHeader header = headerBuilder.build(); + int keyDataLength = getKeyDataLength(header.getAlgorithm(), encryptionProvider); + ECParameterSpec params = ((ECPublicKey) encryptionKey).getParams(); + KeyPair ephemeralKeyPair = generateEcKeyPair(params); + ECPublicKey ephemeralPublicKey = (ECPublicKey) ephemeralKeyPair.getPublic(); + ECPrivateKey ephemeralPrivateKey = (ECPrivateKey) ephemeralKeyPair.getPrivate(); + + byte[] agreementPartyUInfo = header.getAgreementPartyUInfo() != null + ? base64UrlDecode(header.getAgreementPartyUInfo()) + : new byte[0]; + byte[] agreementPartyVInfo = header.getAgreementPartyVInfo() != null + ? base64UrlDecode(header.getAgreementPartyVInfo()) + : new byte[0]; + + headerBuilder.ephemeralPublicKey(toECPublicJWK(ephemeralPublicKey)); + + String algorithmID = getAlgorithmID(header.getAlgorithm(), header.getEncryptionAlgorithm()); + byte[] derivedKey = deriveKey(encryptionKey, ephemeralPrivateKey, keyDataLength, algorithmID, + agreementPartyUInfo, agreementPartyVInfo); + + if (Algorithm.ECDH_ES.equals(header.getAlgorithm())) { + keyStorage.setCEKBytes(derivedKey); + encryptionProvider.deserializeCEK(keyStorage); + return new byte[0]; + } else { + byte[] inputKeyBytes = keyStorage.getCekBytes(); // bytes making up the key to be wrapped + byte[] keyBytes = derivedKey; // bytes making up AES key doing the wrapping + SymmetricKey aesKey = new SymmetricSecretKey(FipsAES.KW, keyBytes); + FipsAES.KeyWrapOperatorFactory factory = new FipsAES.KeyWrapOperatorFactory(); + KeyWrapper wrapper = factory.createKeyWrapper(aesKey, FipsAES.KW); + return wrapper.wrap(inputKeyBytes, 0, inputKeyBytes.length); + } + } + + private byte[] base64UrlDecode(String encoded) { + return Base64Url.decode(encoded == null ? "" : encoded); + } + + private static KeyPair generateEcKeyPair(ECParameterSpec params) { + try { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC", "BCFIPS"); + SecureRandom randomGen = SecureRandom.getInstance("DEFAULT", "BCFIPS"); + keyGen.initialize(params, randomGen); + return keyGen.generateKeyPair(); + } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException | NoSuchProviderException e) { + throw new IllegalArgumentException(e); + } + } + + private static byte[] deriveOtherInfo(int keyDataLength, String algorithmID, byte[] agreementPartyUInfo, + byte[] agreementPartyVInfo) { + byte[] algorithmId = encodeDataLengthData(algorithmID.getBytes(Charset.forName("ASCII"))); + byte[] partyUInfo = encodeDataLengthData(agreementPartyUInfo); + byte[] partyVInfo = encodeDataLengthData(agreementPartyVInfo); + byte[] suppPubInfo = toByteArray(keyDataLength); + byte[] suppPrivInfo = emptyBytes(); + return concat(algorithmId, partyUInfo, partyVInfo, suppPubInfo, suppPrivInfo); + } + + public static byte[] deriveKey(Key publicKey, Key privateKey, int keyDataLength, String algorithmID, + byte[] agreementPartyUInfo, byte[] agreementPartyVInfo) { + byte[] otherInfo = deriveOtherInfo(keyDataLength, algorithmID, agreementPartyUInfo, agreementPartyVInfo); + FipsEC.DHAgreementFactory factory = new FipsEC.DHAgreementFactory(); + FipsAgreement agree = factory.createAgreement( + new AsymmetricECPrivateKey(FipsEC.ALGORITHM, privateKey.getEncoded()), + FipsEC.DH.withKDF(FipsKDF.CONCATENATION.withPRF(AgreementKDFPRF.SHA256), otherInfo, keyDataLength / 8)); + return agree.calculate(new AsymmetricECPublicKey(FipsEC.ALGORITHM, publicKey.getEncoded())); + } + + private static ECPublicJWK toECPublicJWK(ECPublicKey ecKey) { + ECPublicJWK k = new ECPublicJWK(); + int fieldSize = ecKey.getParams().getCurve().getField().getFieldSize(); + k.setCrv("P-" + fieldSize); + k.setKeyType(KeyType.EC); + k.setX(Base64Url.encode(JWKUtil.toIntegerBytes(ecKey.getW().getAffineX(), fieldSize))); + k.setY(Base64Url.encode(JWKUtil.toIntegerBytes(ecKey.getW().getAffineY(), fieldSize))); + return k; + } + + private static PublicKey toPublicKey(ECPublicJWK jwk) { + String crv = jwk.getCrv(); + String xStr = jwk.getX(); + String yStr = jwk.getY(); + + if (crv == null) { + throw new IllegalArgumentException("JWK crv must be set"); + } + if (xStr == null) { + throw new IllegalArgumentException("JWK x must be set"); + } + if (yStr == null) { + throw new IllegalArgumentException("JWK y must be set"); + } + + BigInteger x = new BigInteger(1, Base64Url.decode(xStr)); + BigInteger y = new BigInteger(1, Base64Url.decode(yStr)); + + try { + ECPoint point = new ECPoint(x, y); + X9ECParameters ecParams = NISTNamedCurves.getByName(crv); + ECParameterSpec params = new ECDomainParameterSpec( + new ECDomainParameters(ecParams.getCurve(), ecParams.getG(), ecParams.getN(), ecParams.getH())); + ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(point, params); + KeyFactory keyFactory = KeyFactory.getInstance("EC", "BCFIPS"); + return keyFactory.generatePublic(pubKeySpec); + } catch (InvalidKeySpecException | NoSuchAlgorithmException | NoSuchProviderException e) { + throw new IllegalArgumentException(e); + } + } + + private static String getAlgorithmID(String alg, String enc) { + if (Algorithm.ECDH_ES_A128KW.equals(alg) || Algorithm.ECDH_ES_A192KW.equals(alg) + || Algorithm.ECDH_ES_A256KW.equals(alg)) { + return alg; + } else if (Algorithm.ECDH_ES.equals(alg)) { + return enc; + } else { + throw new IllegalArgumentException("Unsupported algorithm"); + } + } + + private static int getKeyDataLength(String alg, JWEEncryptionProvider encryptionProvider) { + if (Algorithm.ECDH_ES_A128KW.equals(alg)) { + return 128; + } else if (Algorithm.ECDH_ES_A192KW.equals(alg)) { + return 192; + } else if (Algorithm.ECDH_ES_A256KW.equals(alg)) { + return 256; + } else if (Algorithm.ECDH_ES.equals(alg)) { + return encryptionProvider.getExpectedCEKLength() * 8; + } else { + throw new IllegalArgumentException("Unsupported algorithm"); + } + } + + private static byte[] encodeDataLengthData(final byte[] data) { + byte[] databytes = data != null ? data : new byte[0]; + byte[] datalen = toByteArray(databytes.length); + return concat(datalen, databytes); + } + + private static byte[] emptyBytes() { + return new byte[0]; + } + + private static byte[] toByteArray(int intValue) { + return new byte[] { (byte) (intValue >> 24), (byte) (intValue >> 16), (byte) (intValue >> 8), (byte) intValue }; + } + + private static byte[] concat(byte[]... byteArrays) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + for (byte[] bytes : byteArrays) { + if (bytes != null) { + baos.write(bytes); + } + } + return baos.toByteArray(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPS1402Provider.java b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPS1402Provider.java index 991c14ccc15..bdc672579b7 100644 --- a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPS1402Provider.java +++ b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPS1402Provider.java @@ -86,6 +86,10 @@ public class FIPS1402Provider implements CryptoProvider { providers.put(CryptoConstants.RSA1_5, new FIPSRsaKeyEncryptionJWEAlgorithmProvider(FipsRSA.WRAP_PKCS1v1_5)); providers.put(CryptoConstants.RSA_OAEP, new FIPSRsaKeyEncryptionJWEAlgorithmProvider(FipsRSA.WRAP_OAEP)); providers.put(CryptoConstants.RSA_OAEP_256, new FIPSRsaKeyEncryptionJWEAlgorithmProvider(FipsRSA.WRAP_OAEP.withDigest(FipsSHS.Algorithm.SHA256))); + providers.put(CryptoConstants.ECDH_ES, new BCFIPSEcdhEsAlgorithmProvider()); + providers.put(CryptoConstants.ECDH_ES_A128KW, new BCFIPSEcdhEsAlgorithmProvider()); + providers.put(CryptoConstants.ECDH_ES_A192KW, new BCFIPSEcdhEsAlgorithmProvider()); + providers.put(CryptoConstants.ECDH_ES_A256KW, new BCFIPSEcdhEsAlgorithmProvider()); Security.insertProviderAt(new KeycloakFipsSecurityProvider(bcFipsProvider), 1); if (existingBcFipsProvider == null) { diff --git a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPSAesKeyWrapAlgorithmProvider.java b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPSAesKeyWrapAlgorithmProvider.java index d563be8bdb7..59f527ca805 100644 --- a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPSAesKeyWrapAlgorithmProvider.java +++ b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPSAesKeyWrapAlgorithmProvider.java @@ -7,7 +7,9 @@ import org.bouncycastle.crypto.KeyWrapper; import org.bouncycastle.crypto.SymmetricKey; import org.bouncycastle.crypto.SymmetricSecretKey; import org.bouncycastle.crypto.fips.FipsAES; +import org.keycloak.jose.jwe.JWEHeader; import org.keycloak.jose.jwe.JWEKeyStorage; +import org.keycloak.jose.jwe.JWEHeader.JWEHeaderBuilder; import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider; import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; @@ -17,7 +19,7 @@ import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; public class FIPSAesKeyWrapAlgorithmProvider implements JWEAlgorithmProvider { @Override - public byte[] decodeCek(byte[] encodedCek, Key encryptionKey) throws Exception { + public byte[] decodeCek(byte[] encodedCek, Key encryptionKey, JWEHeader header, JWEEncryptionProvider encryptionProvider) throws Exception { byte[] keyBytes = encryptionKey.getEncoded(); // bytes making up AES key doing the wrapping SymmetricKey aesKey = new SymmetricSecretKey(FipsAES.KW, keyBytes); FipsAES.KeyWrapOperatorFactory factory = new FipsAES.KeyWrapOperatorFactory(); @@ -26,7 +28,7 @@ public class FIPSAesKeyWrapAlgorithmProvider implements JWEAlgorithmProvider { } @Override - public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey) throws Exception { + public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey, JWEHeaderBuilder headerBuilder) throws Exception { byte[] inputKeyBytes = keyStorage.getCekBytes(); // bytes making up the key to be wrapped byte[] keyBytes = encryptionKey.getEncoded(); // bytes making up AES key doing the wrapping SymmetricKey aesKey = new SymmetricSecretKey(FipsAES.KW, keyBytes); diff --git a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPSRsaKeyEncryptionJWEAlgorithmProvider.java b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPSRsaKeyEncryptionJWEAlgorithmProvider.java index a9a23c08e54..4fc6e7b25ef 100644 --- a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPSRsaKeyEncryptionJWEAlgorithmProvider.java +++ b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPSRsaKeyEncryptionJWEAlgorithmProvider.java @@ -8,7 +8,9 @@ import org.bouncycastle.crypto.KeyWrapperUsingSecureRandom; import org.bouncycastle.crypto.asymmetric.AsymmetricRSAPrivateKey; import org.bouncycastle.crypto.asymmetric.AsymmetricRSAPublicKey; import org.bouncycastle.crypto.fips.FipsRSA; +import org.keycloak.jose.jwe.JWEHeader; import org.keycloak.jose.jwe.JWEKeyStorage; +import org.keycloak.jose.jwe.JWEHeader.JWEHeaderBuilder; import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider; import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; @@ -27,7 +29,7 @@ public class FIPSRsaKeyEncryptionJWEAlgorithmProvider implements JWEAlgorithmPro } @Override - public byte[] decodeCek(byte[] encodedCek, Key privateKey) throws Exception { + public byte[] decodeCek(byte[] encodedCek, Key privateKey, JWEHeader header, JWEEncryptionProvider encryptionProvider) throws Exception { AsymmetricRSAPrivateKey rsaPrivateKey = new AsymmetricRSAPrivateKey(FipsRSA.ALGORITHM, privateKey.getEncoded()); @@ -41,7 +43,7 @@ public class FIPSRsaKeyEncryptionJWEAlgorithmProvider implements JWEAlgorithmPro @Override - public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key publicKey) throws Exception { + public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key publicKey, JWEHeaderBuilder headerBuilder) throws Exception { AsymmetricRSAPublicKey rsaPubKey = new AsymmetricRSAPublicKey(FipsRSA.ALGORITHM, publicKey.getEncoded()); byte[] inputKeyBytes = keyStorage.getCekBytes(); diff --git a/crypto/fips1402/src/test/java/org/keycloak/crypto/fips/test/BCFIPSECDSACryptoProviderTest.java b/crypto/fips1402/src/test/java/org/keycloak/crypto/fips/test/BCFIPSECDSACryptoProviderTest.java index 79e58f92766..d3e41dd9a49 100644 --- a/crypto/fips1402/src/test/java/org/keycloak/crypto/fips/test/BCFIPSECDSACryptoProviderTest.java +++ b/crypto/fips1402/src/test/java/org/keycloak/crypto/fips/test/BCFIPSECDSACryptoProviderTest.java @@ -23,7 +23,9 @@ import org.junit.runners.Parameterized; import org.keycloak.common.crypto.CryptoIntegration; import org.keycloak.crypto.Algorithm; import org.keycloak.crypto.fips.BCFIPSECDSACryptoProvider; -import org.keycloak.keys.AbstractEcdsaKeyProviderFactory; +import org.keycloak.keys.AbstractEcKeyProviderFactory; +import org.keycloak.keys.GeneratedEcdhKeyProviderFactory; +import org.keycloak.keys.GeneratedEcdsaKeyProviderFactory; import org.keycloak.rule.CryptoInitRule; import java.security.InvalidAlgorithmParameterException; @@ -73,8 +75,11 @@ public class BCFIPSECDSACryptoProviderTest { try { KeyPairGenerator kpg = CryptoIntegration.getProvider().getKeyPairGen("ECDSA"); - String domainParamNistRep = AbstractEcdsaKeyProviderFactory.convertAlgorithmToECDomainParmNistRep(algorithm); - String curve = AbstractEcdsaKeyProviderFactory.convertECDomainParmNistRepToSecRep(domainParamNistRep); + String domainParamNistRep = GeneratedEcdsaKeyProviderFactory.convertJWSAlgorithmToECDomainParmNistRep(algorithm); + if (domainParamNistRep == null) { + domainParamNistRep = GeneratedEcdhKeyProviderFactory.convertJWEAlgorithmToECDomainParmNistRep(algorithm); + } + String curve = AbstractEcKeyProviderFactory.convertECDomainParmNistRepToSecRep(domainParamNistRep); ECGenParameterSpec parameterSpec = new ECGenParameterSpec(curve); kpg.initialize(parameterSpec); return kpg.generateKeyPair(); diff --git a/crypto/fips1402/src/test/java/org/keycloak/crypto/fips/test/BCFIPSEcdhEsAlgorithmProviderTest.java b/crypto/fips1402/src/test/java/org/keycloak/crypto/fips/test/BCFIPSEcdhEsAlgorithmProviderTest.java new file mode 100644 index 00000000000..6124220908a --- /dev/null +++ b/crypto/fips1402/src/test/java/org/keycloak/crypto/fips/test/BCFIPSEcdhEsAlgorithmProviderTest.java @@ -0,0 +1,117 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.crypto.fips.test; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPrivateKeySpec; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; + +import org.bouncycastle.asn1.nist.NISTNamedCurves; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.crypto.asymmetric.ECDomainParameters; +import org.bouncycastle.jcajce.spec.ECDomainParameterSpec; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; +import org.keycloak.common.util.Base64Url; +import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.fips.BCFIPSEcdhEsAlgorithmProvider; +import org.keycloak.jose.jwe.JWE; +import org.keycloak.jose.jwe.JWEConstants; +import org.keycloak.jose.jwe.JWEException; +import org.keycloak.jose.jwe.JWEHeader; +import org.keycloak.rule.CryptoInitRule; + +public class BCFIPSEcdhEsAlgorithmProviderTest { + @ClassRule + public static CryptoInitRule cryptoInitRule = new CryptoInitRule(); + + /** + * Test ECDH-ES Key Agreement Computation. + * + * @see Example + * ECDH-ES Key Agreement Computation + * @throws InvalidKeySpecException exception + * @throws NoSuchAlgorithmException exception + * @throws NoSuchProviderException exception + */ + @Test + public void deriveKey() throws InvalidKeySpecException, NoSuchAlgorithmException, NoSuchProviderException { + PrivateKey ephemeralPrivateKey = getPrivateKey("P-256", "0_NxaRPUMQoAJt50Gz8YiTr8gRTwyEaCumd-MToTmIo"); + PublicKey encryptionPublicKey = getPublicKey("P-256", "weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", + "e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck"); + byte[] derivedKey = BCFIPSEcdhEsAlgorithmProvider.deriveKey(encryptionPublicKey, ephemeralPrivateKey, 128, + "A128GCM", Base64Url.decode("QWxpY2U"), Base64Url.decode("Qm9i")); + Assert.assertEquals("VqqN6vgjbSBcIijNcacQGg", Base64Url.encode(derivedKey)); + } + + @Test + public void encodeDecode() + throws JWEException, InvalidKeySpecException, NoSuchAlgorithmException, NoSuchProviderException { + PublicKey encryptionPublicKey = getPublicKey("P-256", "weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", + "e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck"); + String content = "plaintext"; + JWE jweEncode = new JWE() + .header(JWEHeader.builder().algorithm(Algorithm.ECDH_ES_A128KW) + .encryptionAlgorithm(JWEConstants.A128CBC_HS256) + .build()) + .content(content.getBytes(StandardCharsets.UTF_8)); + jweEncode.getKeyStorage().setEncryptionKey(encryptionPublicKey); + String encodedJwe = jweEncode.encodeJwe(); + + PrivateKey decryptionPrivateKey = getPrivateKey("P-256", "VEmDZpDXXK8p8N0Cndsxs924q6nS1RXFASRl6BfUqdw"); + JWE jweDecode = new JWE(); + jweDecode.getKeyStorage().setDecryptionKey(decryptionPrivateKey); + jweDecode = jweDecode.verifyAndDecodeJwe(encodedJwe); + Assert.assertArrayEquals(jweEncode.getContent(), jweDecode.getContent()); + } + + private PublicKey getPublicKey(String crv, String xStr, String yStr) + throws InvalidKeySpecException, NoSuchAlgorithmException, NoSuchProviderException { + BigInteger x = new BigInteger(1, Base64Url.decode(xStr)); + BigInteger y = new BigInteger(1, Base64Url.decode(yStr)); + ECPoint point = new ECPoint(x, y); + X9ECParameters ecParams = NISTNamedCurves.getByName(crv); + ECParameterSpec params = new ECDomainParameterSpec( + new ECDomainParameters(ecParams.getCurve(), ecParams.getG(), ecParams.getN(), ecParams.getH())); + ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(point, params); + KeyFactory keyFactory = KeyFactory.getInstance("EC", "BCFIPS"); + return keyFactory.generatePublic(pubKeySpec); + } + + private PrivateKey getPrivateKey(String crv, String dStr) + throws InvalidKeySpecException, NoSuchAlgorithmException, NoSuchProviderException { + BigInteger d = new BigInteger(1, Base64Url.decode(dStr)); + X9ECParameters ecParams = NISTNamedCurves.getByName(crv); + ECParameterSpec params = new ECDomainParameterSpec( + new ECDomainParameters(ecParams.getCurve(), ecParams.getG(), ecParams.getN(), ecParams.getH())); + ECPrivateKeySpec privKeySpec = new ECPrivateKeySpec(d, params); + KeyFactory keyFactory = KeyFactory.getInstance("EC", "BCFIPS"); + return keyFactory.generatePrivate(privKeySpec); + } +} diff --git a/services/src/main/java/org/keycloak/crypto/EcdhEsA128KwCekManagementProviderFactory.java b/services/src/main/java/org/keycloak/crypto/EcdhEsA128KwCekManagementProviderFactory.java new file mode 100644 index 00000000000..3fa6e3334a6 --- /dev/null +++ b/services/src/main/java/org/keycloak/crypto/EcdhEsA128KwCekManagementProviderFactory.java @@ -0,0 +1,37 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.crypto; + +import org.keycloak.jose.jwe.JWEConstants; +import org.keycloak.models.KeycloakSession; + +public class EcdhEsA128KwCekManagementProviderFactory implements CekManagementProviderFactory { + + public static final String ID = JWEConstants.ECDH_ES_A128KW; + + @Override + public String getId() { + return ID; + } + + @Override + public CekManagementProvider create(KeycloakSession session) { + return new EcdhEsCekManagementProvider(session, ID); + } + +} diff --git a/services/src/main/java/org/keycloak/crypto/EcdhEsA192KwCekManagementProviderFactory.java b/services/src/main/java/org/keycloak/crypto/EcdhEsA192KwCekManagementProviderFactory.java new file mode 100644 index 00000000000..b07269f427e --- /dev/null +++ b/services/src/main/java/org/keycloak/crypto/EcdhEsA192KwCekManagementProviderFactory.java @@ -0,0 +1,37 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.crypto; + +import org.keycloak.jose.jwe.JWEConstants; +import org.keycloak.models.KeycloakSession; + +public class EcdhEsA192KwCekManagementProviderFactory implements CekManagementProviderFactory { + + public static final String ID = JWEConstants.ECDH_ES_A192KW; + + @Override + public String getId() { + return ID; + } + + @Override + public CekManagementProvider create(KeycloakSession session) { + return new EcdhEsCekManagementProvider(session, ID); + } + +} diff --git a/services/src/main/java/org/keycloak/crypto/EcdhEsA256KwCekManagementProviderFactory.java b/services/src/main/java/org/keycloak/crypto/EcdhEsA256KwCekManagementProviderFactory.java new file mode 100644 index 00000000000..135b2ad3295 --- /dev/null +++ b/services/src/main/java/org/keycloak/crypto/EcdhEsA256KwCekManagementProviderFactory.java @@ -0,0 +1,37 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.crypto; + +import org.keycloak.jose.jwe.JWEConstants; +import org.keycloak.models.KeycloakSession; + +public class EcdhEsA256KwCekManagementProviderFactory implements CekManagementProviderFactory { + + public static final String ID = JWEConstants.ECDH_ES_A256KW; + + @Override + public String getId() { + return ID; + } + + @Override + public CekManagementProvider create(KeycloakSession session) { + return new EcdhEsCekManagementProvider(session, ID); + } + +} diff --git a/services/src/main/java/org/keycloak/crypto/EcdhEsCekManagementProvider.java b/services/src/main/java/org/keycloak/crypto/EcdhEsCekManagementProvider.java new file mode 100644 index 00000000000..41a21c3f36e --- /dev/null +++ b/services/src/main/java/org/keycloak/crypto/EcdhEsCekManagementProvider.java @@ -0,0 +1,46 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.crypto; + +import org.keycloak.common.crypto.CryptoIntegration; +import org.keycloak.jose.jwe.JWEConstants; +import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider; +import org.keycloak.models.KeycloakSession; + +public class EcdhEsCekManagementProvider implements CekManagementProvider { + + private final KeycloakSession session; + private final String jweAlgorithmName; + + public EcdhEsCekManagementProvider(KeycloakSession session, String jweAlgorithmName) { + this.session = session; + this.jweAlgorithmName = jweAlgorithmName; + } + + @Override + public JWEAlgorithmProvider jweAlgorithmProvider() { + if (JWEConstants.ECDH_ES.equals(jweAlgorithmName) || JWEConstants.ECDH_ES_A128KW.equals(jweAlgorithmName) + || JWEConstants.ECDH_ES_A192KW.equals(jweAlgorithmName) + || JWEConstants.ECDH_ES_A256KW.equals(jweAlgorithmName)) { + return CryptoIntegration.getProvider().getAlgorithmProvider(JWEAlgorithmProvider.class, jweAlgorithmName); + } else { + return null; + } + } + +} diff --git a/services/src/main/java/org/keycloak/crypto/EcdhEsCekManagementProviderFactory.java b/services/src/main/java/org/keycloak/crypto/EcdhEsCekManagementProviderFactory.java new file mode 100644 index 00000000000..ff5ad053c32 --- /dev/null +++ b/services/src/main/java/org/keycloak/crypto/EcdhEsCekManagementProviderFactory.java @@ -0,0 +1,37 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.crypto; + +import org.keycloak.jose.jwe.JWEConstants; +import org.keycloak.models.KeycloakSession; + +public class EcdhEsCekManagementProviderFactory implements CekManagementProviderFactory { + + public static final String ID = JWEConstants.ECDH_ES; + + @Override + public String getId() { + return ID; + } + + @Override + public CekManagementProvider create(KeycloakSession session) { + return new EcdhEsCekManagementProvider(session, ID); + } + +} diff --git a/services/src/main/java/org/keycloak/keys/AbstractEcdsaKeyProvider.java b/services/src/main/java/org/keycloak/keys/AbstractEcKeyProvider.java similarity index 87% rename from services/src/main/java/org/keycloak/keys/AbstractEcdsaKeyProvider.java rename to services/src/main/java/org/keycloak/keys/AbstractEcKeyProvider.java index 647adb3c305..5ccbf7bf6bf 100644 --- a/services/src/main/java/org/keycloak/keys/AbstractEcdsaKeyProvider.java +++ b/services/src/main/java/org/keycloak/keys/AbstractEcKeyProvider.java @@ -27,7 +27,7 @@ import org.keycloak.models.RealmModel; import java.security.KeyPair; import java.util.stream.Stream; -public abstract class AbstractEcdsaKeyProvider implements KeyProvider { +public abstract class AbstractEcKeyProvider implements KeyProvider { private final KeyStatus status; @@ -35,7 +35,7 @@ public abstract class AbstractEcdsaKeyProvider implements KeyProvider { private final KeyWrapper key; - public AbstractEcdsaKeyProvider(RealmModel realm, ComponentModel model) { + public AbstractEcKeyProvider(RealmModel realm, ComponentModel model) { this.model = model; this.status = KeyStatus.from(model.get(Attributes.ACTIVE_KEY, true), model.get(Attributes.ENABLED_KEY, true)); @@ -54,16 +54,16 @@ public abstract class AbstractEcdsaKeyProvider implements KeyProvider { return Stream.of(key); } - protected KeyWrapper createKeyWrapper(KeyPair keyPair, String ecInNistRep) { + protected KeyWrapper createKeyWrapper(KeyPair keyPair, String algorithm, KeyUse keyUse) { KeyWrapper key = new KeyWrapper(); key.setProviderId(model.getId()); key.setProviderPriority(model.get("priority", 0l)); key.setKid(KeyUtils.createKeyId(keyPair.getPublic())); - key.setUse(KeyUse.SIG); + key.setUse(keyUse); key.setType(KeyType.EC); - key.setAlgorithm(AbstractEcdsaKeyProviderFactory.convertECDomainParmNistRepToAlgorithm(ecInNistRep)); + key.setAlgorithm(algorithm); key.setStatus(status); key.setPrivateKey(keyPair.getPrivate()); key.setPublicKey(keyPair.getPublic()); diff --git a/services/src/main/java/org/keycloak/keys/AbstractEcdsaKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/AbstractEcKeyProviderFactory.java similarity index 60% rename from services/src/main/java/org/keycloak/keys/AbstractEcdsaKeyProviderFactory.java rename to services/src/main/java/org/keycloak/keys/AbstractEcKeyProviderFactory.java index f704292c4c9..e9b0f0c32c3 100644 --- a/services/src/main/java/org/keycloak/keys/AbstractEcdsaKeyProviderFactory.java +++ b/services/src/main/java/org/keycloak/keys/AbstractEcKeyProviderFactory.java @@ -18,11 +18,9 @@ package org.keycloak.keys; import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentValidationException; -import org.keycloak.crypto.Algorithm; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.provider.ConfigurationValidationHelper; -import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigurationBuilder; import java.security.KeyPair; @@ -30,19 +28,10 @@ import java.security.KeyPairGenerator; import java.security.SecureRandom; import java.security.spec.ECGenParameterSpec; -import static org.keycloak.provider.ProviderConfigProperty.LIST_TYPE; +public abstract class AbstractEcKeyProviderFactory implements KeyProviderFactory { -public abstract class AbstractEcdsaKeyProviderFactory implements KeyProviderFactory { + public static final String DEFAULT_EC_ELLIPTIC_CURVE = "P-256"; - protected static final String ECDSA_PRIVATE_KEY_KEY = "ecdsaPrivateKey"; - protected static final String ECDSA_PUBLIC_KEY_KEY = "ecdsaPublicKey"; - protected static final String ECDSA_ELLIPTIC_CURVE_KEY = "ecdsaEllipticCurveKey"; - - // only support NIST P-256 for ES256, P-384 for ES384, P-521 for ES512 - protected static ProviderConfigProperty ECDSA_ELLIPTIC_CURVE_PROPERTY = new ProviderConfigProperty(ECDSA_ELLIPTIC_CURVE_KEY, "Elliptic Curve", "Elliptic Curve used in ECDSA", LIST_TYPE, - String.valueOf(GeneratedEcdsaKeyProviderFactory.DEFAULT_ECDSA_ELLIPTIC_CURVE), - "P-256", "P-384", "P-521"); - public final static ProviderConfigurationBuilder configurationBuilder() { return ProviderConfigurationBuilder.create() .property(Attributes.PRIORITY_PROPERTY) @@ -58,7 +47,7 @@ public abstract class AbstractEcdsaKeyProviderFactory implements KeyProviderFact .checkBoolean(Attributes.ACTIVE_PROPERTY, false); } - public static KeyPair generateEcdsaKeyPair(String keySpecName) { + public static KeyPair generateEcKeyPair(String keySpecName) { try { KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC"); SecureRandom randomGen = SecureRandom.getInstance("SHA1PRNG"); @@ -75,44 +64,17 @@ public abstract class AbstractEcdsaKeyProviderFactory implements KeyProviderFact String ecInSecRep = null; switch(ecInNistRep) { case "P-256" : - ecInSecRep = "secp256r1"; + ecInSecRep = "secp256r1"; break; case "P-384" : - ecInSecRep = "secp384r1"; + ecInSecRep = "secp384r1"; break; case "P-521" : - ecInSecRep = "secp521r1"; + ecInSecRep = "secp521r1"; break; default : // return null } return ecInSecRep; } - - public static String convertECDomainParmNistRepToAlgorithm(String ecInNistRep) { - switch(ecInNistRep) { - case "P-256" : - return Algorithm.ES256; - case "P-384" : - return Algorithm.ES384; - case "P-521" : - return Algorithm.ES512; - default : - return null; - } - } - - public static String convertAlgorithmToECDomainParmNistRep(String algorithm) { - switch(algorithm) { - case Algorithm.ES256 : - return "P-256"; - case Algorithm.ES384 : - return "P-384"; - case Algorithm.ES512 : - return "P-521"; - default : - return null; - } - } - } diff --git a/services/src/main/java/org/keycloak/keys/AbstractGeneratedEcKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/AbstractGeneratedEcKeyProviderFactory.java new file mode 100644 index 00000000000..dbcac7dedea --- /dev/null +++ b/services/src/main/java/org/keycloak/keys/AbstractGeneratedEcKeyProviderFactory.java @@ -0,0 +1,123 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.keys; + +import org.jboss.logging.Logger; +import org.keycloak.common.util.Base64; +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.component.ComponentModel; +import org.keycloak.component.ComponentValidationException; +import org.keycloak.crypto.KeyUse; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.provider.ConfigurationValidationHelper; +import org.keycloak.provider.ProviderConfigProperty; + +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.interfaces.ECPublicKey; +import java.security.spec.X509EncodedKeySpec; + +public abstract class AbstractGeneratedEcKeyProviderFactory + extends AbstractEcKeyProviderFactory { + + abstract protected String getDefaultEcEllipticCurve(); + + abstract protected String getEcEllipticCurveKey(); + + abstract protected String getEcEllipticCurveKey(String algorithm); + + abstract protected ProviderConfigProperty getEcEllipticCurveProperty(); + + abstract protected String getEcPrivateKeyKey(); + + abstract protected String getEcPublicKeyKey(); + + abstract protected Logger getLogger(); + + abstract protected boolean isSupportedEcAlgorithm(String algorithm); + + abstract protected boolean isValidKeyUse(KeyUse keyUse); + + @Override + public boolean createFallbackKeys(KeycloakSession session, KeyUse keyUse, String algorithm) { + if (isValidKeyUse(keyUse) && isSupportedEcAlgorithm(algorithm)) { + RealmModel realm = session.getContext().getRealm(); + + ComponentModel generated = new ComponentModel(); + generated.setName("fallback-" + algorithm); + generated.setParentId(realm.getId()); + generated.setProviderId(getId()); + generated.setProviderType(KeyProvider.class.getName()); + + MultivaluedHashMap config = new MultivaluedHashMap<>(); + config.putSingle(Attributes.PRIORITY_KEY, "-100"); + config.putSingle(getEcEllipticCurveKey(), getEcEllipticCurveKey(algorithm)); + generated.setConfig(config); + + realm.addComponentModel(generated); + + return true; + } else { + return false; + } + } + + @Override + public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException { + super.validateConfiguration(session, realm, model); + + ConfigurationValidationHelper.check(model).checkList(getEcEllipticCurveProperty(), false); + + String ecInNistRep = model.get(getEcEllipticCurveKey()); + if (ecInNistRep == null) ecInNistRep = getDefaultEcEllipticCurve(); + + if (!(model.contains(getEcPrivateKeyKey()) && model.contains(getEcPublicKeyKey()))) { + generateKeys(model, ecInNistRep); + getLogger().debugv("Generated keys for {0}", realm.getName()); + } else { + String currentEc = getCurveFromPublicKey(model.getConfig().getFirst(getEcPublicKeyKey())); + if (!ecInNistRep.equals(currentEc)) { + generateKeys(model, ecInNistRep); + getLogger().debugv("Elliptic Curve changed, generating new keys for {0}", realm.getName()); + } + } + } + + protected void generateKeys(ComponentModel model, String ecInNistRep) { + KeyPair keyPair; + try { + keyPair = generateEcKeyPair(convertECDomainParmNistRepToSecRep(ecInNistRep)); + model.put(getEcPrivateKeyKey(), Base64.encodeBytes(keyPair.getPrivate().getEncoded())); + model.put(getEcPublicKeyKey(), Base64.encodeBytes(keyPair.getPublic().getEncoded())); + model.put(getEcEllipticCurveKey(), ecInNistRep); + } catch (Throwable t) { + throw new ComponentValidationException("Failed to generate EC keys", t); + } + } + + protected String getCurveFromPublicKey(String publicEcKeyBase64Encoded) { + try { + KeyFactory kf = KeyFactory.getInstance("EC"); + X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(Base64.decode(publicEcKeyBase64Encoded)); + ECPublicKey ecKey = (ECPublicKey) kf.generatePublic(publicKeySpec); + return "P-" + ecKey.getParams().getCurve().getField().getFieldSize(); + } catch (Throwable t) { + throw new ComponentValidationException("Failed to get EC from its public key", t); + } + } +} diff --git a/services/src/main/java/org/keycloak/keys/GeneratedEcdhKeyProvider.java b/services/src/main/java/org/keycloak/keys/GeneratedEcdhKeyProvider.java new file mode 100644 index 00000000000..fae1aa82212 --- /dev/null +++ b/services/src/main/java/org/keycloak/keys/GeneratedEcdhKeyProvider.java @@ -0,0 +1,64 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.keys; + +import org.jboss.logging.Logger; +import org.keycloak.common.util.Base64; +import org.keycloak.component.ComponentModel; +import org.keycloak.crypto.KeyUse; +import org.keycloak.crypto.KeyWrapper; +import org.keycloak.models.RealmModel; + +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +public class GeneratedEcdhKeyProvider extends AbstractEcKeyProvider { + private static final Logger logger = Logger.getLogger(GeneratedEcdhKeyProvider.class); + + public GeneratedEcdhKeyProvider(RealmModel realm, ComponentModel model) { + super(realm, model); + } + + @Override + protected KeyWrapper loadKey(RealmModel realm, ComponentModel model) { + String privateEcdhKeyBase64Encoded = model.getConfig().getFirst(GeneratedEcdhKeyProviderFactory.ECDH_PRIVATE_KEY_KEY); + String publicEcdhKeyBase64Encoded = model.getConfig().getFirst(GeneratedEcdhKeyProviderFactory.ECDH_PUBLIC_KEY_KEY); + String ecdhAlgorithm = model.getConfig().getFirst(GeneratedEcdhKeyProviderFactory.ECDH_ALGORITHM_KEY); + + try { + PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(Base64.decode(privateEcdhKeyBase64Encoded)); + KeyFactory kf = KeyFactory.getInstance("EC"); + PrivateKey decodedPrivateKey = kf.generatePrivate(privateKeySpec); + + X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(Base64.decode(publicEcdhKeyBase64Encoded)); + PublicKey decodedPublicKey = kf.generatePublic(publicKeySpec); + + KeyPair keyPair = new KeyPair(decodedPublicKey, decodedPrivateKey); + + return createKeyWrapper(keyPair, ecdhAlgorithm, KeyUse.ENC); + } catch (Exception e) { + logger.warnf("Exception at decodeEcdhPublicKey. %s", e.toString()); + return null; + } + + } + +} diff --git a/services/src/main/java/org/keycloak/keys/GeneratedEcdhKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/GeneratedEcdhKeyProviderFactory.java new file mode 100644 index 00000000000..967e7c987b3 --- /dev/null +++ b/services/src/main/java/org/keycloak/keys/GeneratedEcdhKeyProviderFactory.java @@ -0,0 +1,155 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.keys; + +import org.jboss.logging.Logger; +import org.keycloak.component.ComponentModel; +import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.KeyUse; +import org.keycloak.models.KeycloakSession; +import org.keycloak.provider.ProviderConfigProperty; + +import static org.keycloak.provider.ProviderConfigProperty.LIST_TYPE; + +import java.util.List; + +public class GeneratedEcdhKeyProviderFactory extends AbstractGeneratedEcKeyProviderFactory { + + // secp256r1,NIST P-256,X9.62 prime256v1,1.2.840.10045.3.1.7 + public static final String DEFAULT_ECDH_ELLIPTIC_CURVE = DEFAULT_EC_ELLIPTIC_CURVE; + + public static final String ECDH_ALGORITHM_KEY = "ecdhAlgorithm"; + + public static final String ECDH_ELLIPTIC_CURVE_KEY = "ecdhEllipticCurveKey"; + public static final String ECDH_PRIVATE_KEY_KEY = "ecdhPrivateKey"; + public static final String ECDH_PUBLIC_KEY_KEY = "ecdhPublicKey"; + + // only support NIST P-256 for ES256, P-384 for ES384, P-521 for ES512 + protected static ProviderConfigProperty ECDH_ELLIPTIC_CURVE_PROPERTY = new ProviderConfigProperty(ECDH_ELLIPTIC_CURVE_KEY, "Elliptic Curve", "Elliptic Curve used in ECDH", LIST_TYPE, + String.valueOf(GeneratedEcdhKeyProviderFactory.DEFAULT_ECDH_ELLIPTIC_CURVE), + "P-256", "P-384", "P-521"); + + protected static ProviderConfigProperty ECDH_ALGORITHM_PROPERTY = new ProviderConfigProperty(ECDH_ALGORITHM_KEY, + "Algorithm", "Algorithm for processing the Content Encryption Key", LIST_TYPE, Algorithm.ECDH_ES, + Algorithm.ECDH_ES, Algorithm.ECDH_ES_A128KW, Algorithm.ECDH_ES_A192KW, Algorithm.ECDH_ES_A256KW); + + private static final String HELP_TEXT = "Generates ECDH keys"; + + public static final String ID = "ecdh-generated"; + + private static final Logger logger = Logger.getLogger(GeneratedEcdhKeyProviderFactory.class); + + private static final List CONFIG_PROPERTIES = AbstractGeneratedEcKeyProviderFactory.configurationBuilder() + .property(ECDH_ELLIPTIC_CURVE_PROPERTY) + .property(ECDH_ALGORITHM_PROPERTY) + .build(); + + public static String convertECDomainParmNistRepToJWEAlgorithm(String ecInNistRep) { + switch(ecInNistRep) { + case "P-256" : + return Algorithm.ECDH_ES_A128KW; + case "P-384" : + return Algorithm.ECDH_ES_A192KW; + case "P-521" : + return Algorithm.ECDH_ES_A256KW; + default : + return null; + } + } + + public static String convertJWEAlgorithmToECDomainParmNistRep(String algorithm) { + switch(algorithm) { + case Algorithm.ECDH_ES_A128KW : + return "P-256"; + case Algorithm.ECDH_ES_A192KW : + return "P-384"; + case Algorithm.ECDH_ES_A256KW : + return "P-521"; + default : + return null; + } + } + + @Override + public KeyProvider create(KeycloakSession session, ComponentModel model) { + return new GeneratedEcdhKeyProvider(session.getContext().getRealm(), model); + } + + @Override + public List getConfigProperties() { + return CONFIG_PROPERTIES; + } + + @Override + protected String getDefaultEcEllipticCurve() { + return DEFAULT_ECDH_ELLIPTIC_CURVE; + } + + @Override + protected String getEcEllipticCurveKey() { + return ECDH_ELLIPTIC_CURVE_KEY; + } + + @Override + protected String getEcEllipticCurveKey(String algorithm) { + if (Algorithm.ECDH_ES.equals(algorithm)) { + return DEFAULT_ECDH_ELLIPTIC_CURVE; + } + return convertJWEAlgorithmToECDomainParmNistRep(algorithm); + } + + @Override + protected ProviderConfigProperty getEcEllipticCurveProperty() { + return ECDH_ELLIPTIC_CURVE_PROPERTY; + } + + @Override + protected String getEcPrivateKeyKey() { + return ECDH_PRIVATE_KEY_KEY; + } + + @Override + protected String getEcPublicKeyKey() { + return ECDH_PUBLIC_KEY_KEY; + } + + @Override + public String getHelpText() { + return HELP_TEXT; + } + + @Override + public String getId() { + return ID; + } + + @Override + protected Logger getLogger() { + return logger; + } + + @Override + protected boolean isSupportedEcAlgorithm(String algorithm) { + return (algorithm.equals(Algorithm.ECDH_ES) || algorithm.equals(Algorithm.ECDH_ES_A128KW) + || algorithm.equals(Algorithm.ECDH_ES_A192KW) || algorithm.equals(Algorithm.ECDH_ES_A256KW)); + } + + @Override + protected boolean isValidKeyUse(KeyUse keyUse) { + return KeyUse.ENC.equals(keyUse); + } +} diff --git a/services/src/main/java/org/keycloak/keys/GeneratedEcdsaKeyProvider.java b/services/src/main/java/org/keycloak/keys/GeneratedEcdsaKeyProvider.java index 94603215165..304281b326e 100644 --- a/services/src/main/java/org/keycloak/keys/GeneratedEcdsaKeyProvider.java +++ b/services/src/main/java/org/keycloak/keys/GeneratedEcdsaKeyProvider.java @@ -19,6 +19,7 @@ package org.keycloak.keys; import org.jboss.logging.Logger; import org.keycloak.common.util.Base64; import org.keycloak.component.ComponentModel; +import org.keycloak.crypto.KeyUse; import org.keycloak.crypto.KeyWrapper; import org.keycloak.models.RealmModel; @@ -29,15 +30,15 @@ import java.security.PublicKey; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; -public class GeneratedEcdsaKeyProvider extends AbstractEcdsaKeyProvider { +public class GeneratedEcdsaKeyProvider extends AbstractEcKeyProvider { private static final Logger logger = Logger.getLogger(GeneratedEcdsaKeyProvider.class); public GeneratedEcdsaKeyProvider(RealmModel realm, ComponentModel model) { super(realm, model); } - @Override - protected KeyWrapper loadKey(RealmModel realm, ComponentModel model) { + @Override + protected KeyWrapper loadKey(RealmModel realm, ComponentModel model) { String privateEcdsaKeyBase64Encoded = model.getConfig().getFirst(GeneratedEcdsaKeyProviderFactory.ECDSA_PRIVATE_KEY_KEY); String publicEcdsaKeyBase64Encoded = model.getConfig().getFirst(GeneratedEcdsaKeyProviderFactory.ECDSA_PUBLIC_KEY_KEY); String ecInNistRep = model.getConfig().getFirst(GeneratedEcdsaKeyProviderFactory.ECDSA_ELLIPTIC_CURVE_KEY); @@ -51,8 +52,8 @@ public class GeneratedEcdsaKeyProvider extends AbstractEcdsaKeyProvider { PublicKey decodedPublicKey = kf.generatePublic(publicKeySpec); KeyPair keyPair = new KeyPair(decodedPublicKey, decodedPrivateKey); - - return createKeyWrapper(keyPair, ecInNistRep); + return createKeyWrapper(keyPair, + GeneratedEcdsaKeyProviderFactory.convertECDomainParmNistRepToJWSAlgorithm(ecInNistRep), KeyUse.SIG); } catch (Exception e) { logger.warnf("Exception at decodeEcdsaPublicKey. %s", e.toString()); return null; diff --git a/services/src/main/java/org/keycloak/keys/GeneratedEcdsaKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/GeneratedEcdsaKeyProviderFactory.java index 5ab2ec6fa52..f1def90fa93 100644 --- a/services/src/main/java/org/keycloak/keys/GeneratedEcdsaKeyProviderFactory.java +++ b/services/src/main/java/org/keycloak/keys/GeneratedEcdsaKeyProviderFactory.java @@ -17,35 +17,37 @@ package org.keycloak.keys; import org.jboss.logging.Logger; -import org.keycloak.common.util.Base64; -import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.component.ComponentModel; -import org.keycloak.component.ComponentValidationException; import org.keycloak.crypto.Algorithm; import org.keycloak.crypto.KeyUse; import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.provider.ConfigurationValidationHelper; import org.keycloak.provider.ProviderConfigProperty; -import java.security.KeyFactory; -import java.security.KeyPair; -import java.security.interfaces.ECPublicKey; -import java.security.spec.X509EncodedKeySpec; +import static org.keycloak.provider.ProviderConfigProperty.LIST_TYPE; + import java.util.List; -public class GeneratedEcdsaKeyProviderFactory extends AbstractEcdsaKeyProviderFactory { +public class GeneratedEcdsaKeyProviderFactory extends AbstractGeneratedEcKeyProviderFactory { private static final Logger logger = Logger.getLogger(GeneratedEcdsaKeyProviderFactory.class); + public static final String ECDSA_PRIVATE_KEY_KEY = "ecdsaPrivateKey"; + public static final String ECDSA_PUBLIC_KEY_KEY = "ecdsaPublicKey"; + public static final String ECDSA_ELLIPTIC_CURVE_KEY = "ecdsaEllipticCurveKey"; + + // only support NIST P-256 for ES256, P-384 for ES384, P-521 for ES512 + protected static ProviderConfigProperty ECDSA_ELLIPTIC_CURVE_PROPERTY = new ProviderConfigProperty(ECDSA_ELLIPTIC_CURVE_KEY, "Elliptic Curve", "Elliptic Curve used in ECDSA", LIST_TYPE, + String.valueOf(GeneratedEcdsaKeyProviderFactory.DEFAULT_ECDSA_ELLIPTIC_CURVE), + "P-256", "P-384", "P-521"); + public static final String ID = "ecdsa-generated"; private static final String HELP_TEXT = "Generates ECDSA keys"; // secp256r1,NIST P-256,X9.62 prime256v1,1.2.840.10045.3.1.7 - public static final String DEFAULT_ECDSA_ELLIPTIC_CURVE = "P-256"; + public static final String DEFAULT_ECDSA_ELLIPTIC_CURVE = DEFAULT_EC_ELLIPTIC_CURVE; - private static final List CONFIG_PROPERTIES = AbstractEcdsaKeyProviderFactory.configurationBuilder() + private static final List CONFIG_PROPERTIES = AbstractGeneratedEcKeyProviderFactory.configurationBuilder() .property(ECDSA_ELLIPTIC_CURVE_PROPERTY) .build(); @@ -54,30 +56,6 @@ public class GeneratedEcdsaKeyProviderFactory extends AbstractEcdsaKeyProviderFa return new GeneratedEcdsaKeyProvider(session.getContext().getRealm(), model); } - @Override - public boolean createFallbackKeys(KeycloakSession session, KeyUse keyUse, String algorithm) { - if (keyUse.equals(KeyUse.SIG) && (algorithm.equals(Algorithm.ES256) || algorithm.equals(Algorithm.ES384) || algorithm.equals(Algorithm.ES512))) { - RealmModel realm = session.getContext().getRealm(); - - ComponentModel generated = new ComponentModel(); - generated.setName("fallback-" + algorithm); - generated.setParentId(realm.getId()); - generated.setProviderId(ID); - generated.setProviderType(KeyProvider.class.getName()); - - MultivaluedHashMap config = new MultivaluedHashMap<>(); - config.putSingle(Attributes.PRIORITY_KEY, "-100"); - config.putSingle(ECDSA_ELLIPTIC_CURVE_KEY, convertAlgorithmToECDomainParmNistRep(algorithm)); - generated.setConfig(config); - - realm.addComponentModel(generated); - - return true; - } else { - return false; - } - } - @Override public String getHelpText() { return HELP_TEXT; @@ -94,46 +72,74 @@ public class GeneratedEcdsaKeyProviderFactory extends AbstractEcdsaKeyProviderFa } @Override - public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException { - super.validateConfiguration(session, realm, model); + protected Logger getLogger() { + return logger; + } - ConfigurationValidationHelper.check(model).checkList(ECDSA_ELLIPTIC_CURVE_PROPERTY, false); + @Override + protected boolean isValidKeyUse(KeyUse keyUse) { + return KeyUse.SIG.equals(keyUse); + } - String ecInNistRep = model.get(ECDSA_ELLIPTIC_CURVE_KEY); - if (ecInNistRep == null) ecInNistRep = DEFAULT_ECDSA_ELLIPTIC_CURVE; + @Override + protected boolean isSupportedEcAlgorithm(String algorithm) { + return (algorithm.equals(Algorithm.ES256) || algorithm.equals(Algorithm.ES384) + || algorithm.equals(Algorithm.ES512)); + } - if (!(model.contains(ECDSA_PRIVATE_KEY_KEY) && model.contains(ECDSA_PUBLIC_KEY_KEY))) { - generateKeys(model, ecInNistRep); - logger.debugv("Generated keys for {0}", realm.getName()); - } else { - String currentEc = getCurveFromPublicKey(model.getConfig().getFirst(GeneratedEcdsaKeyProviderFactory.ECDSA_PUBLIC_KEY_KEY)); - if (!ecInNistRep.equals(currentEc)) { - generateKeys(model, ecInNistRep); - logger.debugv("Elliptic Curve changed, generating new keys for {0}", realm.getName()); - } + @Override + protected String getEcEllipticCurveKey(String algorithm) { + return convertJWSAlgorithmToECDomainParmNistRep(algorithm); + } + + @Override + protected ProviderConfigProperty getEcEllipticCurveProperty() { + return ECDSA_ELLIPTIC_CURVE_PROPERTY; + } + + @Override + protected String getEcEllipticCurveKey() { + return ECDSA_ELLIPTIC_CURVE_KEY; + } + + @Override + protected String getEcPrivateKeyKey() { + return ECDSA_PRIVATE_KEY_KEY; + } + + @Override + protected String getEcPublicKeyKey() { + return ECDSA_PUBLIC_KEY_KEY; + } + + @Override + protected String getDefaultEcEllipticCurve() { + return DEFAULT_ECDSA_ELLIPTIC_CURVE; + } + + public static String convertECDomainParmNistRepToJWSAlgorithm(String ecInNistRep) { + switch(ecInNistRep) { + case "P-256" : + return Algorithm.ES256; + case "P-384" : + return Algorithm.ES384; + case "P-521" : + return Algorithm.ES512; + default : + return null; } } - private void generateKeys(ComponentModel model, String ecInNistRep) { - KeyPair keyPair; - try { - keyPair = generateEcdsaKeyPair(convertECDomainParmNistRepToSecRep(ecInNistRep)); - model.put(ECDSA_PRIVATE_KEY_KEY, Base64.encodeBytes(keyPair.getPrivate().getEncoded())); - model.put(ECDSA_PUBLIC_KEY_KEY, Base64.encodeBytes(keyPair.getPublic().getEncoded())); - model.put(ECDSA_ELLIPTIC_CURVE_KEY, ecInNistRep); - } catch (Throwable t) { - throw new ComponentValidationException("Failed to generate ECDSA keys", t); - } - } - - private String getCurveFromPublicKey(String publicEcdsaKeyBase64Encoded) { - try { - KeyFactory kf = KeyFactory.getInstance("EC"); - X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(Base64.decode(publicEcdsaKeyBase64Encoded)); - ECPublicKey ecKey = (ECPublicKey) kf.generatePublic(publicKeySpec); - return "P-" + ecKey.getParams().getCurve().getField().getFieldSize(); - } catch (Throwable t) { - throw new ComponentValidationException("Failed to get EC from its public key", t); + public static String convertJWSAlgorithmToECDomainParmNistRep(String algorithm) { + switch(algorithm) { + case Algorithm.ES256 : + return "P-256"; + case Algorithm.ES384 : + return "P-384"; + case Algorithm.ES512 : + return "P-521"; + default : + return null; } } } diff --git a/services/src/main/resources/META-INF/services/org.keycloak.crypto.CekManagementProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.crypto.CekManagementProviderFactory index 191513341dc..57575695f78 100644 --- a/services/src/main/resources/META-INF/services/org.keycloak.crypto.CekManagementProviderFactory +++ b/services/src/main/resources/META-INF/services/org.keycloak.crypto.CekManagementProviderFactory @@ -1,3 +1,7 @@ +org.keycloak.crypto.EcdhEsCekManagementProviderFactory +org.keycloak.crypto.EcdhEsA128KwCekManagementProviderFactory +org.keycloak.crypto.EcdhEsA192KwCekManagementProviderFactory +org.keycloak.crypto.EcdhEsA256KwCekManagementProviderFactory org.keycloak.crypto.RsaesPkcs1CekManagementProviderFactory org.keycloak.crypto.RsaesOaepCekManagementProviderFactory org.keycloak.crypto.RsaesOaep256CekManagementProviderFactory \ No newline at end of file diff --git a/services/src/main/resources/META-INF/services/org.keycloak.keys.KeyProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.keys.KeyProviderFactory index 0efff00b868..e0becc2b8a4 100644 --- a/services/src/main/resources/META-INF/services/org.keycloak.keys.KeyProviderFactory +++ b/services/src/main/resources/META-INF/services/org.keycloak.keys.KeyProviderFactory @@ -20,6 +20,7 @@ org.keycloak.keys.GeneratedAesKeyProviderFactory org.keycloak.keys.GeneratedRsaKeyProviderFactory org.keycloak.keys.JavaKeystoreKeyProviderFactory org.keycloak.keys.ImportedRsaKeyProviderFactory +org.keycloak.keys.GeneratedEcdhKeyProviderFactory org.keycloak.keys.GeneratedEcdsaKeyProviderFactory org.keycloak.keys.GeneratedRsaEncKeyProviderFactory org.keycloak.keys.ImportedRsaEncKeyProviderFactory 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 143c0b490bc..b788904a649 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 @@ -9,7 +9,9 @@ import org.keycloak.crypto.JavaAlgorithm; import org.keycloak.crypto.KeyStatus; import org.keycloak.crypto.KeyType; import org.keycloak.crypto.KeyUse; -import org.keycloak.keys.AbstractEcdsaKeyProviderFactory; +import org.keycloak.keys.AbstractEcKeyProviderFactory; +import org.keycloak.keys.GeneratedEcdhKeyProviderFactory; +import org.keycloak.keys.GeneratedEcdsaKeyProviderFactory; import org.keycloak.keys.KeyProvider; import org.keycloak.representations.idm.ComponentRepresentation; import org.keycloak.representations.idm.KeysMetadataRepresentation; @@ -44,8 +46,13 @@ public class KeyUtils { try { KeyPairGenerator kpg = CryptoIntegration.getProvider().getKeyPairGen("ECDSA"); - String domainParamNistRep = AbstractEcdsaKeyProviderFactory.convertAlgorithmToECDomainParmNistRep(algorithm); - String curve = AbstractEcdsaKeyProviderFactory.convertECDomainParmNistRepToSecRep(domainParamNistRep); + String domainParamNistRep = GeneratedEcdsaKeyProviderFactory + .convertJWSAlgorithmToECDomainParmNistRep(algorithm); + if (domainParamNistRep == null) { + domainParamNistRep = GeneratedEcdhKeyProviderFactory + .convertJWEAlgorithmToECDomainParmNistRep(algorithm); + } + String curve = AbstractEcKeyProviderFactory.convertECDomainParmNistRepToSecRep(domainParamNistRep); ECGenParameterSpec parameterSpec = new ECGenParameterSpec(curve); kpg.initialize(parameterSpec); return kpg.generateKeyPair(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractKcOidcBrokerJWEEcdhEsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractKcOidcBrokerJWEEcdhEsTest.java new file mode 100644 index 00000000000..b55516011d1 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractKcOidcBrokerJWEEcdhEsTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.broker; + +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.models.utils.DefaultKeyProviders; +import org.keycloak.representations.idm.ComponentExportRepresentation; + +public abstract class AbstractKcOidcBrokerJWEEcdhEsTest extends KcOidcBrokerJWETest { + + private final String crv; + private final String encAlg; + + public AbstractKcOidcBrokerJWEEcdhEsTest(String crv, String encAlg, String encEnc, String sigAlg) { + super(encAlg, encEnc, sigAlg); + this.crv = crv; + this.encAlg = encAlg; + } + + protected ComponentExportRepresentation getProviderKeyProvider() { + // create the ECDSA component for the signature in the specified alg + ComponentExportRepresentation component = new ComponentExportRepresentation(); + component.setName("ecdsa-generated"); + component.setProviderId("ecdsa-generated"); + + MultivaluedHashMap config = new MultivaluedHashMap<>(); + config.putSingle("priority", DefaultKeyProviders.DEFAULT_PRIORITY); + config.putSingle("ecdsaEllipticCurveKey", this.crv); + component.setConfig(config); + + return component; + } + + protected ComponentExportRepresentation getConsumerKeyProvider() { + // create the ECDH component for the encryption in the specified alg + ComponentExportRepresentation component = new ComponentExportRepresentation(); + component.setName("ecdh-generated"); + component.setProviderId("ecdh-generated"); + + MultivaluedHashMap config = new MultivaluedHashMap<>(); + config.putSingle("priority", DefaultKeyProviders.DEFAULT_PRIORITY); + config.putSingle("ecdhAlgorithm", this.encAlg); + config.putSingle("ecdhEllipticCurveKey", this.crv); + component.setConfig(config); + + return component; + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsA128KwP256A128CbcHs256Test.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsA128KwP256A128CbcHs256Test.java new file mode 100644 index 00000000000..1d514f9624c --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsA128KwP256A128CbcHs256Test.java @@ -0,0 +1,26 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.broker; + +import org.keycloak.crypto.Algorithm; +import org.keycloak.jose.jwe.JWEConstants; + +public class KcOidcBrokerJWEEcdhEsA128KwP256A128CbcHs256Test extends AbstractKcOidcBrokerJWEEcdhEsTest { + public KcOidcBrokerJWEEcdhEsA128KwP256A128CbcHs256Test() { + super("P-256", JWEConstants.ECDH_ES_A128KW, JWEConstants.A128CBC_HS256, Algorithm.ES256); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsA128KwP256A128GcmTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsA128KwP256A128GcmTest.java new file mode 100644 index 00000000000..cc6229cacee --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsA128KwP256A128GcmTest.java @@ -0,0 +1,26 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.broker; + +import org.keycloak.crypto.Algorithm; +import org.keycloak.jose.jwe.JWEConstants; + +public class KcOidcBrokerJWEEcdhEsA128KwP256A128GcmTest extends AbstractKcOidcBrokerJWEEcdhEsTest { + public KcOidcBrokerJWEEcdhEsA128KwP256A128GcmTest() { + super("P-256", JWEConstants.ECDH_ES_A128KW, JWEConstants.A128GCM, Algorithm.ES256); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsA192KwP384A192CbcHs384Test.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsA192KwP384A192CbcHs384Test.java new file mode 100644 index 00000000000..ba8f2f5a893 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsA192KwP384A192CbcHs384Test.java @@ -0,0 +1,26 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.broker; + +import org.keycloak.crypto.Algorithm; +import org.keycloak.jose.jwe.JWEConstants; + +public class KcOidcBrokerJWEEcdhEsA192KwP384A192CbcHs384Test extends AbstractKcOidcBrokerJWEEcdhEsTest { + public KcOidcBrokerJWEEcdhEsA192KwP384A192CbcHs384Test() { + super("P-384", JWEConstants.ECDH_ES_A192KW, JWEConstants.A192CBC_HS384, Algorithm.ES384); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsA192KwP384A192GcmTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsA192KwP384A192GcmTest.java new file mode 100644 index 00000000000..eac7af5a10d --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsA192KwP384A192GcmTest.java @@ -0,0 +1,26 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.broker; + +import org.keycloak.crypto.Algorithm; +import org.keycloak.jose.jwe.JWEConstants; + +public class KcOidcBrokerJWEEcdhEsA192KwP384A192GcmTest extends AbstractKcOidcBrokerJWEEcdhEsTest { + public KcOidcBrokerJWEEcdhEsA192KwP384A192GcmTest() { + super("P-384", JWEConstants.ECDH_ES_A192KW, JWEConstants.A192GCM, Algorithm.ES384); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsA256KwP521A256CbcHs512Test.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsA256KwP521A256CbcHs512Test.java new file mode 100644 index 00000000000..4163027ed10 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsA256KwP521A256CbcHs512Test.java @@ -0,0 +1,26 @@ +/* +KcOidcBrokerJWEEcdhEsA256KwP521A256GcmTest.java * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.broker; + +import org.keycloak.crypto.Algorithm; +import org.keycloak.jose.jwe.JWEConstants; + +public class KcOidcBrokerJWEEcdhEsA256KwP521A256CbcHs512Test extends AbstractKcOidcBrokerJWEEcdhEsTest { + public KcOidcBrokerJWEEcdhEsA256KwP521A256CbcHs512Test() { + super("P-521", JWEConstants.ECDH_ES_A256KW, JWEConstants.A256CBC_HS512, Algorithm.ES512); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsA256KwP521A256GcmTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsA256KwP521A256GcmTest.java new file mode 100644 index 00000000000..eb1dcd6616a --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsA256KwP521A256GcmTest.java @@ -0,0 +1,26 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.broker; + +import org.keycloak.crypto.Algorithm; +import org.keycloak.jose.jwe.JWEConstants; + +public class KcOidcBrokerJWEEcdhEsA256KwP521A256GcmTest extends AbstractKcOidcBrokerJWEEcdhEsTest { + public KcOidcBrokerJWEEcdhEsA256KwP521A256GcmTest() { + super("P-521", JWEConstants.ECDH_ES_A256KW, JWEConstants.A256GCM, Algorithm.ES512); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsP384A192CbcHs384Test.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsP384A192CbcHs384Test.java new file mode 100644 index 00000000000..bd753385522 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsP384A192CbcHs384Test.java @@ -0,0 +1,26 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.broker; + +import org.keycloak.crypto.Algorithm; +import org.keycloak.jose.jwe.JWEConstants; + +public class KcOidcBrokerJWEEcdhEsP384A192CbcHs384Test extends AbstractKcOidcBrokerJWEEcdhEsTest { + public KcOidcBrokerJWEEcdhEsP384A192CbcHs384Test() { + super("P-384", JWEConstants.ECDH_ES, JWEConstants.A192CBC_HS384, Algorithm.ES384); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsP384A192GcmTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsP384A192GcmTest.java new file mode 100644 index 00000000000..3721b0345d7 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWEEcdhEsP384A192GcmTest.java @@ -0,0 +1,26 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.broker; + +import org.keycloak.crypto.Algorithm; +import org.keycloak.jose.jwe.JWEConstants; + +public class KcOidcBrokerJWEEcdhEsP384A192GcmTest extends AbstractKcOidcBrokerJWEEcdhEsTest { + public KcOidcBrokerJWEEcdhEsP384A192GcmTest() { + super("P-384", JWEConstants.ECDH_ES, JWEConstants.A192GCM, Algorithm.ES384); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWETest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWETest.java index 1e8c673e8ea..13cf24c5f98 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWETest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerJWETest.java @@ -78,6 +78,24 @@ public class KcOidcBrokerJWETest extends AbstractBrokerTest { this.sigAlg = sigAlg; } + protected ComponentExportRepresentation getConsumerKeyProvider() { + // create the RSA component for the encryption in the specified alg + ComponentExportRepresentation component = new ComponentExportRepresentation(); + component.setName("rsa-enc-generated"); + component.setProviderId("rsa-enc-generated"); + + MultivaluedHashMap config = new MultivaluedHashMap<>(); + config.putSingle("priority", DefaultKeyProviders.DEFAULT_PRIORITY); + config.putSingle("keyUse", KeyUse.ENC.name()); + config.putSingle("algorithm", encAlg); + component.setConfig(config); + return component; + } + + protected ComponentExportRepresentation getProviderKeyProvider() { + return null; + } + @Override protected BrokerConfiguration getBrokerConfiguration() { return new KcOidcBrokerConfiguration() { @@ -116,23 +134,34 @@ public class KcOidcBrokerJWETest extends AbstractBrokerTest { RealmRepresentation realm = super.createConsumerRealm(); if (encAlg != null) { - // create the RSA component for the encryption in the specified alg - ComponentExportRepresentation component = new ComponentExportRepresentation(); - component.setName("rsa-enc-generated"); - component.setProviderId("rsa-enc-generated"); - - MultivaluedHashMap config = new MultivaluedHashMap<>(); - config.putSingle("priority", DefaultKeyProviders.DEFAULT_PRIORITY); - config.putSingle("keyUse", KeyUse.ENC.name()); - config.putSingle("algorithm", encAlg); - component.setConfig(config); - - MultivaluedHashMap components = realm.getComponents(); - if (components == null) { - components = new MultivaluedHashMap<>(); - realm.setComponents(components); + ComponentExportRepresentation component = getConsumerKeyProvider(); + if (component != null) { + MultivaluedHashMap components = realm.getComponents(); + if (components == null) { + components = new MultivaluedHashMap<>(); + realm.setComponents(components); + } + components.add(KeyProvider.class.getName(), component); + } + } + + return realm; + } + + @Override + public RealmRepresentation createProviderRealm() { + RealmRepresentation realm = super.createProviderRealm(); + + if (sigAlg != null) { + ComponentExportRepresentation component = getProviderKeyProvider(); + if (component != null) { + MultivaluedHashMap components = realm.getComponents(); + if (components == null) { + components = new MultivaluedHashMap<>(); + realm.setComponents(components); + } + components.add(KeyProvider.class.getName(), component); } - components.add(KeyProvider.class.getName(), component); } return realm; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/GeneratedEcdhKeyProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/GeneratedEcdhKeyProviderTest.java new file mode 100644 index 00000000000..5c16a20fecf --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/GeneratedEcdhKeyProviderTest.java @@ -0,0 +1,337 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.keys; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson; + +import java.security.KeyFactory; +import java.security.interfaces.ECPublicKey; +import java.security.spec.X509EncodedKeySpec; +import java.util.List; + +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response; + +import org.jboss.arquillian.graphene.page.Page; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.common.util.Base64; +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.KeyType; +import org.keycloak.crypto.KeyUse; +import org.keycloak.keys.Attributes; +import org.keycloak.keys.GeneratedEcdhKeyProviderFactory; +import org.keycloak.keys.KeyProvider; +import org.keycloak.representations.idm.ComponentRepresentation; +import org.keycloak.representations.idm.KeysMetadataRepresentation; +import org.keycloak.representations.idm.KeysMetadataRepresentation.KeyMetadataRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.AbstractKeycloakTest; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.pages.AppPage; +import org.keycloak.testsuite.pages.LoginPage; + +public class GeneratedEcdhKeyProviderTest extends AbstractKeycloakTest { + private static final String DEFAULT_EC = GeneratedEcdhKeyProviderFactory.DEFAULT_ECDH_ELLIPTIC_CURVE; + private static final String ECDH_ELLIPTIC_CURVE_KEY = GeneratedEcdhKeyProviderFactory.ECDH_ELLIPTIC_CURVE_KEY; + private static final String ECDH_ALGORITHM_KEY = GeneratedEcdhKeyProviderFactory.ECDH_ALGORITHM_KEY; + private static final String TEST_REALM_NAME = "test"; + + @Rule + public AssertEvents events = new AssertEvents(this); + + @Page + protected AppPage appPage; + + @Page + protected LoginPage loginPage; + + @Override + public void addTestRealms(List testRealms) { + RealmRepresentation realm = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class); + testRealms.add(realm); + } + + @Test + public void defaultEcDirect() { + supportedEc(null, Algorithm.ECDH_ES); + } + + @Test + public void supportedEcP521Direct() { + supportedEc("P-521", Algorithm.ECDH_ES); + } + + @Test + public void supportedEcP384Direct() { + supportedEc("P-384", Algorithm.ECDH_ES); + } + + @Test + public void supportedEcP256Direct() { + supportedEc("P-256", Algorithm.ECDH_ES); + } + + @Test + public void unsupportedEcK163Direct() { + // NIST.FIPS.186-4 Koblitz Curve over Binary Field + unsupportedEc("K-163", Algorithm.ECDH_ES); + } + + @Test + public void defaultEcKeyWrap128() { + supportedEc(null, Algorithm.ECDH_ES_A128KW); + } + + @Test + public void defaultEcKeyWrap192() { + supportedEc(null, Algorithm.ECDH_ES_A192KW); + } + + @Test + public void defaultEcKeyWrap256() { + supportedEc(null, Algorithm.ECDH_ES_A256KW); + } + + @Test + public void supportedEcP521KeyWrap128() { + supportedEc("P-521", Algorithm.ECDH_ES_A128KW); + } + + @Test + public void supportedEcP521KeyWrap192() { + supportedEc("P-521", Algorithm.ECDH_ES_A192KW); + } + + @Test + public void supportedEcP521KeyWrap256() { + supportedEc("P-521", Algorithm.ECDH_ES_A256KW); + } + + @Test + public void supportedEcP384KeyWrap128() { + supportedEc("P-384", Algorithm.ECDH_ES_A128KW); + } + + @Test + public void supportedEcP384KeyWrap192() { + supportedEc("P-384", Algorithm.ECDH_ES_A192KW); + } + + @Test + public void supportedEcP384KeyWrap256() { + supportedEc("P-384", Algorithm.ECDH_ES_A256KW); + } + + @Test + public void supportedEcP256KeyWrap128() { + supportedEc("P-256", Algorithm.ECDH_ES_A128KW); + } + + @Test + public void supportedEcP256KeyWrap192() { + supportedEc("P-256", Algorithm.ECDH_ES_A192KW); + } + + @Test + public void supportedEcP256KeyWrap256() { + supportedEc("P-256", Algorithm.ECDH_ES_A256KW); + } + + @Test + public void unsupportedEcK163KeyWrap128() { + // NIST.FIPS.186-4 Koblitz Curve over Binary Field + unsupportedEc("K-163", Algorithm.ECDH_ES_A128KW); + } + + @Test + public void unsupportedEcK163KeyWrap192() { + // NIST.FIPS.186-4 Koblitz Curve over Binary Field + unsupportedEc("K-163", Algorithm.ECDH_ES_A192KW); + } + + @Test + public void unsupportedEcK163KeyWrap256() { + // NIST.FIPS.186-4 Koblitz Curve over Binary Field + unsupportedEc("K-163", Algorithm.ECDH_ES_A256KW); + } + + private String supportedEc(String ecInNistRep, String algorithm) { + long priority = System.currentTimeMillis(); + + ComponentRepresentation rep = createRep("valid", GeneratedEcdhKeyProviderFactory.ID); + rep.setConfig(new MultivaluedHashMap<>()); + rep.getConfig().putSingle(Attributes.PRIORITY_KEY, Long.toString(priority)); + if (ecInNistRep != null) { + rep.getConfig().putSingle(ECDH_ELLIPTIC_CURVE_KEY, ecInNistRep); + } else { + ecInNistRep = DEFAULT_EC; + } + rep.getConfig().putSingle(ECDH_ALGORITHM_KEY, algorithm); + + Response response = adminClient.realm(TEST_REALM_NAME).components().add(rep); + String id = ApiUtil.getCreatedId(response); + getCleanup().addComponentId(id); + response.close(); + + ComponentRepresentation createdRep = adminClient.realm(TEST_REALM_NAME).components().component(id).toRepresentation(); + + // stands for the number of properties in the key provider config + assertEquals(3, createdRep.getConfig().size()); + assertEquals(Long.toString(priority), createdRep.getConfig().getFirst(Attributes.PRIORITY_KEY)); + assertEquals(ecInNistRep, createdRep.getConfig().getFirst(ECDH_ELLIPTIC_CURVE_KEY)); + assertEquals(algorithm, createdRep.getConfig().getFirst(ECDH_ALGORITHM_KEY)); + + KeysMetadataRepresentation keys = adminClient.realm(TEST_REALM_NAME).keys().getKeyMetadata(); + + KeysMetadataRepresentation.KeyMetadataRepresentation key = null; + + for (KeyMetadataRepresentation k : keys.getKeys()) { + if (KeyType.EC.equals(k.getType()) && id.equals(k.getProviderId())) { + key = k; + break; + } + } + assertNotNull(key); + + assertEquals(id, key.getProviderId()); + assertEquals(KeyType.EC, key.getType()); + assertEquals(KeyUse.ENC, key.getUse()); + assertEquals(priority, key.getProviderPriority()); + + return id; // created key's component id + } + + private void unsupportedEc(String ecInNistRep, String algorithmMode) { + long priority = System.currentTimeMillis(); + + ComponentRepresentation rep = createRep("valid", GeneratedEcdhKeyProviderFactory.ID); + rep.setConfig(new MultivaluedHashMap<>()); + rep.getConfig().putSingle(Attributes.PRIORITY_KEY, Long.toString(priority)); + rep.getConfig().putSingle(ECDH_ELLIPTIC_CURVE_KEY, ecInNistRep); + rep.getConfig().putSingle(ECDH_ALGORITHM_KEY, algorithmMode); + boolean isEcAccepted = true; + + Response response = null; + try { + response = adminClient.realm(TEST_REALM_NAME).components().add(rep); + String id = ApiUtil.getCreatedId(response); + getCleanup().addComponentId(id); + response.close(); + } catch (WebApplicationException e) { + isEcAccepted = false; + } finally { + response.close(); + } + assertEquals(isEcAccepted, false); + } + + @Test + public void changeCurveFromP256ToP384Direct() throws Exception { + changeCurve("P-256", "P-384", Algorithm.ECDH_ES, Algorithm.ECDH_ES); + } + + @Test + public void changeCurveFromP384ToP521Direct() throws Exception { + changeCurve("P-384", "P-521", Algorithm.ECDH_ES, Algorithm.ECDH_ES); + } + + @Test + public void changeCurveFromP521ToP256Direct() throws Exception { + changeCurve("P-521", "P-256", Algorithm.ECDH_ES, Algorithm.ECDH_ES); + } + + @Test + public void changeCurveFromP256ToP384KeyWrap() throws Exception { + changeCurve("P-256", "P-384", Algorithm.ECDH_ES_A128KW, Algorithm.ECDH_ES_A192KW); + } + + @Test + public void changeCurveFromP384ToP521KeyWrap() throws Exception { + changeCurve("P-384", "P-521", Algorithm.ECDH_ES_A192KW, Algorithm.ECDH_ES_A256KW); + } + + @Test + public void changeCurveFromP521ToP256KeyWrap() throws Exception { + changeCurve("P-521", "P-256", Algorithm.ECDH_ES_A256KW, Algorithm.ECDH_ES_A128KW); + } + + private void changeCurve(String fromEcInNistRep, String toEcInNistRep, String fromAlgorithm, String toAlgorithm) + throws Exception { + String keyComponentId = supportedEc(fromEcInNistRep, fromAlgorithm); + KeysMetadataRepresentation keys = adminClient.realm(TEST_REALM_NAME).keys().getKeyMetadata(); + KeysMetadataRepresentation.KeyMetadataRepresentation originalKey = null; + for (KeyMetadataRepresentation k : keys.getKeys()) { + if (KeyType.EC.equals(k.getType()) && keyComponentId.equals(k.getProviderId())) { + originalKey = k; + break; + } + } + + ComponentRepresentation createdRep = adminClient.realm(TEST_REALM_NAME).components().component(keyComponentId).toRepresentation(); + createdRep.getConfig().putSingle(ECDH_ELLIPTIC_CURVE_KEY, toEcInNistRep); + createdRep.getConfig().putSingle(ECDH_ALGORITHM_KEY, toAlgorithm); + adminClient.realm(TEST_REALM_NAME).components().component(keyComponentId).update(createdRep); + + createdRep = adminClient.realm(TEST_REALM_NAME).components().component(keyComponentId).toRepresentation(); + + // stands for the number of properties in the key provider config + assertEquals(3, createdRep.getConfig().size()); + assertEquals(toEcInNistRep, createdRep.getConfig().getFirst(ECDH_ELLIPTIC_CURVE_KEY)); + assertEquals(toAlgorithm, createdRep.getConfig().getFirst(ECDH_ALGORITHM_KEY)); + + keys = adminClient.realm(TEST_REALM_NAME).keys().getKeyMetadata(); + KeysMetadataRepresentation.KeyMetadataRepresentation key = null; + for (KeyMetadataRepresentation k : keys.getKeys()) { + if (KeyType.EC.equals(k.getType()) && keyComponentId.equals(k.getProviderId())) { + key = k; + break; + } + } + assertNotNull(key); + + assertEquals(keyComponentId, key.getProviderId()); + assertNotEquals(originalKey.getKid(), key.getKid()); // kid is changed if key was regenerated + assertEquals(KeyType.EC, key.getType()); + assertEquals(KeyUse.ENC, key.getUse()); + assertEquals(originalKey.getAlgorithm(), fromAlgorithm); + assertEquals(key.getAlgorithm(), toAlgorithm); + assertEquals(toEcInNistRep, getCurveFromPublicKey(key.getPublicKey())); + } + + protected ComponentRepresentation createRep(String name, String providerId) { + ComponentRepresentation rep = new ComponentRepresentation(); + rep.setName(name); + rep.setParentId(adminClient.realm(TEST_REALM_NAME).toRepresentation().getId()); + rep.setProviderId(providerId); + rep.setProviderType(KeyProvider.class.getName()); + rep.setConfig(new MultivaluedHashMap<>()); + return rep; + } + + private String getCurveFromPublicKey(String publicEcKeyBase64Encoded) throws Exception { + KeyFactory kf = KeyFactory.getInstance("EC"); + X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(Base64.decode(publicEcKeyBase64Encoded)); + ECPublicKey ecKey = (ECPublicKey) kf.generatePublic(publicKeySpec); + return "P-" + ecKey.getParams().getCurve().getField().getFieldSize(); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/GeneratedEcdsaKeyProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/GeneratedEcdsaKeyProviderTest.java index 761e1e8f74e..653696770fd 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/GeneratedEcdsaKeyProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/GeneratedEcdsaKeyProviderTest.java @@ -35,7 +35,7 @@ import org.junit.Test; import org.keycloak.common.util.Base64; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.crypto.KeyType; -import org.keycloak.keys.AbstractEcdsaKeyProviderFactory; +import org.keycloak.keys.Attributes; import org.keycloak.keys.GeneratedEcdsaKeyProviderFactory; import org.keycloak.keys.KeyProvider; import org.keycloak.representations.idm.ComponentRepresentation; @@ -49,8 +49,8 @@ import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.LoginPage; public class GeneratedEcdsaKeyProviderTest extends AbstractKeycloakTest { - private static final String DEFAULT_EC = "P-256"; - private static final String ECDSA_ELLIPTIC_CURVE_KEY = "ecdsaEllipticCurveKey"; + private static final String DEFAULT_EC = GeneratedEcdsaKeyProviderFactory.DEFAULT_ECDSA_ELLIPTIC_CURVE; + private static final String ECDSA_ELLIPTIC_CURVE_KEY = GeneratedEcdsaKeyProviderFactory.ECDSA_ELLIPTIC_CURVE_KEY; private static final String TEST_REALM_NAME = "test"; @Rule @@ -99,7 +99,7 @@ public class GeneratedEcdsaKeyProviderTest extends AbstractKeycloakTest { ComponentRepresentation rep = createRep("valid", GeneratedEcdsaKeyProviderFactory.ID); rep.setConfig(new MultivaluedHashMap<>()); - rep.getConfig().putSingle("priority", Long.toString(priority)); + rep.getConfig().putSingle(Attributes.PRIORITY_KEY, Long.toString(priority)); if (ecInNistRep != null) { rep.getConfig().putSingle(ECDSA_ELLIPTIC_CURVE_KEY, ecInNistRep); } else { @@ -115,7 +115,7 @@ public class GeneratedEcdsaKeyProviderTest extends AbstractKeycloakTest { // stands for the number of properties in the key provider config assertEquals(2, createdRep.getConfig().size()); - assertEquals(Long.toString(priority), createdRep.getConfig().getFirst("priority")); + assertEquals(Long.toString(priority), createdRep.getConfig().getFirst(Attributes.PRIORITY_KEY)); assertEquals(ecInNistRep, createdRep.getConfig().getFirst(ECDSA_ELLIPTIC_CURVE_KEY)); KeysMetadataRepresentation keys = adminClient.realm(TEST_REALM_NAME).keys().getKeyMetadata(); @@ -210,7 +210,7 @@ public class GeneratedEcdsaKeyProviderTest extends AbstractKeycloakTest { assertNotEquals(originalKey.getKid(), key.getKid()); // kid is changed if key was regenerated assertEquals(KeyType.EC, key.getType()); assertNotEquals(originalKey.getAlgorithm(), key.getAlgorithm()); - assertEquals(ToEcInNistRep, AbstractEcdsaKeyProviderFactory.convertAlgorithmToECDomainParmNistRep(key.getAlgorithm())); + assertEquals(ToEcInNistRep, GeneratedEcdsaKeyProviderFactory.convertJWSAlgorithmToECDomainParmNistRep(key.getAlgorithm())); assertEquals(ToEcInNistRep, getCurveFromPublicKey(key.getPublicKey())); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/AbstractWellKnownProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/AbstractWellKnownProviderTest.java index 6ea0d26099f..2e117c2ccb4 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/AbstractWellKnownProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/AbstractWellKnownProviderTest.java @@ -56,7 +56,6 @@ import org.keycloak.testsuite.util.AdminClientUtil; import org.keycloak.testsuite.util.ClientManager; import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.TokenSignatureUtil; -import org.keycloak.testsuite.wellknown.CustomOIDCWellKnownProviderFactory; import org.keycloak.util.JsonSerialization; import java.io.IOException; @@ -144,15 +143,15 @@ public abstract class AbstractWellKnownProviderTest extends AbstractKeycloakTest Assert.assertNames(oidcConfig.getAuthorizationSigningAlgValuesSupported(), Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512, Algorithm.EdDSA); // request object encryption algorithms - Assert.assertNames(oidcConfig.getRequestObjectEncryptionAlgValuesSupported(), JWEConstants.RSA_OAEP, JWEConstants.RSA_OAEP_256, JWEConstants.RSA1_5); + Assert.assertNames(oidcConfig.getRequestObjectEncryptionAlgValuesSupported(), JWEConstants.ECDH_ES, JWEConstants.ECDH_ES_A128KW, JWEConstants.ECDH_ES_A192KW, JWEConstants.ECDH_ES_A256KW, JWEConstants.RSA_OAEP, JWEConstants.RSA_OAEP_256, JWEConstants.RSA1_5); Assert.assertNames(oidcConfig.getRequestObjectEncryptionEncValuesSupported(), JWEConstants.A256GCM, JWEConstants.A192GCM, JWEConstants.A128GCM, JWEConstants.A128CBC_HS256, JWEConstants.A192CBC_HS384, JWEConstants.A256CBC_HS512); // Encryption algorithms - Assert.assertNames(oidcConfig.getIdTokenEncryptionAlgValuesSupported(), JWEConstants.RSA1_5, JWEConstants.RSA_OAEP, JWEConstants.RSA_OAEP_256); + Assert.assertNames(oidcConfig.getIdTokenEncryptionAlgValuesSupported(), JWEConstants.ECDH_ES, JWEConstants.ECDH_ES_A128KW, JWEConstants.ECDH_ES_A192KW, JWEConstants.ECDH_ES_A256KW, JWEConstants.RSA1_5, JWEConstants.RSA_OAEP, JWEConstants.RSA_OAEP_256); Assert.assertNames(oidcConfig.getIdTokenEncryptionEncValuesSupported(), JWEConstants.A128CBC_HS256, JWEConstants.A128GCM, JWEConstants.A192CBC_HS384, JWEConstants.A192GCM, JWEConstants.A256CBC_HS512, JWEConstants.A256GCM); - Assert.assertNames(oidcConfig.getAuthorizationEncryptionAlgValuesSupported(), JWEConstants.RSA1_5, JWEConstants.RSA_OAEP, JWEConstants.RSA_OAEP_256); + Assert.assertNames(oidcConfig.getAuthorizationEncryptionAlgValuesSupported(), JWEConstants.ECDH_ES, JWEConstants.ECDH_ES_A128KW, JWEConstants.ECDH_ES_A192KW, JWEConstants.ECDH_ES_A256KW, JWEConstants.RSA1_5, JWEConstants.RSA_OAEP, JWEConstants.RSA_OAEP_256); Assert.assertNames(oidcConfig.getAuthorizationEncryptionEncValuesSupported(), JWEConstants.A128CBC_HS256, JWEConstants.A128GCM, JWEConstants.A192CBC_HS384, JWEConstants.A192GCM, JWEConstants.A256CBC_HS512, JWEConstants.A256GCM); - Assert.assertNames(oidcConfig.getUserInfoEncryptionAlgValuesSupported(), JWEConstants.RSA1_5, JWEConstants.RSA_OAEP, JWEConstants.RSA_OAEP_256); + Assert.assertNames(oidcConfig.getUserInfoEncryptionAlgValuesSupported(), JWEConstants.ECDH_ES, JWEConstants.ECDH_ES_A128KW, JWEConstants.ECDH_ES_A192KW, JWEConstants.ECDH_ES_A256KW, JWEConstants.RSA1_5, JWEConstants.RSA_OAEP, JWEConstants.RSA_OAEP_256); Assert.assertNames(oidcConfig.getUserInfoEncryptionEncValuesSupported(), JWEConstants.A128CBC_HS256, JWEConstants.A128GCM, JWEConstants.A192CBC_HS384, JWEConstants.A192GCM, JWEConstants.A256CBC_HS512, JWEConstants.A256GCM); // Client authentication diff --git a/testsuite/integration-arquillian/tests/base/testsuites/fips-suite b/testsuite/integration-arquillian/tests/base/testsuites/fips-suite index a4b646c1530..f22d3f47167 100644 --- a/testsuite/integration-arquillian/tests/base/testsuites/fips-suite +++ b/testsuite/integration-arquillian/tests/base/testsuites/fips-suite @@ -17,6 +17,14 @@ KcAdmTest KcAdmCreateTest SAMLServletAdapterTest SamlSignatureTest +KcOidcBrokerJWEEcdhEsA128KwP256A128CbcHs256Test +KcOidcBrokerJWEEcdhEsA128KwP256A128GcmTest +KcOidcBrokerJWEEcdhEsA192KwP384A192CbcHs384Test +KcOidcBrokerJWEEcdhEsA192KwP384A192GcmTest +KcOidcBrokerJWEEcdhEsA256KwP521A256CbcHs512Test +KcOidcBrokerJWEEcdhEsA256KwP521A256GcmTest +KcOidcBrokerJWEEcdhEsP384A192CbcHs384Test +KcOidcBrokerJWEEcdhEsP384A192GcmTest KcOidcBrokerJWETest KcOidcBrokerJWEUserInfoJustEncryptedTest KcSamlBrokerTest