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