From b20123dcdcabab33f8eba58bf42d91aa29aa06f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Kn=C3=BCppel?= Date: Fri, 26 Jul 2024 20:42:02 +0200 Subject: [PATCH] Add x5c and jwk as optional params to JWSBuilder and JWSHeader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pascal Knüppel --- .../org/keycloak/jose/jws/JWSBuilder.java | 51 ++++++++-- .../java/org/keycloak/jose/jws/JWSHeader.java | 8 ++ .../java/org/keycloak/RSAVerifierTest.java | 98 ++++++++++++------- 3 files changed, 118 insertions(+), 39 deletions(-) diff --git a/core/src/main/java/org/keycloak/jose/jws/JWSBuilder.java b/core/src/main/java/org/keycloak/jose/jws/JWSBuilder.java index c96fc81f0b9..bf13bc76162 100755 --- a/core/src/main/java/org/keycloak/jose/jws/JWSBuilder.java +++ b/core/src/main/java/org/keycloak/jose/jws/JWSBuilder.java @@ -17,9 +17,11 @@ package org.keycloak.jose.jws; +import com.fasterxml.jackson.core.JsonProcessingException; +import org.keycloak.common.util.Base64; import org.keycloak.common.util.Base64Url; -import org.keycloak.crypto.JavaAlgorithm; import org.keycloak.crypto.SignatureSignerContext; +import org.keycloak.jose.jwk.JWK; import org.keycloak.jose.jws.crypto.HMACProvider; import org.keycloak.jose.jws.crypto.RSAProvider; import org.keycloak.util.JsonSerialization; @@ -28,17 +30,22 @@ import javax.crypto.SecretKey; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.PrivateKey; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.List; /** * @author Bill Burke * @version $Revision: 1 $ */ public class JWSBuilder { - String type; - String kid; - String x5t; - String contentType; - byte[] contentBytes; + protected String type; + protected String kid; + protected String x5t; + protected JWK jwk; + protected List x5c; + protected String contentType; + protected byte[] contentBytes; public JWSBuilder type(String type) { this.type = type; @@ -55,6 +62,16 @@ public class JWSBuilder { return this; } + public JWSBuilder jwk(JWK jwk) { + this.jwk = jwk; + return this; + } + + public JWSBuilder x5c(List x5c) { + this.x5c = x5c; + return this; + } + public JWSBuilder contentType(String type) { this.contentType = type; return this; @@ -88,6 +105,28 @@ public class JWSBuilder { if (type != null) builder.append(",\"typ\" : \"").append(type).append("\""); if (kid != null) builder.append(",\"kid\" : \"").append(kid).append("\""); if (x5t != null) builder.append(",\"x5t\" : \"").append(x5t).append("\""); + if (x5c != null && !x5c.isEmpty()) { + builder.append(",\"x5c\" : ["); + for (int i = 0; i < x5c.size(); i++) { + X509Certificate certificate = x5c.get(i); + if (i > 0) { + builder.append(","); + } + try { + builder.append("\"").append(Base64.encodeBytes(certificate.getEncoded())).append("\""); + } catch (CertificateEncodingException e) { + throw new RuntimeException(e); + } + } + builder.append("]"); + } + if (jwk != null) { + try { + builder.append(",\"jwk\" : ").append(JsonSerialization.mapper.writeValueAsString(jwk)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } if (contentType != null) builder.append(",\"cty\":\"").append(contentType).append("\""); builder.append("}"); return Base64Url.encode(builder.toString().getBytes(StandardCharsets.UTF_8)); diff --git a/core/src/main/java/org/keycloak/jose/jws/JWSHeader.java b/core/src/main/java/org/keycloak/jose/jws/JWSHeader.java index 99045b15f82..16a1aa2e634 100755 --- a/core/src/main/java/org/keycloak/jose/jws/JWSHeader.java +++ b/core/src/main/java/org/keycloak/jose/jws/JWSHeader.java @@ -27,6 +27,7 @@ import org.keycloak.jose.JOSEHeader; import org.keycloak.jose.jwk.JWK; import java.io.IOException; +import java.util.List; /** * @author Bill Burke @@ -49,6 +50,9 @@ public class JWSHeader implements JOSEHeader { @JsonProperty("jwk") private JWK key; + @JsonProperty("x5c") + private List x5c; + public JWSHeader() { } @@ -91,6 +95,10 @@ public class JWSHeader implements JOSEHeader { return key; } + public List getX5c() { + return x5c; + } + private static final ObjectMapper mapper = new ObjectMapper(); static { diff --git a/core/src/test/java/org/keycloak/RSAVerifierTest.java b/core/src/test/java/org/keycloak/RSAVerifierTest.java index 36edacbbfe9..f918bab9b55 100755 --- a/core/src/test/java/org/keycloak/RSAVerifierTest.java +++ b/core/src/test/java/org/keycloak/RSAVerifierTest.java @@ -22,22 +22,32 @@ import org.junit.Before; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; -import org.keycloak.common.util.Time; import org.keycloak.common.VerificationException; +import org.keycloak.common.util.Base64; +import org.keycloak.common.util.CertificateUtils; +import org.keycloak.common.util.Time; +import org.keycloak.jose.jwk.JWK; +import org.keycloak.jose.jwk.JWKBuilder; import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.representations.AccessToken; +import org.keycloak.representations.JsonWebToken; import org.keycloak.rule.CryptoInitRule; import org.keycloak.util.JsonSerialization; import org.keycloak.util.TokenUtil; import java.security.KeyPair; import java.security.KeyPairGenerator; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Map; /** * @author Bill Burke * @version $Revision: 1 $ */ public abstract class RSAVerifierTest { + @ClassRule + public static CryptoInitRule cryptoInitRule = new CryptoInitRule(); // private static X509Certificate[] idpCertificates; private static KeyPair idpPair; private static KeyPair badPair; @@ -45,13 +55,9 @@ public abstract class RSAVerifierTest { // private static X509Certificate[] clientCertificateChain; private AccessToken token; - @ClassRule - public static CryptoInitRule cryptoInitRule = new CryptoInitRule(); - @BeforeClass public static void setupCerts() - throws Exception - { + throws Exception { // CryptoIntegration.init(ClassLoader.getSystemClassLoader()); badPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); idpPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); @@ -62,12 +68,12 @@ public abstract class RSAVerifierTest { token = new AccessToken(); token.type(TokenUtil.TOKEN_TYPE_BEARER) - .subject("CN=Client") - .issuer("http://localhost:8080/auth/realm") - .addAccess("service").addRole("admin"); + .subject("CN=Client") + .issuer("http://localhost:8080/auth/realm") + .addAccess("service").addRole("admin"); } - @Test + @Test public void testSimpleVerification() throws Exception { String encoded = new JWSBuilder() .jsonContent(token) @@ -78,30 +84,56 @@ public abstract class RSAVerifierTest { Assert.assertEquals("CN=Client", token.getSubject()); } + @Test + public void testVerificationWithAddedX5cAndJwk() throws Exception { + KeyPair caKeyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); + X509Certificate caCertificate = CertificateUtils.generateV1SelfSignedCertificate(caKeyPair, "root"); + X509Certificate idpCertificate = CertificateUtils.generateV3Certificate(idpPair, + caKeyPair.getPrivate(), + caCertificate, + "idp"); + JWK jwk = JWKBuilder.create().rsa(idpPair.getPublic()); + + String encoded = new JWSBuilder() + .jwk(jwk) + .x5c(List.of(idpCertificate, caCertificate)) + .jsonContent(token) + .rsa256(idpPair.getPrivate()); + TokenVerifier tokenVerifier = TokenVerifier.create(encoded, JsonWebToken.class); + verifySkeletonKeyToken(encoded); + Assert.assertTrue(token.getResourceAccess("service").getRoles().contains("admin")); + Assert.assertEquals("CN=Client", token.getSubject()); + + List x5c = tokenVerifier.getHeader().getX5c(); + Assert.assertEquals(2, x5c.size()); + Assert.assertEquals(Base64.encodeBytes(idpCertificate.getEncoded()), x5c.get(0)); + Assert.assertEquals(Base64.encodeBytes(caCertificate.getEncoded()), x5c.get(1)); + Assert.assertEquals(JsonSerialization.mapper.convertValue(jwk, Map.class), + JsonSerialization.mapper.convertValue(tokenVerifier.getHeader().getKey(), Map.class)); + } + private AccessToken verifySkeletonKeyToken(String encoded) throws VerificationException { return RSATokenVerifier.verifyToken(encoded, idpPair.getPublic(), "http://localhost:8080/auth/realm"); } - // @Test - public void testSpeed() throws Exception - { - // Took 44 seconds with 50000 iterations - byte[] tokenBytes = JsonSerialization.writeValueAsBytes(token); + // @Test + public void testSpeed() throws Exception { + // Took 44 seconds with 50000 iterations + byte[] tokenBytes = JsonSerialization.writeValueAsBytes(token); - long start = System.currentTimeMillis(); - int count = 50000; - for (int i = 0; i < count; i++) - { - String encoded = new JWSBuilder() - .content(tokenBytes) - .rsa256(idpPair.getPrivate()); + long start = System.currentTimeMillis(); + int count = 50000; + for (int i = 0; i < count; i++) { + String encoded = new JWSBuilder() + .content(tokenBytes) + .rsa256(idpPair.getPrivate()); - verifySkeletonKeyToken(encoded); + verifySkeletonKeyToken(encoded); - } - long end = System.currentTimeMillis() - start; - System.out.println("took: " + end); - } + } + long end = System.currentTimeMillis() - start; + System.out.println("took: " + end); + } @Test public void testBadSignature() { @@ -187,8 +219,8 @@ public abstract class RSAVerifierTest { public void testTokenAuth() { token = new AccessToken(); token.subject("CN=Client") - .issuer("http://localhost:8080/auth/realms/demo") - .addAccess("service").addRole("admin").verifyCaller(true); + .issuer("http://localhost:8080/auth/realms/demo") + .addAccess("service").addRole("admin").verifyCaller(true); token.setEmail("bill@jboss.org"); String encoded = new JWSBuilder() @@ -234,10 +266,10 @@ public abstract class RSAVerifierTest { private void verifyAudience(String encodedToken, String expectedAudience) throws VerificationException { TokenVerifier.create(encodedToken, AccessToken.class) - .publicKey(idpPair.getPublic()) - .realmUrl("http://localhost:8080/auth/realm") - .audience(expectedAudience) - .verify(); + .publicKey(idpPair.getPublic()) + .realmUrl("http://localhost:8080/auth/realm") + .audience(expectedAudience) + .verify(); }