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;
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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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<X509Certificate> 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<X509Certificate> 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));

View file

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

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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<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 {
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();
}