Add x5c and jwk as optional params to JWSBuilder and JWSHeader

Signed-off-by: Pascal Knüppel <pascal.knueppel@governikus.de>
This commit is contained in:
Pascal Knüppel 2024-07-26 20:42:02 +02:00 committed by Marek Posolda
parent 4d8c525644
commit b20123dcdc
3 changed files with 118 additions and 39 deletions

View file

@ -17,9 +17,11 @@
package org.keycloak.jose.jws; 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.common.util.Base64Url;
import org.keycloak.crypto.JavaAlgorithm;
import org.keycloak.crypto.SignatureSignerContext; import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jws.crypto.HMACProvider; import org.keycloak.jose.jws.crypto.HMACProvider;
import org.keycloak.jose.jws.crypto.RSAProvider; import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
@ -28,17 +30,22 @@ import javax.crypto.SecretKey;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.List;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class JWSBuilder { public class JWSBuilder {
String type; protected String type;
String kid; protected String kid;
String x5t; protected String x5t;
String contentType; protected JWK jwk;
byte[] contentBytes; protected List<X509Certificate> x5c;
protected String contentType;
protected byte[] contentBytes;
public JWSBuilder type(String type) { public JWSBuilder type(String type) {
this.type = type; this.type = type;
@ -55,6 +62,16 @@ public class JWSBuilder {
return this; return this;
} }
public JWSBuilder jwk(JWK jwk) {
this.jwk = jwk;
return this;
}
public JWSBuilder x5c(List<X509Certificate> x5c) {
this.x5c = x5c;
return this;
}
public JWSBuilder contentType(String type) { public JWSBuilder contentType(String type) {
this.contentType = type; this.contentType = type;
return this; return this;
@ -88,6 +105,28 @@ public class JWSBuilder {
if (type != null) builder.append(",\"typ\" : \"").append(type).append("\""); if (type != null) builder.append(",\"typ\" : \"").append(type).append("\"");
if (kid != null) builder.append(",\"kid\" : \"").append(kid).append("\""); if (kid != null) builder.append(",\"kid\" : \"").append(kid).append("\"");
if (x5t != null) builder.append(",\"x5t\" : \"").append(x5t).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("\""); if (contentType != null) builder.append(",\"cty\":\"").append(contentType).append("\"");
builder.append("}"); builder.append("}");
return Base64Url.encode(builder.toString().getBytes(StandardCharsets.UTF_8)); return Base64Url.encode(builder.toString().getBytes(StandardCharsets.UTF_8));

View file

@ -27,6 +27,7 @@ import org.keycloak.jose.JOSEHeader;
import org.keycloak.jose.jwk.JWK; import org.keycloak.jose.jwk.JWK;
import java.io.IOException; import java.io.IOException;
import java.util.List;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -49,6 +50,9 @@ public class JWSHeader implements JOSEHeader {
@JsonProperty("jwk") @JsonProperty("jwk")
private JWK key; private JWK key;
@JsonProperty("x5c")
private List<String> x5c;
public JWSHeader() { public JWSHeader() {
} }
@ -91,6 +95,10 @@ public class JWSHeader implements JOSEHeader {
return key; return key;
} }
public List<String> getX5c() {
return x5c;
}
private static final ObjectMapper mapper = new ObjectMapper(); private static final ObjectMapper mapper = new ObjectMapper();
static { static {

View file

@ -22,22 +22,32 @@ import org.junit.Before;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.ClassRule; import org.junit.ClassRule;
import org.junit.Test; import org.junit.Test;
import org.keycloak.common.util.Time;
import org.keycloak.common.VerificationException; 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.jose.jws.JWSBuilder;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.rule.CryptoInitRule; import org.keycloak.rule.CryptoInitRule;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import org.keycloak.util.TokenUtil; import org.keycloak.util.TokenUtil;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.KeyPairGenerator; import java.security.KeyPairGenerator;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.Map;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public abstract class RSAVerifierTest { public abstract class RSAVerifierTest {
@ClassRule
public static CryptoInitRule cryptoInitRule = new CryptoInitRule();
// private static X509Certificate[] idpCertificates; // private static X509Certificate[] idpCertificates;
private static KeyPair idpPair; private static KeyPair idpPair;
private static KeyPair badPair; private static KeyPair badPair;
@ -45,13 +55,9 @@ public abstract class RSAVerifierTest {
// private static X509Certificate[] clientCertificateChain; // private static X509Certificate[] clientCertificateChain;
private AccessToken token; private AccessToken token;
@ClassRule
public static CryptoInitRule cryptoInitRule = new CryptoInitRule();
@BeforeClass @BeforeClass
public static void setupCerts() public static void setupCerts()
throws Exception throws Exception {
{
// CryptoIntegration.init(ClassLoader.getSystemClassLoader()); // CryptoIntegration.init(ClassLoader.getSystemClassLoader());
badPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); badPair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
idpPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); idpPair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
@ -62,12 +68,12 @@ public abstract class RSAVerifierTest {
token = new AccessToken(); token = new AccessToken();
token.type(TokenUtil.TOKEN_TYPE_BEARER) token.type(TokenUtil.TOKEN_TYPE_BEARER)
.subject("CN=Client") .subject("CN=Client")
.issuer("http://localhost:8080/auth/realm") .issuer("http://localhost:8080/auth/realm")
.addAccess("service").addRole("admin"); .addAccess("service").addRole("admin");
} }
@Test @Test
public void testSimpleVerification() throws Exception { public void testSimpleVerification() throws Exception {
String encoded = new JWSBuilder() String encoded = new JWSBuilder()
.jsonContent(token) .jsonContent(token)
@ -78,30 +84,56 @@ public abstract class RSAVerifierTest {
Assert.assertEquals("CN=Client", token.getSubject()); 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<String> 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 { private AccessToken verifySkeletonKeyToken(String encoded) throws VerificationException {
return RSATokenVerifier.verifyToken(encoded, idpPair.getPublic(), "http://localhost:8080/auth/realm"); return RSATokenVerifier.verifyToken(encoded, idpPair.getPublic(), "http://localhost:8080/auth/realm");
} }
// @Test // @Test
public void testSpeed() throws Exception public void testSpeed() throws Exception {
{ // Took 44 seconds with 50000 iterations
// Took 44 seconds with 50000 iterations byte[] tokenBytes = JsonSerialization.writeValueAsBytes(token);
byte[] tokenBytes = JsonSerialization.writeValueAsBytes(token);
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
int count = 50000; int count = 50000;
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++) {
{ String encoded = new JWSBuilder()
String encoded = new JWSBuilder() .content(tokenBytes)
.content(tokenBytes) .rsa256(idpPair.getPrivate());
.rsa256(idpPair.getPrivate());
verifySkeletonKeyToken(encoded); verifySkeletonKeyToken(encoded);
} }
long end = System.currentTimeMillis() - start; long end = System.currentTimeMillis() - start;
System.out.println("took: " + end); System.out.println("took: " + end);
} }
@Test @Test
public void testBadSignature() { public void testBadSignature() {
@ -187,8 +219,8 @@ public abstract class RSAVerifierTest {
public void testTokenAuth() { public void testTokenAuth() {
token = new AccessToken(); token = new AccessToken();
token.subject("CN=Client") token.subject("CN=Client")
.issuer("http://localhost:8080/auth/realms/demo") .issuer("http://localhost:8080/auth/realms/demo")
.addAccess("service").addRole("admin").verifyCaller(true); .addAccess("service").addRole("admin").verifyCaller(true);
token.setEmail("bill@jboss.org"); token.setEmail("bill@jboss.org");
String encoded = new JWSBuilder() String encoded = new JWSBuilder()
@ -234,10 +266,10 @@ public abstract class RSAVerifierTest {
private void verifyAudience(String encodedToken, String expectedAudience) throws VerificationException { private void verifyAudience(String encodedToken, String expectedAudience) throws VerificationException {
TokenVerifier.create(encodedToken, AccessToken.class) TokenVerifier.create(encodedToken, AccessToken.class)
.publicKey(idpPair.getPublic()) .publicKey(idpPair.getPublic())
.realmUrl("http://localhost:8080/auth/realm") .realmUrl("http://localhost:8080/auth/realm")
.audience(expectedAudience) .audience(expectedAudience)
.verify(); .verify();
} }