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 1c53e6186cd..db8460d62c3 100644 --- a/common/src/main/java/org/keycloak/common/crypto/CryptoConstants.java +++ b/common/src/main/java/org/keycloak/common/crypto/CryptoConstants.java @@ -24,4 +24,8 @@ public class CryptoConstants { /** Name of Java security provider used with fips BouncyCastle. Should be used in FIPS environment */ public static final String BCFIPS_PROVIDER_ID = "BCFIPS"; + public static final String EC_KEY_SECP256R1 = "secp256r1"; + public static final String EC_KEY_SECP384R1 = "secp384r1"; + public static final String EC_KEY_SECP521R1 = "secp521r1"; + } diff --git a/common/src/main/java/org/keycloak/common/util/KeyUtils.java b/common/src/main/java/org/keycloak/common/util/KeyUtils.java index 14d49b5f909..bb4b067b9a0 100644 --- a/common/src/main/java/org/keycloak/common/util/KeyUtils.java +++ b/common/src/main/java/org/keycloak/common/util/KeyUtils.java @@ -25,7 +25,9 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; +import java.security.SecureRandom; import java.security.interfaces.RSAPrivateCrtKey; +import java.security.spec.ECGenParameterSpec; import java.security.spec.RSAPublicKeySpec; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; @@ -72,6 +74,27 @@ public class KeyUtils { } } + public static KeyPair generateEddsaKeyPair(String curveName) { + try { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance(curveName); + return keyGen.generateKeyPair(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static KeyPair generateEcKeyPair(String keySpecName) { + try { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC"); + SecureRandom randomGen = new SecureRandom(); + ECGenParameterSpec ecSpec = new ECGenParameterSpec(keySpecName); + keyGen.initialize(ecSpec, randomGen); + return keyGen.generateKeyPair(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + public static String createKeyId(Key key) { try { return Base64Url.encode(MessageDigest.getInstance(DEFAULT_MESSAGE_DIGEST).digest(key.getEncoded())); diff --git a/core/src/main/java/org/keycloak/crypto/ECCurve.java b/core/src/main/java/org/keycloak/crypto/ECCurve.java deleted file mode 100644 index c17819f8be1..00000000000 --- a/core/src/main/java/org/keycloak/crypto/ECCurve.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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.crypto; - -public enum ECCurve { - P256, - P384, - P521; - - /** - * Convert standard EC curve names (and aliases) into this enum. - */ - public static ECCurve fromStdCrv(String crv) { - switch (crv) { - case "P-256": - case "secp256r1": - return P256; - case "P-384": - case "secp384r1": - return P384; - case "P-521": - case "secp521r1": - return P521; - default: - throw new IllegalArgumentException("Unexpected EC curve: " + crv); - } - } -} diff --git a/core/src/main/java/org/keycloak/crypto/KeyWrapper.java b/core/src/main/java/org/keycloak/crypto/KeyWrapper.java index 7e7de05cd68..0eb03a8ee3e 100644 --- a/core/src/main/java/org/keycloak/crypto/KeyWrapper.java +++ b/core/src/main/java/org/keycloak/crypto/KeyWrapper.java @@ -22,6 +22,10 @@ import java.util.ArrayList; import java.util.List; import javax.crypto.SecretKey; +import static org.keycloak.common.crypto.CryptoConstants.EC_KEY_SECP256R1; +import static org.keycloak.common.crypto.CryptoConstants.EC_KEY_SECP384R1; +import static org.keycloak.common.crypto.CryptoConstants.EC_KEY_SECP521R1; + public class KeyWrapper { private String providerId; @@ -82,6 +86,8 @@ public class KeyWrapper { *

For keys of type {@link KeyType#EC}, {@link Algorithm#ES256}, {@link Algorithm#ES384}, or {@link Algorithm#ES512} * is returned based on the curve * + *

For keys of type {@link KeyType#OKP}, {@link Algorithm#EdDSA} as that is the only value supported for that key type + * * @return the algorithm set or a default based on the key type. */ public String getAlgorithmOrDefault() { @@ -91,15 +97,21 @@ public class KeyWrapper { if (curve != null) { switch (curve) { case "P-256": + case EC_KEY_SECP256R1: return Algorithm.ES256; case "P-384": + case EC_KEY_SECP384R1: return Algorithm.ES384; case "P-512": + case "P-521": + case EC_KEY_SECP521R1: return Algorithm.ES512; } } case KeyType.RSA: return Algorithm.RS256; + case KeyType.OKP: + return Algorithm.EdDSA; } } return algorithm; diff --git a/core/src/main/java/org/keycloak/sdjwt/IssuerSignedJWT.java b/core/src/main/java/org/keycloak/sdjwt/IssuerSignedJWT.java index eb4ee7d547a..0e7c36c0e42 100644 --- a/core/src/main/java/org/keycloak/sdjwt/IssuerSignedJWT.java +++ b/core/src/main/java/org/keycloak/sdjwt/IssuerSignedJWT.java @@ -34,7 +34,6 @@ import org.keycloak.common.VerificationException; import org.keycloak.crypto.SignatureSignerContext; import org.keycloak.jose.jwk.JWK; import org.keycloak.jose.jws.JWSHeader; -import org.keycloak.sdjwt.vp.KeyBindingJWT; import org.keycloak.util.JsonSerialization; import com.fasterxml.jackson.databind.JsonNode; @@ -44,6 +43,7 @@ import com.fasterxml.jackson.databind.node.LongNode; import com.fasterxml.jackson.databind.node.ObjectNode; import static org.keycloak.OID4VCConstants.CLAIM_NAME_CNF; +import static org.keycloak.OID4VCConstants.CLAIM_NAME_JWK; import static org.keycloak.OID4VCConstants.CLAIM_NAME_SD; import static org.keycloak.OID4VCConstants.CLAIM_NAME_SD_HASH_ALGORITHM; @@ -461,27 +461,10 @@ public class IssuerSignedJWT extends JwsToken { return this; } - /** - * this method requires the public key to be present in the keybindingJwts header as "jwk" claim - */ - public Builder withKeyBinding(KeyBindingJWT keyBinding) { + public Builder withKeyBindingKey(JWK keyBinding) { + ObjectNode jwkNode = JsonSerialization.mapper.convertValue(keyBinding, ObjectNode.class); ObjectNode cnf = JsonNodeFactory.instance.objectNode(); - Optional.ofNullable(keyBinding.getJwsHeader().getOtherClaims().get(OID4VCConstants.CLAIM_NAME_JWK)) - .map(map -> JsonSerialization.mapper.convertValue(map, ObjectNode.class)) - .ifPresent(jwkNode -> cnf.set(OID4VCConstants.CLAIM_NAME_JWK, jwkNode)); - if (!cnf.isEmpty()) { - getClaims().add(new VisibleSdJwtClaim(SdJwtClaimName.of(CLAIM_NAME_CNF), cnf)); - } - return this; - } - - public Builder withKeyBinding(JWK keyBinding) { - return withKeyBinding(JsonSerialization.mapper.convertValue(keyBinding, ObjectNode.class)); - } - - public Builder withKeyBinding(ObjectNode keyBinding) { - ObjectNode cnf = JsonNodeFactory.instance.objectNode(); - cnf.set("jwk", keyBinding); + cnf.set(CLAIM_NAME_JWK, jwkNode); getClaims().add(new VisibleSdJwtClaim(SdJwtClaimName.of(CLAIM_NAME_CNF), cnf)); return this; } diff --git a/core/src/main/java/org/keycloak/sdjwt/JwkParsingUtils.java b/core/src/main/java/org/keycloak/sdjwt/JwkParsingUtils.java index 9bb8b56d0a5..412410c6c6f 100644 --- a/core/src/main/java/org/keycloak/sdjwt/JwkParsingUtils.java +++ b/core/src/main/java/org/keycloak/sdjwt/JwkParsingUtils.java @@ -19,15 +19,11 @@ package org.keycloak.sdjwt; import java.util.Objects; -import org.keycloak.crypto.Algorithm; -import org.keycloak.crypto.AsymmetricSignatureVerifierContext; -import org.keycloak.crypto.ECCurve; -import org.keycloak.crypto.ECDSASignatureVerifierContext; -import org.keycloak.crypto.KeyType; import org.keycloak.crypto.KeyWrapper; import org.keycloak.crypto.SignatureVerifierContext; import org.keycloak.jose.jwk.JWK; import org.keycloak.util.JWKSUtils; +import org.keycloak.util.KeyWrapperUtil; import com.fasterxml.jackson.databind.JsonNode; @@ -50,7 +46,6 @@ public class JwkParsingUtils { public static SignatureVerifierContext convertJwkToVerifierContext(JWK jwk) { // Wrap JWK - KeyWrapper keyWrapper; try { @@ -61,39 +56,6 @@ public class JwkParsingUtils { } // Build verifier - - // KeyType.EC - if (keyWrapper.getType().equals(KeyType.EC)) { - if (keyWrapper.getAlgorithm() == null) { - Objects.requireNonNull(keyWrapper.getCurve()); - - String alg = null; - switch (ECCurve.fromStdCrv(keyWrapper.getCurve())) { - case P256: - alg = Algorithm.ES256; - break; - case P384: - alg = Algorithm.ES384; - break; - case P521: - alg = Algorithm.ES512; - break; - } - - keyWrapper.setAlgorithm(alg); - } - - return new ECDSASignatureVerifierContext(keyWrapper); - } - - // KeyType.RSA - if (keyWrapper.getType().equals(KeyType.RSA)) { - return new AsymmetricSignatureVerifierContext(keyWrapper); - } - - // KeyType is not supported - // This is unreachable as of now given that `JWKSUtils.getKeyWrapper` will fail - // on JWKs with key type not equal to EC or RSA. - throw new IllegalArgumentException("Unexpected key type: " + keyWrapper.getType()); + return KeyWrapperUtil.createSignatureVerifierContext(keyWrapper); } } diff --git a/core/src/main/java/org/keycloak/util/DPoPGenerator.java b/core/src/main/java/org/keycloak/util/DPoPGenerator.java index 9cf87832dbd..3091dd592e1 100644 --- a/core/src/main/java/org/keycloak/util/DPoPGenerator.java +++ b/core/src/main/java/org/keycloak/util/DPoPGenerator.java @@ -24,9 +24,6 @@ import java.security.PrivateKey; import org.keycloak.OAuth2Constants; import org.keycloak.common.util.SecretGenerator; import org.keycloak.common.util.Time; -import org.keycloak.crypto.AsymmetricSignatureSignerContext; -import org.keycloak.crypto.ECDSASignatureSignerContext; -import org.keycloak.crypto.KeyType; import org.keycloak.crypto.KeyUse; import org.keycloak.crypto.KeyWrapper; import org.keycloak.crypto.SignatureSignerContext; @@ -93,23 +90,11 @@ public class DPoPGenerator { } private String sign(JWSHeader jwsHeader, DPoP dpop, KeyWrapper keyWrapper) { - SignatureSignerContext sigCtx = createSignatureSignerContext(keyWrapper); + SignatureSignerContext sigCtx = KeyWrapperUtil.createSignatureSignerContext(keyWrapper); return new JWSBuilder() .header(jwsHeader) .jsonContent(dpop) .sign(sigCtx); } - - private SignatureSignerContext createSignatureSignerContext(KeyWrapper keyWrapper) { - switch (keyWrapper.getType()) { - case KeyType.EC: - return new ECDSASignatureSignerContext(keyWrapper); - case KeyType.RSA: - case KeyType.OKP: - return new AsymmetricSignatureSignerContext(keyWrapper); - default: - throw new IllegalArgumentException("No signer provider for key algorithm type " + keyWrapper.getType()); - } - } } diff --git a/core/src/main/java/org/keycloak/util/KeyWrapperUtil.java b/core/src/main/java/org/keycloak/util/KeyWrapperUtil.java new file mode 100644 index 00000000000..d821c30eceb --- /dev/null +++ b/core/src/main/java/org/keycloak/util/KeyWrapperUtil.java @@ -0,0 +1,40 @@ +package org.keycloak.util; + +import org.keycloak.crypto.AsymmetricSignatureSignerContext; +import org.keycloak.crypto.AsymmetricSignatureVerifierContext; +import org.keycloak.crypto.ECDSASignatureSignerContext; +import org.keycloak.crypto.ECDSASignatureVerifierContext; +import org.keycloak.crypto.KeyType; +import org.keycloak.crypto.KeyWrapper; +import org.keycloak.crypto.SignatureSignerContext; +import org.keycloak.crypto.SignatureVerifierContext; + +public class KeyWrapperUtil { + + public static SignatureSignerContext createSignatureSignerContext(KeyWrapper keyWrapper) { + switch (keyWrapper.getType()) { + case KeyType.EC: + return new ECDSASignatureSignerContext(keyWrapper); + case KeyType.RSA: + case KeyType.OKP: + return new AsymmetricSignatureSignerContext(keyWrapper); + default: + throw new IllegalArgumentException("No signer provider for key algorithm type " + keyWrapper.getType()); + } + } + + public static SignatureVerifierContext createSignatureVerifierContext(KeyWrapper keyWrapper) { + switch (keyWrapper.getType()) { + case KeyType.EC: + return new ECDSASignatureVerifierContext(keyWrapper); + case KeyType.RSA: + case KeyType.OKP: + return new AsymmetricSignatureVerifierContext(keyWrapper); + default: + throw new IllegalArgumentException("No signer provider for key algorithm type " + keyWrapper.getType()); + } + } + + private KeyWrapperUtil() { + } +} diff --git a/core/src/test/java/org/keycloak/sdjwt/SdJwtCreationAndSigningTest.java b/core/src/test/java/org/keycloak/sdjwt/SdJwtCreationAndSigningTest.java index 78e33f0b68c..d2aa794f8a3 100644 --- a/core/src/test/java/org/keycloak/sdjwt/SdJwtCreationAndSigningTest.java +++ b/core/src/test/java/org/keycloak/sdjwt/SdJwtCreationAndSigningTest.java @@ -20,9 +20,6 @@ package org.keycloak.sdjwt; import java.nio.charset.StandardCharsets; import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.SecureRandom; -import java.security.spec.ECGenParameterSpec; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.ArrayList; @@ -55,6 +52,8 @@ import org.junit.Assert; import org.junit.ClassRule; import org.junit.Test; +import static org.keycloak.common.crypto.CryptoConstants.EC_KEY_SECP256R1; + /** * @author Pascal Knueppel * @since 13.11.2025 @@ -185,10 +184,10 @@ public abstract class SdJwtCreationAndSigningTest { public void testCreateSdJwtWithKeybindingJwt() throws Exception { final String authorizationServerUrl = "https://example.com"; - KeyWrapper issuerKeyPair = toKeyWrapper(createEcKey()); + KeyWrapper issuerKeyPair = toKeyWrapper(KeyUtils.generateEcKeyPair(EC_KEY_SECP256R1)); JWK issuerJwk = JWKBuilder.create().ec(issuerKeyPair.getPublicKey()); - KeyWrapper holderKeyPair = toKeyWrapper(createEcKey()); + KeyWrapper holderKeyPair = toKeyWrapper(KeyUtils.generateEcKeyPair(EC_KEY_SECP256R1)); JWK holderKeybindingKey = JWKBuilder.create().ec(holderKeyPair.getPublicKey()); SignatureSignerContext issuerSignerContext = new ECDSASignatureSignerContext(issuerKeyPair); @@ -225,7 +224,7 @@ public abstract class SdJwtCreationAndSigningTest { .withKid(issuerJwk.getKeyId()) /* body */ .withClaims(disclosures, disclosureSpec) - .withKeyBinding(holderKeybindingKey) + .withKeyBindingKey(holderKeybindingKey) .withIat(iat) .withNbf(nbf) .withExp(exp) @@ -384,16 +383,6 @@ public abstract class SdJwtCreationAndSigningTest { } } - public KeyPair createEcKey() { - try { - KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC"); - kpg.initialize(new ECGenParameterSpec("secp521r1"), new SecureRandom()); - return kpg.generateKeyPair(); - } catch (Exception e) { - throw new IllegalStateException(e); - } - } - public KeyWrapper toKeyWrapper(KeyPair keyPair) { KeyWrapper keyWrapper = new KeyWrapper(); keyWrapper.setKid(KeyUtils.createKeyId(keyPair.getPublic())); diff --git a/core/src/test/java/org/keycloak/sdjwt/TestSettings.java b/core/src/test/java/org/keycloak/sdjwt/TestSettings.java index 4034b73c84f..005e1eb3215 100644 --- a/core/src/test/java/org/keycloak/sdjwt/TestSettings.java +++ b/core/src/test/java/org/keycloak/sdjwt/TestSettings.java @@ -19,10 +19,8 @@ package org.keycloak.sdjwt; import java.math.BigInteger; import java.security.KeyFactory; import java.security.KeyPair; -import java.security.KeyPairGenerator; import java.security.PrivateKey; import java.security.PublicKey; -import java.security.spec.ECGenParameterSpec; import java.security.spec.ECParameterSpec; import java.security.spec.ECPoint; import java.security.spec.ECPrivateKeySpec; @@ -193,10 +191,7 @@ public class TestSettings { // generate key spec private static ECParameterSpec generateEcdsaKeySpec(String paramSpecName) { try { - KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC"); - ECGenParameterSpec ecGenParameterSpec = new ECGenParameterSpec(paramSpecName); - keyPairGenerator.initialize(ecGenParameterSpec); - KeyPair keyPair = keyPairGenerator.generateKeyPair(); + KeyPair keyPair = KeyUtils.generateEcKeyPair(paramSpecName); return ((java.security.interfaces.ECPublicKey) keyPair.getPublic()).getParams(); } catch (Exception e) { throw new RuntimeException("Error obtaining ECParameterSpec for P-256 curve", e); diff --git a/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/SdJwtKeyBindingTest.java b/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/SdJwtKeyBindingTest.java new file mode 100644 index 00000000000..d4ece01b1f7 --- /dev/null +++ b/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/SdJwtKeyBindingTest.java @@ -0,0 +1,242 @@ +package org.keycloak.sdjwt.sdjwtvp; + +import java.security.KeyPair; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.keycloak.common.VerificationException; +import org.keycloak.common.util.KeyUtils; +import org.keycloak.common.util.Time; +import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.KeyWrapper; +import org.keycloak.crypto.SignatureVerifierContext; +import org.keycloak.jose.jwk.ECPublicJWK; +import org.keycloak.jose.jwk.JWK; +import org.keycloak.jose.jwk.JWKBuilder; +import org.keycloak.jose.jwk.OKPPublicJWK; +import org.keycloak.jose.jwk.RSAPublicJWK; +import org.keycloak.rule.CryptoInitRule; +import org.keycloak.sdjwt.DisclosureSpec; +import org.keycloak.sdjwt.IssuerSignedJWT; +import org.keycloak.sdjwt.IssuerSignedJwtVerificationOpts; +import org.keycloak.sdjwt.SdJwt; +import org.keycloak.sdjwt.SdJwtUtils; +import org.keycloak.sdjwt.TestSettings; +import org.keycloak.sdjwt.TestUtils; +import org.keycloak.sdjwt.vp.KeyBindingJWT; +import org.keycloak.sdjwt.vp.KeyBindingJwtVerificationOpts; +import org.keycloak.sdjwt.vp.SdJwtVP; +import org.keycloak.util.JWKSUtils; +import org.keycloak.util.KeyWrapperUtil; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; + +import static org.keycloak.OID4VCConstants.CLAIM_NAME_EXP; +import static org.keycloak.OID4VCConstants.CLAIM_NAME_IAT; +import static org.keycloak.OID4VCConstants.CLAIM_NAME_ISSUER; +import static org.keycloak.OID4VCConstants.CLAIM_NAME_JWK; +import static org.keycloak.OID4VCConstants.CLAIM_NAME_NBF; +import static org.keycloak.common.crypto.CryptoConstants.EC_KEY_SECP256R1; +import static org.keycloak.common.crypto.CryptoConstants.EC_KEY_SECP384R1; +import static org.keycloak.common.crypto.CryptoConstants.EC_KEY_SECP521R1; +import static org.keycloak.sdjwt.sdjwtvp.SdJwtVPVerificationTest.testSettings; + +import static org.hamcrest.CoreMatchers.is; + +/** + * Test of various algorithms and scenarios for SD-JWT key binding + */ +public abstract class SdJwtKeyBindingTest { + + @ClassRule + public static CryptoInitRule cryptoInitRule = new CryptoInitRule(); + + @Test + public void testEdDSAKeyBindingWithEd25519() throws VerificationException { + testKeyBinding(() -> KeyUtils.generateEddsaKeyPair(Algorithm.Ed25519), + keyPair -> JWKBuilder.create().okp(keyPair.getPublic()), + jwk -> assertEdDSAKey(jwk, Algorithm.Ed25519)); + } + + @Test + public void testEdDSAKeyBindingWithEd448() throws VerificationException { + testKeyBinding(() -> KeyUtils.generateEddsaKeyPair(Algorithm.Ed448), + keyPair -> JWKBuilder.create().okp(keyPair.getPublic()), + jwk -> assertEdDSAKey(jwk, Algorithm.Ed448)); + } + + @Test + public void testEc256KeyBinding() throws VerificationException { + testKeyBinding(() -> KeyUtils.generateEcKeyPair(EC_KEY_SECP256R1), + keyPair -> JWKBuilder.create().ec(keyPair.getPublic()), + jwk -> assertEcKey(jwk, "P-256")); + } + + @Test + public void testEc384KeyBinding() throws VerificationException { + testKeyBinding(() -> KeyUtils.generateEcKeyPair(EC_KEY_SECP384R1), + keyPair -> JWKBuilder.create().ec(keyPair.getPublic()), + jwk -> assertEcKey(jwk, "P-384")); + } + + @Test + public void testEc521KeyBinding() throws VerificationException { + testKeyBinding(() -> KeyUtils.generateEcKeyPair(EC_KEY_SECP521R1), + keyPair -> JWKBuilder.create().ec(keyPair.getPublic()), + jwk -> assertEcKey(jwk, "P-521")); + } + + @Test + public void testRSA2048KeyBinding() throws VerificationException { + testKeyBinding(() -> KeyUtils.generateRsaKeyPair(2048), + keyPair -> JWKBuilder.create().rsa(keyPair.getPublic()), + jwk -> assertRsaKey(jwk)); + } + + @Test + public void testRSA4096KeyBinding() throws VerificationException { + testKeyBinding(() -> KeyUtils.generateRsaKeyPair(4096), + keyPair -> JWKBuilder.create().rsa(keyPair.getPublic()), + jwk -> assertRsaKey(jwk)); + } + + private void testKeyBinding(Supplier keyPairSupplier, Function jwkProvider, Consumer keyFormatValidator) throws VerificationException { + DisclosureSpec disclosureSpec = DisclosureSpec.builder() + .withUndisclosedClaim("given_name", "2GLC42sKQveCfGfryNRN9w") + .withUndisclosedClaim("family_name", "eluV5Og3gSNII8EYnsxA_A") + .withUndisclosedClaim("email", "6Ij7tM-a5iVPGboS5tmvVA") + .build(); + + // Read claims provided by the holder + ObjectNode holderClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s3.3-holder-claims.json"); + + int currentTime = Time.currentTime(); + holderClaimSet.put(CLAIM_NAME_ISSUER, "https://example.com/issuer"); + holderClaimSet.put(CLAIM_NAME_IAT, currentTime); + holderClaimSet.put(CLAIM_NAME_NBF, currentTime); + holderClaimSet.put(CLAIM_NAME_EXP, currentTime + 60); + + // Generate key-binding key pair + KeyPair keyPair = keyPairSupplier.get(); + JWK publicJwk = jwkProvider.apply(keyPair); + + KeyWrapper keyWrapper = JWKSUtils.getKeyWrapper(publicJwk); + keyWrapper.setPrivateKey(keyPair.getPrivate()); + + KeyBindingJWT keyBindingJWT = generateKeyBindingJWT(publicJwk.getKeyId()); + + // Create issuer-signed JWT with the key attached + IssuerSignedJWT issuerSignedJWT = IssuerSignedJWT.builder() + .withClaims(holderClaimSet, disclosureSpec) + .withKeyBindingKey(publicJwk) + .build(); + SdJwt sdJwt = SdJwt.builder() + .withIssuerSignedJwt(issuerSignedJWT) + .withKeybindingJwt(keyBindingJWT) + .build(TestSettings.getInstance().getIssuerSignerContext(), KeyWrapperUtil.createSignatureSignerContext(keyWrapper)); + + String sdJwtString = sdJwt.toString(); + + // 2 - Parse presentation and verify successfully (especially key binding) + + // Just use the presentation with all the claims disclosed as provided by issuer + SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtString); + + // Expect correct JWK + JsonNode cnf = sdJwtVP.getCnfClaim(); + Assert.assertNotNull(cnf); + JWK jwk = SdJwtUtils.mapper.convertValue(cnf.get(CLAIM_NAME_JWK), JWK.class); + + keyFormatValidator.accept(jwk); + + sdJwtVP.verify( + defaultIssuerVerifyingKeys(), + defaultIssuerSignedJwtVerificationOpts().build(), + defaultKeyBindingJwtVerificationOpts().build() + ); + + // 3 - Test incorrect key-binding signature + KeyBindingJWT invalidBindingJWT = generateKeyBindingJWT(publicJwk.getKeyId()); + invalidBindingJWT.getPayload().put("nonce", "invalid"); + String invalidSdJwt = SdJwt.builder() + .withIssuerSignedJwt(issuerSignedJWT) + .withKeybindingJwt(invalidBindingJWT) + .build(TestSettings.getInstance().getIssuerSignerContext(), KeyWrapperUtil.createSignatureSignerContext(keyWrapper)) + .toString(); + + // Replace signature with the signature from valid sdJwt + String signature1 = sdJwtString.substring(sdJwtString.lastIndexOf('.') + 1); + invalidSdJwt = invalidSdJwt.substring(0, invalidSdJwt.lastIndexOf('.') + 1); + invalidSdJwt = invalidSdJwt + signature1; + + SdJwtVP invalidSdJwtVP = SdJwtVP.of(invalidSdJwt); + try { + invalidSdJwtVP.verify( + defaultIssuerVerifyingKeys(), + defaultIssuerSignedJwtVerificationOpts().build(), + defaultKeyBindingJwtVerificationOpts().build() + ); + Assert.fail("Not expected to successfully validate key-binding JWT"); + } catch (VerificationException ve) { + Assert.assertEquals("Key binding JWT invalid", ve.getMessage()); + } + } + + private void assertEdDSAKey(JWK jwk, String expectedCurve) { + Assert.assertEquals(2, jwk.getOtherClaims().size()); + Assert.assertEquals(expectedCurve, jwk.getOtherClaims().get(OKPPublicJWK.CRV)); + Assert.assertThat(jwk.getOtherClaims().containsKey(OKPPublicJWK.X), is(true)); + } + + private void assertEcKey(JWK jwk, String expectedCurve) { + Assert.assertEquals(3, jwk.getOtherClaims().size()); + Assert.assertEquals(expectedCurve, jwk.getOtherClaims().get(ECPublicJWK.CRV)); + Assert.assertThat(jwk.getOtherClaims().containsKey(ECPublicJWK.X), is(true)); + Assert.assertThat(jwk.getOtherClaims().containsKey(ECPublicJWK.Y), is(true)); + } + + private void assertRsaKey(JWK jwk) { + Assert.assertEquals(2, jwk.getOtherClaims().size()); + Assert.assertThat(jwk.getOtherClaims().containsKey(RSAPublicJWK.PUBLIC_EXPONENT), is(true)); + Assert.assertThat(jwk.getOtherClaims().containsKey(RSAPublicJWK.MODULUS), is(true)); + } + + private KeyBindingJWT generateKeyBindingJWT(String keyId) { + int currentTime = Time.currentTime(); + + return KeyBindingJWT.builder() + /* header */ + .withKid(keyId) + /* body */ + .withIat(currentTime) + .withNbf(currentTime) + .withExp(currentTime + 60) + .withNonce("1234567890") + .withAudience("https://verifier.example.org") + .build(); + } + + + private List defaultIssuerVerifyingKeys() { + return Collections.singletonList(testSettings.issuerVerifierContext); + } + + private IssuerSignedJwtVerificationOpts.Builder defaultIssuerSignedJwtVerificationOpts() { + return IssuerSignedJwtVerificationOpts.builder() + .withClockSkew(0); + } + + private KeyBindingJwtVerificationOpts.Builder defaultKeyBindingJwtVerificationOpts() { + return KeyBindingJwtVerificationOpts.builder() + .withKeyBindingRequired(true) + .withNonceCheck("1234567890") + .withAudCheck("https://verifier.example.org"); + } +} diff --git a/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/SdJwtVPTest.java b/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/SdJwtVPTest.java index 3523159f57e..3b67a577dbe 100644 --- a/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/SdJwtVPTest.java +++ b/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/SdJwtVPTest.java @@ -38,6 +38,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; + /** * @author Francis Pouatcha */ diff --git a/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwtKeyBindingTest.java b/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwtKeyBindingTest.java new file mode 100644 index 00000000000..58154dbb2ca --- /dev/null +++ b/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwtKeyBindingTest.java @@ -0,0 +1,16 @@ +package org.keycloak.crypto.def.test.sdjwt; + +import org.keycloak.common.util.Environment; +import org.keycloak.sdjwt.sdjwtvp.SdJwtKeyBindingTest; + +import org.junit.Assume; +import org.junit.Before; + +public class DefaultCryptoSdJwtKeyBindingTest extends SdJwtKeyBindingTest { + + @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()); + } +} diff --git a/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwtKeyBindingTest.java b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwtKeyBindingTest.java new file mode 100644 index 00000000000..b19baf4491e --- /dev/null +++ b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwtKeyBindingTest.java @@ -0,0 +1,8 @@ +package org.keycloak.crypto.elytron.test.sdjwt; + +import org.keycloak.sdjwt.sdjwtvp.SdJwtKeyBindingTest; + +public class ElytronCryptoSdJwtKeyBindingTest extends SdJwtKeyBindingTest { + + +} diff --git a/crypto/fips1402/src/test/java/org/keycloak/crypto/fips/test/sdjwt/FIPS1402SdJwtKeyBindingTest.java b/crypto/fips1402/src/test/java/org/keycloak/crypto/fips/test/sdjwt/FIPS1402SdJwtKeyBindingTest.java new file mode 100644 index 00000000000..fec614d25b6 --- /dev/null +++ b/crypto/fips1402/src/test/java/org/keycloak/crypto/fips/test/sdjwt/FIPS1402SdJwtKeyBindingTest.java @@ -0,0 +1,16 @@ +package org.keycloak.crypto.fips.test.sdjwt; + +import org.keycloak.common.util.Environment; +import org.keycloak.sdjwt.sdjwtvp.SdJwtKeyBindingTest; + +import org.junit.Assume; +import org.junit.Before; + +public class FIPS1402SdJwtKeyBindingTest extends SdJwtKeyBindingTest { + + @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()); + } +} diff --git a/services/src/main/java/org/keycloak/keys/AbstractEcKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/AbstractEcKeyProviderFactory.java index 96f9f91adc8..8874b6213e6 100644 --- a/services/src/main/java/org/keycloak/keys/AbstractEcKeyProviderFactory.java +++ b/services/src/main/java/org/keycloak/keys/AbstractEcKeyProviderFactory.java @@ -16,10 +16,6 @@ */ package org.keycloak.keys; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.SecureRandom; -import java.security.spec.ECGenParameterSpec; import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentValidationException; @@ -49,18 +45,6 @@ public abstract class AbstractEcKeyProviderFactory implem .checkBoolean(Attributes.EC_GENERATE_CERTIFICATE_PROPERTY, false); } - public static KeyPair generateEcKeyPair(String keySpecName) { - try { - KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC"); - SecureRandom randomGen = new SecureRandom(); - ECGenParameterSpec ecSpec = new ECGenParameterSpec(keySpecName); - keyGen.initialize(ecSpec, randomGen); - return keyGen.generateKeyPair(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - public static String convertECDomainParmNistRepToSecRep(String ecInNistRep) { // convert Elliptic Curve Domain Parameter Name in NIST to SEC which is used to generate its EC key String ecInSecRep = null; diff --git a/services/src/main/java/org/keycloak/keys/AbstractEddsaKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/AbstractEddsaKeyProviderFactory.java index e6ace71356b..d381e165f2a 100644 --- a/services/src/main/java/org/keycloak/keys/AbstractEddsaKeyProviderFactory.java +++ b/services/src/main/java/org/keycloak/keys/AbstractEddsaKeyProviderFactory.java @@ -16,9 +16,6 @@ */ package org.keycloak.keys; -import java.security.KeyPair; -import java.security.KeyPairGenerator; - import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentValidationException; import org.keycloak.crypto.Algorithm; @@ -59,13 +56,4 @@ public abstract class AbstractEddsaKeyProviderFactory implements KeyProviderFact .checkBoolean(Attributes.ACTIVE_PROPERTY, false); } - public static KeyPair generateEddsaKeyPair(String curveName) { - try { - KeyPairGenerator keyGen = KeyPairGenerator.getInstance(curveName); - return keyGen.generateKeyPair(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - } diff --git a/services/src/main/java/org/keycloak/keys/AbstractGeneratedEcKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/AbstractGeneratedEcKeyProviderFactory.java index e171751c88d..979ded51be1 100644 --- a/services/src/main/java/org/keycloak/keys/AbstractGeneratedEcKeyProviderFactory.java +++ b/services/src/main/java/org/keycloak/keys/AbstractGeneratedEcKeyProviderFactory.java @@ -22,6 +22,7 @@ import java.security.interfaces.ECPublicKey; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; +import org.keycloak.common.util.KeyUtils; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentValidationException; @@ -102,7 +103,7 @@ public abstract class AbstractGeneratedEcKeyProviderFactoryMarek Posolda */ @@ -139,22 +138,22 @@ public class TestingOIDCEndpointsApplicationResource { break; case Algorithm.ES256: keyType = KeyType.EC; - keyPair = generateEcdsaKey("secp256r1"); + keyPair = KeyUtils.generateEcKeyPair(EC_KEY_SECP256R1); break; case Algorithm.ES384: keyType = KeyType.EC; - keyPair = generateEcdsaKey("secp384r1"); + keyPair = KeyUtils.generateEcKeyPair(EC_KEY_SECP384R1); break; case Algorithm.ES512: keyType = KeyType.EC; - keyPair = generateEcdsaKey("secp521r1"); + keyPair = KeyUtils.generateEcKeyPair(EC_KEY_SECP521R1); break; case Algorithm.EdDSA: if (curve == null) { curve = Algorithm.Ed25519; } keyType = KeyType.OKP; - keyPair = generateEddsaKey(curve); + keyPair = KeyUtils.generateEddsaKeyPair(curve); break; case JWEConstants.RSA1_5: case JWEConstants.RSA_OAEP: @@ -186,21 +185,6 @@ public class TestingOIDCEndpointsApplicationResource { return getKeysAsPem(); } - private KeyPair generateEcdsaKey(String ecDomainParamName) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { - KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC"); - SecureRandom randomGen = new SecureRandom(); - ECGenParameterSpec ecSpec = new ECGenParameterSpec(ecDomainParamName); - keyGen.initialize(ecSpec, randomGen); - KeyPair keyPair = keyGen.generateKeyPair(); - return keyPair; - } - - private KeyPair generateEddsaKey(String curveName) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { - KeyPairGenerator keyGen = KeyPairGenerator.getInstance(curveName); - KeyPair keyPair = keyGen.generateKeyPair(); - return keyPair; - } - @GET @Produces(MediaType.APPLICATION_JSON) @Path("/get-keys-as-pem") diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/DPoPTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/DPoPTest.java index 7621f01b141..0386ca48ef8 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/DPoPTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/DPoPTest.java @@ -357,7 +357,7 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest { public void testDPoPProofByConfidentialClient_EdDSA() throws Exception { // Generating keys String curveName = AbstractEddsaKeyProviderFactory.DEFAULT_EDDSA_ELLIPTIC_CURVE; - KeyPair keyPair = AbstractEddsaKeyProviderFactory.generateEddsaKeyPair(curveName); + KeyPair keyPair = KeyUtils.generateEddsaKeyPair(curveName); // JWK JWKBuilder b = JWKBuilder.create() diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientPoliciesUtil.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientPoliciesUtil.java index bb1060800f6..eb32b3a90e5 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientPoliciesUtil.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientPoliciesUtil.java @@ -21,12 +21,9 @@ import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.Key; import java.security.KeyPair; -import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; -import java.security.SecureRandom; import java.security.interfaces.ECPublicKey; -import java.security.spec.ECGenParameterSpec; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; @@ -468,12 +465,7 @@ public final class ClientPoliciesUtil { } public static KeyPair generateEcdsaKey(String ecDomainParamName) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { - KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC"); - SecureRandom randomGen = new SecureRandom(); - ECGenParameterSpec ecSpec = new ECGenParameterSpec(ecDomainParamName); - keyGen.initialize(ecSpec, randomGen); - KeyPair keyPair = keyGen.generateKeyPair(); - return keyPair; + return org.keycloak.common.util.KeyUtils.generateEcKeyPair(ecDomainParamName); } public static String generateSignedDPoPProof(String jti, String htm, String htu, Long iat, String algorithm, JWSHeader jwsHeader, PrivateKey privateKey, String accessToken) throws IOException {