fixes incorrect JWK thumprint computation

Closes #38394

Signed-off-by: Thomas Richner <thomas.richner@oviva.com>
This commit is contained in:
Thomas Richner 2025-03-24 18:35:22 +01:00 committed by Marek Posolda
parent e180a00229
commit 9920aa248e
3 changed files with 52 additions and 5 deletions

View File

@ -17,6 +17,7 @@
package org.keycloak.util;
import com.fasterxml.jackson.databind.JsonNode;
import org.jboss.logging.Logger;
import org.keycloak.crypto.KeyUse;
import org.keycloak.crypto.KeyWrapper;
@ -33,6 +34,7 @@ import org.keycloak.jose.jws.crypto.HashUtils;
import java.io.IOException;
import java.security.PublicKey;
import java.security.interfaces.ECPublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ -144,20 +146,30 @@ public class JWKSUtils {
}
// TreeMap uses the natural ordering of the keys.
// Therefore, it follows the way of hash value calculation for a public key defined by RFC 7678
// Therefore, it follows the way of hash value calculation for a public key defined by RFC 7638
public static String computeThumbprint(JWK key, String hashAlg) {
Map<String, String> members = new TreeMap<>();
members.put(JWK.KEY_TYPE, key.getKeyType());
String kty = key.getKeyType();
String[] requiredMembers = JWK_THUMBPRINT_REQUIRED_MEMBERS.get(kty);
for (String member : JWK_THUMBPRINT_REQUIRED_MEMBERS.get(key.getKeyType())) {
members.put(member, (String) key.getOtherClaims().get(member));
// e.g. `oct`, see RFC 7638 Section 3.2
if (requiredMembers == null) {
throw new UnsupportedOperationException("Unsupported key type: " + kty);
}
Map<String, String> members = new TreeMap<>();
members.put(JWK.KEY_TYPE, kty);
try {
JsonNode node = JsonSerialization.writeValueAsNode(key);
for (String member : requiredMembers) {
members.put(member, node.get(member).asText());
}
byte[] bytes = JsonSerialization.writeValueAsBytes(members);
byte[] hash = HashUtils.hash(hashAlg, bytes);
return Base64Url.encode(hash);
} catch (IOException ex) {
logger.debugf(ex, "Failed to compute JWK thumbprint for key '%s'.", key.getKeyId());
return null;
}
}

View File

@ -70,6 +70,10 @@ public class JsonSerialization {
return mapper.writeValueAsBytes(obj);
}
public static JsonNode writeValueAsNode(Object obj) {
return mapper.valueToTree(obj);
}
public static <T> T readValue(byte[] bytes, Class<T> type) throws IOException {
return mapper.readValue(bytes, type);
}

View File

@ -22,6 +22,7 @@ import org.junit.Test;
import org.keycloak.crypto.KeyUse;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.crypto.PublicKeysWrapper;
import org.keycloak.jose.jwk.ECPublicJWK;
import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.rule.CryptoInitRule;
@ -29,6 +30,7 @@ import org.keycloak.rule.CryptoInitRule;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
public abstract class JWKSUtilsTest {
@ -36,6 +38,35 @@ public abstract class JWKSUtilsTest {
@ClassRule
public static CryptoInitRule cryptoInitRule = new CryptoInitRule();
@Test
public void publicEcMatches() throws Exception {
String keyA = "{" +
" \"kty\": \"EC\"," +
" \"use\": \"sig\"," +
" \"crv\": \"P-384\"," +
" \"kid\": \"key-a\"," +
" \"x\": \"KVZ5h_W0-8fXmUrxmyRpO_9vwwI7urXfyxGdxm1hpEuhPj2hhDxivnb2BhNvtC6O\"," +
" \"y\": \"1J3JVw_zR3uB3biAE7fs3V_4tJy2M1JinzWj9a4je5GSoW6zgGV4bk85OcuyUAhj\"," +
" \"alg\": \"ES384\"" +
" }";
ECPublicJWK ecPublicKey = JsonSerialization.readValue(keyA, ECPublicJWK.class);
JWK publicKey = JsonSerialization.readValue(keyA, JWK.class);
assertEquals(JWKSUtils.computeThumbprint(publicKey), JWKSUtils.computeThumbprint(ecPublicKey));
}
@Test
public void unsupportedKeyType() throws Exception {
String keyA = "{" +
" \"kty\": \"OCT\"," +
" \"use\": \"sig\"" +
" }";
JWK publicKey = JsonSerialization.readValue(keyA, JWK.class);
assertThrows(UnsupportedOperationException.class, () -> JWKSUtils.computeThumbprint(publicKey));
}
@Test
public void publicRs256() throws Exception {