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:
parent
4d8c525644
commit
b20123dcdc
3 changed files with 118 additions and 39 deletions
|
@ -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));
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue