Make sd-jwt key binding verification work with EdDSA keys

closes #44369

Signed-off-by: mposolda <mposolda@gmail.com>
This commit is contained in:
mposolda 2025-11-20 11:39:00 +01:00 committed by Marek Posolda
parent d0e4d1f620
commit cbb823bc0e
24 changed files with 395 additions and 217 deletions

View File

@ -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";
}

View File

@ -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()));

View File

@ -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);
}
}
}

View File

@ -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 {
* <p>For keys of type {@link KeyType#EC}, {@link Algorithm#ES256}, {@link Algorithm#ES384}, or {@link Algorithm#ES512}
* is returned based on the curve
*
* <p>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;

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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());
}
}
}

View File

@ -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() {
}
}

View File

@ -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()));

View File

@ -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);

View File

@ -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<KeyPair> keyPairSupplier, Function<KeyPair, JWK> jwkProvider, Consumer<JWK> 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<SignatureVerifierContext> 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");
}
}

View File

@ -38,6 +38,7 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
/**
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/

View File

@ -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());
}
}

View File

@ -0,0 +1,8 @@
package org.keycloak.crypto.elytron.test.sdjwt;
import org.keycloak.sdjwt.sdjwtvp.SdJwtKeyBindingTest;
public class ElytronCryptoSdJwtKeyBindingTest extends SdJwtKeyBindingTest {
}

View File

@ -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());
}
}

View File

@ -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<T extends KeyProvider> 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;

View File

@ -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);
}
}
}

View File

@ -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 AbstractGeneratedEcKeyProviderFactory<T extends KeyProvide
protected void generateKeys(ComponentModel model, String ecInNistRep) {
KeyPair keyPair;
try {
keyPair = generateEcKeyPair(convertECDomainParmNistRepToSecRep(ecInNistRep));
keyPair = KeyUtils.generateEcKeyPair(convertECDomainParmNistRepToSecRep(ecInNistRep));
model.put(getEcPrivateKeyKey(), Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded()));
model.put(getEcPublicKeyKey(), Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()));
model.put(getEcEllipticCurveKey(), ecInNistRep);

View File

@ -23,6 +23,7 @@ import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.List;
import org.keycloak.common.util.KeyUtils;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
@ -120,7 +121,7 @@ public class GeneratedEddsaKeyProviderFactory extends AbstractEddsaKeyProviderFa
private void generateKeys(ComponentModel model, String curveName) {
KeyPair keyPair;
try {
keyPair = generateEddsaKeyPair(curveName);
keyPair = KeyUtils.generateEddsaKeyPair(curveName);
model.put(EDDSA_PRIVATE_KEY_KEY, Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded()));
model.put(EDDSA_PUBLIC_KEY_KEY, Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()));
model.put(EDDSA_ELLIPTIC_CURVE_KEY, curveName);

View File

@ -19,7 +19,6 @@ package org.keycloak.jose.jwk;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
@ -51,8 +50,7 @@ public class ServerJWKTest {
@Test
public void publicEd25519() throws Exception {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(Algorithm.Ed25519);
KeyPair keyPair = keyGen.generateKeyPair();
KeyPair keyPair = KeyUtils.generateEddsaKeyPair(Algorithm.Ed25519);
PublicKey publicKey = keyPair.getPublic();
JWK jwk = JWKBuilder.create().kid(KeyUtils.createKeyId(keyPair.getPublic())).algorithm(Algorithm.EdDSA).okp(publicKey);
@ -82,8 +80,7 @@ public class ServerJWKTest {
@Test
public void publicEd448() throws Exception {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(Algorithm.Ed448);
KeyPair keyPair = keyGen.generateKeyPair();
KeyPair keyPair = KeyUtils.generateEddsaKeyPair(Algorithm.Ed448);
PublicKey publicKey = keyPair.getPublic();
JWK jwk = JWKBuilder.create().kid(KeyUtils.createKeyId(keyPair.getPublic())).algorithm(Algorithm.EdDSA).okp(publicKey);

View File

@ -4,12 +4,11 @@ import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.spec.ECGenParameterSpec;
import java.util.HashMap;
import java.util.Map;
import org.keycloak.common.crypto.CryptoIntegration;
import org.keycloak.common.util.KeyUtils;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.ECDSASignatureSignerContext;
import org.keycloak.crypto.KeyUse;
@ -26,6 +25,8 @@ import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import static org.keycloak.common.crypto.CryptoConstants.EC_KEY_SECP256R1;
public class OAuthIdentityProvider {
private final HttpServer httpServer;
@ -123,10 +124,7 @@ public class OAuthIdentityProvider {
KeyUse keyUse = spiffe ? KeyUse.JWT_SVID : KeyUse.SIG;
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
ECGenParameterSpec ecSpec = new ECGenParameterSpec("secp256r1");
keyPairGenerator.initialize(ecSpec);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
KeyPair keyPair = KeyUtils.generateEcKeyPair(EC_KEY_SECP256R1);
JWK jwk = JWKBuilder.create().ec(keyPair.getPublic());
if (!spiffe) {

View File

@ -19,13 +19,8 @@ package org.keycloak.testsuite.rest.resource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.spec.ECGenParameterSpec;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
@ -90,6 +85,10 @@ import org.keycloak.util.JsonSerialization;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.jboss.resteasy.reactive.NoCache;
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;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@ -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")

View File

@ -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()

View File

@ -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 {