Skip to content

Commit 6a27a4c

Browse files
authored
EdDSA support for DPoP (#42362)
closes #42286 Signed-off-by: mposolda <[email protected]>
1 parent cbf915c commit 6a27a4c

File tree

15 files changed

+209
-71
lines changed

15 files changed

+209
-71
lines changed

core/src/main/java/org/keycloak/crypto/AsymmetricSignatureSignerContext.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
public class AsymmetricSignatureSignerContext implements SignatureSignerContext {
2323

24-
private final KeyWrapper key;
24+
protected final KeyWrapper key;
2525

2626
public AsymmetricSignatureSignerContext(KeyWrapper key) throws SignatureException {
2727
this.key = key;

core/src/main/java/org/keycloak/jose/jwk/ECPublicJWK.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,8 @@
1717

1818
package org.keycloak.jose.jwk;
1919

20+
import com.fasterxml.jackson.annotation.JsonIgnore;
2021
import com.fasterxml.jackson.annotation.JsonProperty;
21-
import org.keycloak.common.util.PemUtils;
22-
23-
import java.security.NoSuchAlgorithmException;
2422

2523
/**
2624
* @author <a href="mailto:[email protected]">Stian Thorgersen</a>
@@ -65,4 +63,26 @@ public String getY() {
6563
public void setY(String y) {
6664
this.y = y;
6765
}
66+
67+
@JsonIgnore
68+
@Override
69+
public <T> T getOtherClaim(String claimName, Class<T> claimType) {
70+
Object claim = null;
71+
switch (claimName) {
72+
case CRV:
73+
claim = getCrv();
74+
break;
75+
case X:
76+
claim = getX();
77+
break;
78+
case Y:
79+
claim = getY();
80+
break;
81+
}
82+
if (claim != null) {
83+
return claimType.cast(claim);
84+
} else {
85+
return super.getOtherClaim(claimName, claimType);
86+
}
87+
}
6888
}

core/src/main/java/org/keycloak/jose/jwk/JWK.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import com.fasterxml.jackson.annotation.JsonAnyGetter;
2121
import com.fasterxml.jackson.annotation.JsonAnySetter;
22+
import com.fasterxml.jackson.annotation.JsonIgnore;
2223
import com.fasterxml.jackson.annotation.JsonProperty;
2324
import org.keycloak.common.util.PemUtils;
2425

@@ -169,5 +170,19 @@ public void setOtherClaims(String name, Object value) {
169170
otherClaims.put(name, value);
170171
}
171172

173+
/**
174+
* Ability to retrieve custom claims in a unified way. The subclasses (like for example OKPublicJWK) may contain the custom claims
175+
* as Java properties when the "JWK" class can contain the same claims inside the "otherClaims" map. This method allows to obtain the
176+
* claim in both ways regardless of if we have "JWK" class or some of it's subclass
177+
*
178+
* @param claimName claim name
179+
* @param claimType claim type
180+
* @return claim if present or null
181+
*/
182+
@JsonIgnore
183+
public <T> T getOtherClaim(String claimName, Class<T> claimType) {
184+
Object o = getOtherClaims().get(claimName);
185+
return o == null ? null : claimType.cast(o);
186+
}
172187

173188
}

core/src/main/java/org/keycloak/jose/jwk/OKPPublicJWK.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
package org.keycloak.jose.jwk;
1919

20+
import com.fasterxml.jackson.annotation.JsonIgnore;
2021
import org.keycloak.crypto.KeyType;
2122

2223
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -53,4 +54,23 @@ public void setX(String x) {
5354
this.x = x;
5455
}
5556

57+
@JsonIgnore
58+
@Override
59+
public <T> T getOtherClaim(String claimName, Class<T> claimType) {
60+
Object claim = null;
61+
switch (claimName) {
62+
case CRV:
63+
claim = getCrv();
64+
break;
65+
case X:
66+
claim = getX();
67+
break;
68+
}
69+
if (claim != null) {
70+
return claimType.cast(claim);
71+
} else {
72+
return super.getOtherClaim(claimName, claimType);
73+
}
74+
}
75+
5676
}

core/src/main/java/org/keycloak/jose/jwk/RSAPublicJWK.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
package org.keycloak.jose.jwk;
1919

20+
import com.fasterxml.jackson.annotation.JsonIgnore;
2021
import com.fasterxml.jackson.annotation.JsonProperty;
2122
import org.keycloak.common.util.PemUtils;
2223

@@ -88,4 +89,22 @@ public String getSha256x509Thumbprint() {
8889
return sha256x509Thumbprint;
8990
}
9091

92+
@JsonIgnore
93+
@Override
94+
public <T> T getOtherClaim(String claimName, Class<T> claimType) {
95+
Object claim = null;
96+
switch (claimName) {
97+
case MODULUS:
98+
claim = getModulus();
99+
break;
100+
case PUBLIC_EXPONENT:
101+
claim = getPublicExponent();
102+
break;
103+
}
104+
if (claim != null) {
105+
return claimType.cast(claim);
106+
} else {
107+
return super.getOtherClaim(claimName, claimType);
108+
}
109+
}
91110
}

core/src/main/java/org/keycloak/util/DPoPGenerator.java

Lines changed: 40 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -20,29 +20,25 @@
2020
import java.security.Key;
2121
import java.security.KeyPair;
2222
import java.security.PrivateKey;
23-
import java.security.interfaces.RSAPublicKey;
2423

2524
import org.keycloak.OAuth2Constants;
26-
import org.keycloak.common.util.Base64Url;
2725
import org.keycloak.common.util.SecretGenerator;
2826
import org.keycloak.common.util.Time;
2927
import org.keycloak.crypto.AsymmetricSignatureSignerContext;
3028
import org.keycloak.crypto.ECDSASignatureSignerContext;
3129
import org.keycloak.crypto.KeyType;
3230
import org.keycloak.crypto.KeyUse;
3331
import org.keycloak.crypto.KeyWrapper;
34-
import org.keycloak.crypto.SignatureException;
3532
import org.keycloak.crypto.SignatureSignerContext;
3633
import org.keycloak.jose.jwk.JWK;
37-
import org.keycloak.jose.jwk.RSAPublicJWK;
34+
import org.keycloak.jose.jwk.JWKBuilder;
3835
import org.keycloak.jose.jws.JWSBuilder;
3936
import org.keycloak.jose.jws.JWSHeader;
4037
import org.keycloak.jose.jws.crypto.HashUtils;
4138
import org.keycloak.representations.dpop.DPoP;
4239

4340
import static org.keycloak.OAuth2Constants.DPOP_DEFAULT_ALGORITHM;
4441
import static org.keycloak.OAuth2Constants.DPOP_JWT_HEADER_TYPE;
45-
import static org.keycloak.jose.jwk.JWKUtil.toIntegerBytes;
4642

4743
/**
4844
* Utility for generating signed DPoP proofs
@@ -57,58 +53,61 @@ public class DPoPGenerator {
5753
public static String generateRsaSignedDPoPProof(KeyPair rsaKeyPair, String httpMethod, String endpointURL, String accessToken) {
5854
JWK jwkRsa = createRsaJwk(rsaKeyPair.getPublic());
5955
JWSHeader jwsRsaHeader = new JWSHeader(DPOP_DEFAULT_ALGORITHM, DPOP_JWT_HEADER_TYPE, jwkRsa.getKeyId(), jwkRsa);
60-
return generateSignedDPoPProof(SecretGenerator.getInstance().generateSecureID(), httpMethod, endpointURL, (long) Time.currentTime(),
56+
return new DPoPGenerator().generateSignedDPoPProof(SecretGenerator.getInstance().generateSecureID(), httpMethod, endpointURL, (long) Time.currentTime(),
6157
jwsRsaHeader, rsaKeyPair.getPrivate(), accessToken);
6258
}
6359

6460

65-
public static JWK createRsaJwk(Key publicKey) {
66-
RSAPublicKey rsaKey = (RSAPublicKey) publicKey;
61+
private static JWK createRsaJwk(Key publicKey) {
62+
return JWKBuilder.create()
63+
.rsa(publicKey, KeyUse.SIG);
64+
}
6765

68-
RSAPublicJWK k = new RSAPublicJWK();
69-
k.setKeyType(KeyType.RSA);
70-
k.setModulus(Base64Url.encode(toIntegerBytes(rsaKey.getModulus())));
71-
k.setPublicExponent(Base64Url.encode(toIntegerBytes(rsaKey.getPublicExponent())));
7266

73-
return k;
67+
public String generateSignedDPoPProof(String jti, String htm, String htu, Long iat, JWSHeader jwsHeader, PrivateKey privateKey, String accessToken) {
68+
DPoP dpop = generateDPoP(jti, htm, htu, iat, accessToken);
69+
KeyWrapper keyWrapper = getKeyWrapper(jwsHeader, privateKey);
70+
return sign(jwsHeader, dpop, keyWrapper);
7471
}
7572

73+
private DPoP generateDPoP(String jti, String htm, String htu, Long iat, String accessToken) {
74+
DPoP dpop = new DPoP();
75+
dpop.id(jti);
76+
dpop.setHttpMethod(htm);
77+
dpop.setHttpUri(htu);
78+
dpop.iat(iat);
79+
if (accessToken != null) {
80+
dpop.setAccessTokenHash(HashUtils.accessTokenHash(OAuth2Constants.DPOP_DEFAULT_ALGORITHM.toString(), accessToken, true));
81+
}
82+
return dpop;
83+
}
7684

77-
public static String generateSignedDPoPProof(String jti, String htm, String htu, Long iat, JWSHeader jwsHeader, PrivateKey privateKey, String accessToken) {
78-
try {
79-
DPoP dpop = new DPoP();
80-
dpop.id(jti);
81-
dpop.setHttpMethod(htm);
82-
dpop.setHttpUri(htu);
83-
dpop.iat(iat);
84-
if (accessToken != null) {
85-
dpop.setAccessTokenHash(HashUtils.accessTokenHash(OAuth2Constants.DPOP_DEFAULT_ALGORITHM.toString(), accessToken, true));
86-
}
85+
protected KeyWrapper getKeyWrapper(JWSHeader jwsHeader, PrivateKey privateKey) {
86+
JWK jwkKey = jwsHeader.getKey();
87+
if (jwkKey == null) {
88+
throw new IllegalArgumentException("The JWSHeader does not have key in the 'jwk' claim");
89+
}
90+
KeyWrapper keyWrapper = JWKSUtils.getKeyWrapper(jwkKey, true);
91+
keyWrapper.setPrivateKey(privateKey);
92+
return keyWrapper;
93+
}
8794

88-
KeyWrapper keyWrapper = new KeyWrapper();
89-
keyWrapper.setKid(jwsHeader.getKeyId());
90-
keyWrapper.setAlgorithm(jwsHeader.getAlgorithm().toString());
91-
keyWrapper.setPrivateKey(privateKey);
92-
keyWrapper.setType(privateKey.getAlgorithm());
93-
keyWrapper.setUse(KeyUse.SIG);
94-
SignatureSignerContext sigCtx = createSignatureSignerContext(keyWrapper);
95+
private String sign(JWSHeader jwsHeader, DPoP dpop, KeyWrapper keyWrapper) {
96+
SignatureSignerContext sigCtx = createSignatureSignerContext(keyWrapper);
9597

96-
return new JWSBuilder()
97-
.header(jwsHeader)
98-
.jsonContent(dpop)
99-
.sign(sigCtx);
100-
} catch (SignatureException e) {
101-
throw new RuntimeException(e);
102-
}
98+
return new JWSBuilder()
99+
.header(jwsHeader)
100+
.jsonContent(dpop)
101+
.sign(sigCtx);
103102
}
104103

105-
private static SignatureSignerContext createSignatureSignerContext(KeyWrapper keyWrapper) {
104+
private SignatureSignerContext createSignatureSignerContext(KeyWrapper keyWrapper) {
106105
switch (keyWrapper.getType()) {
107-
case KeyType.RSA:
108-
return new AsymmetricSignatureSignerContext(keyWrapper);
109106
case KeyType.EC:
110107
return new ECDSASignatureSignerContext(keyWrapper);
111-
// TODO: EdDSA?
108+
case KeyType.RSA:
109+
case KeyType.OKP:
110+
return new AsymmetricSignatureSignerContext(keyWrapper);
112111
default:
113112
throw new IllegalArgumentException("No signer provider for key algorithm type " + keyWrapper.getType());
114113
}

core/src/main/java/org/keycloak/util/JWKSUtils.java

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
package org.keycloak.util;
1919

20-
import com.fasterxml.jackson.databind.JsonNode;
2120
import org.jboss.logging.Logger;
2221
import org.keycloak.crypto.KeyUse;
2322
import org.keycloak.crypto.KeyWrapper;
@@ -34,7 +33,6 @@
3433

3534
import java.io.IOException;
3635
import java.security.PublicKey;
37-
import java.security.interfaces.ECPublicKey;
3836
import java.util.ArrayList;
3937
import java.util.HashMap;
4038
import java.util.List;
@@ -55,6 +53,7 @@ public class JWKSUtils {
5553
static {
5654
JWK_THUMBPRINT_REQUIRED_MEMBERS.put(KeyType.RSA, new String[] { RSAPublicJWK.MODULUS, RSAPublicJWK.PUBLIC_EXPONENT });
5755
JWK_THUMBPRINT_REQUIRED_MEMBERS.put(KeyType.EC, new String[] { ECPublicJWK.CRV, ECPublicJWK.X, ECPublicJWK.Y });
56+
JWK_THUMBPRINT_REQUIRED_MEMBERS.put(KeyType.OKP, new String[] { OKPPublicJWK.CRV, OKPPublicJWK.X });
5857
}
5958

6059
/**
@@ -80,7 +79,7 @@ public static PublicKeysWrapper getKeyWrappersForUse(JSONWebKeySet keySet, JWK.U
8079
} else if ((requestedUse.asString().equals(jwk.getPublicKeyUse()) || (jwk.getPublicKeyUse() == null && useRequestedUseWhenNull))
8180
&& parser.isKeyTypeSupported(jwk.getKeyType())) {
8281
try {
83-
KeyWrapper keyWrapper = wrap(jwk, parser);
82+
KeyWrapper keyWrapper = wrap(jwk, parser, false);
8483
keyWrapper.setUse(getKeyUse(requestedUse.asString()));
8584
result.add(keyWrapper);
8685
} catch (RuntimeException e) {
@@ -118,26 +117,32 @@ public static JWK getKeyForUse(JSONWebKeySet keySet, JWK.Use requestedUse) {
118117
}
119118

120119
public static KeyWrapper getKeyWrapper(JWK jwk) {
120+
return getKeyWrapper(jwk, false);
121+
}
122+
123+
public static KeyWrapper getKeyWrapper(JWK jwk, boolean skipPublicKey) {
121124
JWKParser parser = JWKParser.create(jwk);
122125
if (parser.isKeyTypeSupported(jwk.getKeyType())) {
123-
return wrap(jwk, parser);
126+
return wrap(jwk, parser, skipPublicKey);
124127
} else {
125128
return null;
126129
}
127130
}
128131

129-
private static KeyWrapper wrap(JWK jwk, JWKParser parser) {
132+
private static KeyWrapper wrap(JWK jwk, JWKParser parser, boolean skipPublicKey) {
130133
KeyWrapper keyWrapper = new KeyWrapper();
131134
keyWrapper.setKid(jwk.getKeyId());
132135
if (jwk.getAlgorithm() != null) {
133136
keyWrapper.setAlgorithm(jwk.getAlgorithm());
134137
}
135-
if (jwk.getOtherClaims().get(OKPPublicJWK.CRV) != null) {
136-
keyWrapper.setCurve((String) jwk.getOtherClaims().get(OKPPublicJWK.CRV));
138+
if (jwk.getOtherClaim(OKPPublicJWK.CRV, String.class) != null) {
139+
keyWrapper.setCurve(jwk.getOtherClaim(OKPPublicJWK.CRV, String.class));
137140
}
138141
keyWrapper.setType(jwk.getKeyType());
139142
keyWrapper.setUse(getKeyUse(jwk.getPublicKeyUse()));
140-
keyWrapper.setPublicKey(parser.toPublicKey());
143+
if (!skipPublicKey) {
144+
keyWrapper.setPublicKey(parser.toPublicKey());
145+
}
141146
return keyWrapper;
142147
}
143148

@@ -160,9 +165,8 @@ public static String computeThumbprint(JWK key, String hashAlg) {
160165
members.put(JWK.KEY_TYPE, kty);
161166

162167
try {
163-
JsonNode node = JsonSerialization.writeValueAsNode(key);
164168
for (String member : requiredMembers) {
165-
members.put(member, node.get(member).asText());
169+
members.put(member, key.getOtherClaim(member, String.class));
166170
}
167171

168172
byte[] bytes = JsonSerialization.writeValueAsBytes(members);

core/src/main/java15/org/keycloak/jose/jwk/EdECUtilsImpl.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ public JWK okp(String kid, String algorithm, Key key, KeyUse keyUse) {
7070

7171
@Override
7272
public PublicKey createOKPPublicKey(JWK jwk) {
73-
String x = (String) jwk.getOtherClaims().get(OKPPublicJWK.X);
74-
String crv = (String) jwk.getOtherClaims().get(OKPPublicJWK.CRV);
73+
String x = jwk.getOtherClaim(OKPPublicJWK.X, String.class);
74+
String crv = jwk.getOtherClaim(OKPPublicJWK.CRV, String.class);
7575
// JWK representation "x" of a public key
7676
int bytesLength = 0;
7777
if (Algorithm.Ed25519.equals(crv)) {

services/src/main/java/org/keycloak/keys/AbstractEddsaKeyProviderFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public abstract class AbstractEddsaKeyProviderFactory implements KeyProviderFact
3838
protected static final String EDDSA_PRIVATE_KEY_KEY = "eddsaPrivateKey";
3939
protected static final String EDDSA_PUBLIC_KEY_KEY = "eddsaPublicKey";
4040
protected static final String EDDSA_ELLIPTIC_CURVE_KEY = "eddsaEllipticCurveKey";
41-
protected static final String DEFAULT_EDDSA_ELLIPTIC_CURVE = Algorithm.Ed25519;
41+
public static final String DEFAULT_EDDSA_ELLIPTIC_CURVE = Algorithm.Ed25519;
4242

4343
protected static ProviderConfigProperty EDDSA_ELLIPTIC_CURVE_PROPERTY = new ProviderConfigProperty(EDDSA_ELLIPTIC_CURVE_KEY,
4444
"Elliptic Curve", "Elliptic Curve used in EdDSA", LIST_TYPE,

services/src/main/java/org/keycloak/protocol/oid4vc/issuance/keybinding/AbstractProofValidator.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ private KeyWrapper getKeyWrapper(JWK jwk, String algorithm) {
5151
keyWrapper.setAlgorithm(algorithm);
5252

5353
// Set the curve if any
54-
if (jwk.getOtherClaims().get(OKPPublicJWK.CRV) != null) {
55-
keyWrapper.setCurve((String) jwk.getOtherClaims().get(OKPPublicJWK.CRV));
54+
if (jwk.getOtherClaim(OKPPublicJWK.CRV, String.class) != null) {
55+
keyWrapper.setCurve(jwk.getOtherClaim(OKPPublicJWK.CRV, String.class));
5656
}
5757

5858
JWKParser parser = JWKParser.create(jwk);

0 commit comments

Comments
 (0)