From a91cbc3074fe06a14830d321e1804461efc5faa3 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Tue, 14 Nov 2023 23:53:20 +0000 Subject: [PATCH] Support for setting a certificate chain header when building JWT (#748) --- .../jwt/build/JwtSignatureBuilder.java | 19 ++++++ .../jwt/build/impl/JwtClaimsBuilderImpl.java | 30 ++++++++-- .../io/smallrye/jwt/build/JwtSignTest.java | 59 +++++++++++++++++++ .../src/test/resources/certificate.pem | 18 ++++++ .../src/test/resources/privateKey2.pem | 28 +++++++++ 5 files changed, 150 insertions(+), 4 deletions(-) create mode 100644 implementation/jwt-build/src/test/resources/certificate.pem create mode 100644 implementation/jwt-build/src/test/resources/privateKey2.pem diff --git a/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/JwtSignatureBuilder.java b/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/JwtSignatureBuilder.java index 3028b84d..86fca2c5 100644 --- a/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/JwtSignatureBuilder.java +++ b/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/JwtSignatureBuilder.java @@ -1,6 +1,7 @@ package io.smallrye.jwt.build; import java.security.cert.X509Certificate; +import java.util.List; import io.smallrye.jwt.algorithm.SignatureAlgorithm; @@ -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 chain); + /** * Custom JWT signature header. * diff --git a/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/impl/JwtClaimsBuilderImpl.java b/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/impl/JwtClaimsBuilderImpl.java index 124d102a..bffdfcfd 100644 --- a/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/impl/JwtClaimsBuilderImpl.java +++ b/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/impl/JwtClaimsBuilderImpl.java @@ -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; @@ -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; @@ -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; } @@ -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; } @@ -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; } @@ -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 chain) { + List 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; } diff --git a/implementation/jwt-build/src/test/java/io/smallrye/jwt/build/JwtSignTest.java b/implementation/jwt-build/src/test/java/io/smallrye/jwt/build/JwtSignTest.java index 9904e6a0..d9af4e0b 100644 --- a/implementation/jwt-build/src/test/java/io/smallrye/jwt/build/JwtSignTest.java +++ b/implementation/jwt-build/src/test/java/io/smallrye/jwt/build/JwtSignTest.java @@ -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; @@ -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 @@ -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 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 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 getJwsHeaders(String compactJws, int expectedSize) throws Exception { int firstDot = compactJws.indexOf("."); String headersJson = new Base64Url().base64UrlDecodeToUtf8String(compactJws.substring(0, firstDot)); diff --git a/implementation/jwt-build/src/test/resources/certificate.pem b/implementation/jwt-build/src/test/resources/certificate.pem new file mode 100644 index 00000000..ba4f5987 --- /dev/null +++ b/implementation/jwt-build/src/test/resources/certificate.pem @@ -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----- diff --git a/implementation/jwt-build/src/test/resources/privateKey2.pem b/implementation/jwt-build/src/test/resources/privateKey2.pem new file mode 100644 index 00000000..6a16774c --- /dev/null +++ b/implementation/jwt-build/src/test/resources/privateKey2.pem @@ -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-----