Skip to content

Commit

Permalink
Support for setting a certificate chain header when building JWT (#748)
Browse files Browse the repository at this point in the history
  • Loading branch information
sberyozkin authored Nov 14, 2023
1 parent 771a5e0 commit a91cbc3
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.smallrye.jwt.build;

import java.security.cert.X509Certificate;
import java.util.List;

import io.smallrye.jwt.algorithm.SignatureAlgorithm;

Expand Down Expand Up @@ -81,6 +82,24 @@ default JwtSignatureBuilder signatureKeyId(String keyId) {
*/
JwtSignatureBuilder thumbprintS256(X509Certificate cert);

/**
* Set X.509 Certificate 'x5c' chain.
*
* @param cert the certificate
* @return JwtSignatureBuilder
*/
default JwtSignatureBuilder chain(X509Certificate cert) {
return chain(List.of(cert));
}

/**
* Set X.509 Certificate 'x5c' chain.
*
* @param chain the certificate chain
* @return JwtSignatureBuilder
*/
JwtSignatureBuilder chain(List<X509Certificate> chain);

/**
* Custom JWT signature header.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package io.smallrye.jwt.build.impl;

import java.security.PrivateKey;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
Expand All @@ -19,8 +22,10 @@
import jakarta.json.JsonValue;

import org.eclipse.microprofile.jwt.Claims;
import org.jose4j.base64url.Base64;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.jwt.NumericDate;
import org.jose4j.jwx.HeaderParameterNames;
import org.jose4j.keys.X509Util;

import io.smallrye.jwt.algorithm.SignatureAlgorithm;
Expand Down Expand Up @@ -205,7 +210,7 @@ public JwtSignatureBuilder header(String name, Object value) {
*/
@Override
public JwtSignatureBuilder algorithm(SignatureAlgorithm algorithm) {
headers.put("alg", algorithm.name());
headers.put(HeaderParameterNames.ALGORITHM, algorithm.name());
return this;
}

Expand All @@ -214,7 +219,7 @@ public JwtSignatureBuilder algorithm(SignatureAlgorithm algorithm) {
*/
@Override
public JwtSignatureBuilder keyId(String keyId) {
headers.put("kid", keyId);
headers.put(HeaderParameterNames.KEY_ID, keyId);
return this;
}

Expand All @@ -223,7 +228,7 @@ public JwtSignatureBuilder keyId(String keyId) {
*/
@Override
public JwtSignatureBuilder thumbprint(X509Certificate cert) {
headers.put("x5t", X509Util.x5t(cert));
headers.put(HeaderParameterNames.X509_CERTIFICATE_THUMBPRINT, X509Util.x5t(cert));
return this;
}

Expand All @@ -232,7 +237,24 @@ public JwtSignatureBuilder thumbprint(X509Certificate cert) {
*/
@Override
public JwtSignatureBuilder thumbprintS256(X509Certificate cert) {
headers.put("x5t#S256", X509Util.x5tS256(cert));
headers.put(HeaderParameterNames.X509_CERTIFICATE_SHA256_THUMBPRINT, X509Util.x5tS256(cert));
return this;
}

/**
* {@inheritDoc}
*/
@Override
public JwtSignatureBuilder chain(List<X509Certificate> chain) {
List<String> base64EncodedCerts = new ArrayList<>(chain.size());
try {
for (X509Certificate cert : chain) {
base64EncodedCerts.add(Base64.encode(cert.getEncoded()));
}
headers.put(HeaderParameterNames.X509_CERTIFICATE_CHAIN, base64EncodedCerts);
} catch (CertificateEncodingException ex) {
throw ImplMessages.msg.signatureException(ex);
}
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
Expand Down Expand Up @@ -64,13 +65,19 @@
import org.jose4j.jwt.MalformedClaimException;
import org.jose4j.jwt.NumericDate;
import org.jose4j.jwt.consumer.InvalidJwtSignatureException;
import org.jose4j.jwt.consumer.JwtConsumer;
import org.jose4j.jwt.consumer.JwtConsumerBuilder;
import org.jose4j.jwx.JsonWebStructure;
import org.jose4j.keys.EdDsaKeyUtil;
import org.jose4j.keys.EllipticCurves;
import org.jose4j.keys.resolvers.VerificationKeyResolver;
import org.jose4j.lang.JoseException;
import org.jose4j.lang.UnresolvableKeyException;
import org.junit.jupiter.api.Test;

import io.smallrye.jwt.algorithm.SignatureAlgorithm;
import io.smallrye.jwt.util.KeyUtils;
import io.smallrye.jwt.util.ResourceUtils;

class JwtSignTest {
@Test
Expand Down Expand Up @@ -885,6 +892,58 @@ void wrongKeyForRSAAlgorithm() throws Exception {
}
}

@Test
void testCertificateChainHeader() throws Exception {
X509Certificate cert = KeyUtils.getCertificate(ResourceUtils.readResource("/certificate.pem"));
String jwtString = Jwt.upn("Alice")
.jws().chain(cert)
.sign("/privateKey2.pem");

JwtConsumerBuilder builder = new JwtConsumerBuilder();
builder.setVerificationKeyResolver(new VerificationKeyResolver() {

@Override
public Key resolveKey(JsonWebSignature jws, List<JsonWebStructure> nestingContext)
throws UnresolvableKeyException {
try {
return jws.getCertificateChainHeaderValue().get(0).getPublicKey();
} catch (JoseException ex) {
throw new UnresolvableKeyException("Invalid chain", ex);
}
}
});
builder.setJwsAlgorithmConstraints(AlgorithmConstraints.ConstraintType.PERMIT, "RS256");
JwtClaims jwt = builder.build().process(jwtString).getJwtClaims();

assertEquals("Alice", jwt.getClaimValueAsString("upn"));
}

@Test
void testInvalidCertificateChainHeader() throws Exception {
X509Certificate cert = KeyUtils.getCertificate(ResourceUtils.readResource("/certificate.pem"));
String jwtString = Jwt.upn("Alice")
.jws().chain(cert)
// this key does not correspond to the public key in the loaded certificate
.sign("/privateKey.pem");

JwtConsumerBuilder builder = new JwtConsumerBuilder();
builder.setVerificationKeyResolver(new VerificationKeyResolver() {

@Override
public Key resolveKey(JsonWebSignature jws, List<JsonWebStructure> nestingContext)
throws UnresolvableKeyException {
try {
return jws.getCertificateChainHeaderValue().get(0).getPublicKey();
} catch (JoseException ex) {
throw new UnresolvableKeyException("Invalid chain", ex);
}
}
});
builder.setJwsAlgorithmConstraints(AlgorithmConstraints.ConstraintType.PERMIT, "RS256");
JwtConsumer consumer = builder.build();
assertThrows(InvalidJwtSignatureException.class, () -> consumer.process(jwtString));
}

static Map<String, Object> getJwsHeaders(String compactJws, int expectedSize) throws Exception {
int firstDot = compactJws.indexOf(".");
String headersJson = new Base64Url().base64UrlDecodeToUtf8String(compactJws.substring(0, firstDot));
Expand Down
18 changes: 18 additions & 0 deletions implementation/jwt-build/src/test/resources/certificate.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE-----
MIIC9DCCAdygAwIBAgIJAO0Y7B7dV9KpMA0GCSqGSIb3DQEBCwUAMA8xDTALBgNV
BAMMBHRlc3QwHhcNMjAwODI1MTIzOTA1WhcNMjEwODI1MTIzOTA1WjAPMQ0wCwYD
VQQDDAR0ZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsl8Cqii/
4UFg5Lq3R8kZ//Wmapq4KQn4k+foEOymfUbw44E6pCU+iCK0RbyKXgTMErMN3ZFD
xpoDdSeEDoS2kdlRF4XNtFG8RW6+h1wLJGRj7gi9bc0Vg5CzTGWhDvI6oT23KtUa
OBjIWknZtLAR5nEJ7vMADq3QKMHcxofx1GmmAQ2NDmVQJvTfM8wV02oZ2vX6yQjB
6t3vbMyIr+h2GU8teu9v/oUf9A9R2Pm6qULSZ80qyo5BXlwwG2D4HsGCxCdg5PqK
Oi9SOkvlE65eBUR8NXxwHdou+SQ/ry//MPwLSpHo9AggVEmSYlfigyVo5w1FUVo6
uYUG/abuKhCSoQIDAQABo1MwUTAdBgNVHQ4EFgQUcDgMETNCuUuEGWzKoXjrvS8+
kQAwHwYDVR0jBBgwFoAUcDgMETNCuUuEGWzKoXjrvS8+kQAwDwYDVR0TAQH/BAUw
AwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAH0o4kU37vc7OR/+fPQJdrBQqQaukq5ri
yuMSTrU1vSizEXpd0RFE8moSks6+Vh4mEzU7zLAU54N6glcGItEmBko5xgTuQRgd
b0LtR7Bu28QKfRRJxZ9GnGxinDPtFPD+7oTXZfI2Ed/RAuVlbppEBr2Pr2eO9B1x
fgYJNwA1K/XA38G7njxQ/wcpgOp/iFdV7dyR6CyAtwkD92sMnEZKPVz3trBT9DFM
5mynDFn9PHYVB7R5mUjIc7C9yskHGIsqqSBAfsxUqKfCS2NlWjn4+JZ8BYPLgy6X
hlGxh5zhvJPoT6J9+gA5l1Z2xMXI08ym8quIIUJNoYovSf8x4bXVjQ==
-----END CERTIFICATE-----
28 changes: 28 additions & 0 deletions implementation/jwt-build/src/test/resources/privateKey2.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCyXwKqKL/hQWDk
urdHyRn/9aZqmrgpCfiT5+gQ7KZ9RvDjgTqkJT6IIrRFvIpeBMwSsw3dkUPGmgN1
J4QOhLaR2VEXhc20UbxFbr6HXAskZGPuCL1tzRWDkLNMZaEO8jqhPbcq1Ro4GMha
Sdm0sBHmcQnu8wAOrdAowdzGh/HUaaYBDY0OZVAm9N8zzBXTahna9frJCMHq3e9s
zIiv6HYZTy1672/+hR/0D1HY+bqpQtJnzSrKjkFeXDAbYPgewYLEJ2Dk+oo6L1I6
S+UTrl4FRHw1fHAd2i75JD+vL/8w/AtKkej0CCBUSZJiV+KDJWjnDUVRWjq5hQb9
pu4qEJKhAgMBAAECggEAJvBs4X7B3MfsAiLszgQN4/3ZlZ4vI+5kUM2osMEo22J4
RgI5Lgpfa1LALhUp07qSXmauWTdUJ3AJ3zKANrcsMAzUEiGItZu+UR4LA/vJBunP
kvBfgi/qSW12ZvAsx9mDiR2y9evNrH9khalnmHVzgu4ccAimc43oSm1/5+tXlLoZ
1QK/FohxBxAshtuDHGs8yKUL0jpv7dOrjhCj2ibmPYe6AUk9F61sVWO0/i0Q8UAO
cYT3L5nCS5WnLhdCdYpIJJ7xl2PrVE/BAD+JEG5uCOYfVeYh+iCZVfpX17ryfNNU
aBtyxKEGVtHbje3mO86mYN3noaS0w/zpUjBPgV+KEQKBgQDsp6VTmDIqHFTp2cC2
yrDMxRznif92EGv7ccJDZtbTC37mAuf2J7x5b6AiE1EfxEXyGYzSk99sCns+GbL1
EHABUt5pimDCl33b6XvuccQNpnJ0MfM5eRX9Ogyt/OKdDRnQsvrTPNCWOyJjvG01
HQM4mfxaBBnxnvl5meH2pyG/ZQKBgQDA87DnyqEFhTDLX5c1TtwHSRj2xeTPGKG0
GyxOJXcxR8nhtY9ee0kyLZ14RytnOxKarCFgYXeG4IoGEc/I42WbA4sq88tZcbe4
IJkdX0WLMqOTdMrdx9hMU1ytKVUglUJZBVm7FaTQjA+ArMwqkXAA5HBMtArUsfJK
Ut3l0hMIjQKBgQDS1vmAZJQs2Fj+jzYWpLaneOWrk1K5yR+rQUql6jVyiUdhfS1U
LUrJlh3Avh0EhEUc0I6Z/YyMITpztUmu9BoV09K7jMFwHK/RAU+cvFbDIovN4cKk
bbCdjt5FFIyBB278dLjrAb+EWOLmoLVbIKICB47AU+8ZSV1SbTrYGUcD0QKBgQCA
liZv4na6sg9ZiUPAr+QsKserNSiN5zFkULOPBKLRQbFFbPS1l12pRgLqNCu1qQV1
9H5tt6arSRpSfy5FB14gFxV4s23yFrnDyF2h2GsFH+MpEq1bbaI1A10AvUnQ5AeK
QemRpxPmM2DldMK/H5tPzO0WAOoy4r/ATkc4sG4kxQKBgBL9neT0TmJtxlYGzjNc
jdJXs3Q91+nZt3DRMGT9s0917SuP77+FdJYocDiH1rVa9sGG8rkh1jTdqliAxDXw
Im5IGS/0OBnkaN1nnGDk5yTiYxOutC5NSj7ecI5Erud8swW6iGqgz2ioFpGxxIYq
RlgTv/6mVt41KALfKrYIkVLw
-----END PRIVATE KEY-----

0 comments on commit a91cbc3

Please sign in to comment.