From 66f1236ef5d88708cb8caeec60d993d0ba537892 Mon Sep 17 00:00:00 2001 From: Colm O hEigeartaigh Date: Wed, 30 Sep 2026 17:31:59 +0100 Subject: [PATCH] CXF-9032 - Fix JWS verification with a "jwk" key store --- .../cxf/rs/security/jose/jwk/JwkUtils.java | 37 +++++- .../cxf/rs/security/jose/jws/JwsUtils.java | 9 +- .../rs/security/jose/jws/JwsUtilsTest.java | 112 ++++++++++++++++++ 3 files changed, 151 insertions(+), 7 deletions(-) diff --git a/rt/rs/security/jose-parent/jose/src/main/java/org/apache/cxf/rs/security/jose/jwk/JwkUtils.java b/rt/rs/security/jose-parent/jose/src/main/java/org/apache/cxf/rs/security/jose/jwk/JwkUtils.java index 16342a43665..67f24e86b60 100644 --- a/rt/rs/security/jose-parent/jose/src/main/java/org/apache/cxf/rs/security/jose/jwk/JwkUtils.java +++ b/rt/rs/security/jose-parent/jose/src/main/java/org/apache/cxf/rs/security/jose/jwk/JwkUtils.java @@ -296,12 +296,21 @@ public static JsonWebKey loadJsonWebKey(Message m, Properties props, KeyOperatio public static JsonWebKey loadJsonWebKey(Message m, Properties props, KeyOperation keyOper, String inHeaderKid) { PrivateKeyPasswordProvider cb = KeyManagementUtils.loadPasswordProvider(m, props, keyOper); JsonWebKeys jwkSet = loadJwkSet(m, props, cb); - final String kid; - if (inHeaderKid != null - && MessageUtils.getContextualBoolean(m, JoseConstants.RSSEC_ACCEPT_PUBLIC_KEY, false)) { - kid = inHeaderKid; - } else { - kid = KeyManagementUtils.getKeyId(m, props, JoseConstants.RSSEC_KEY_STORE_ALIAS, keyOper); + String kid = KeyManagementUtils.getKeyId(m, props, JoseConstants.RSSEC_KEY_STORE_ALIAS, keyOper); + // A configured alias pins the key, unless accepting public keys has been explicitly enabled, + // in which case the key id from the incoming headers selects the key as before. + // Otherwise the key id selects a key from the configured key set, which lets verification follow + // key rotation (e.g. of a JWKS published by an identity provider), but only a key explicitly + // marked for the requested operation, so that a key set containing other keys can't widen trust. + if (inHeaderKid != null) { + boolean acceptPublicKey = + MessageUtils.getContextualBoolean(m, JoseConstants.RSSEC_ACCEPT_PUBLIC_KEY, false); + if (acceptPublicKey || kid == null) { + JsonWebKey jwk = jwkSet.getKey(inHeaderKid); + if (jwk != null && (acceptPublicKey || isKeyMarkedFor(jwk, keyOper))) { + return jwk; + } + } } if (kid != null) { return jwkSet.getKey(kid); @@ -314,6 +323,22 @@ public static JsonWebKey loadJsonWebKey(Message m, Properties props, KeyOperatio return null; } + private static boolean isKeyMarkedFor(JsonWebKey jwk, KeyOperation keyOper) { + if (keyOper == null) { + return false; + } + List ops = jwk.getKeyOperation(); + if (ops != null) { + return ops.contains(keyOper); + } + PublicKeyUse use = jwk.getPublicKeyUse(); + if (use == null) { + return false; + } + boolean sigOper = keyOper == KeyOperation.SIGN || keyOper == KeyOperation.VERIFY; + return sigOper == (use == PublicKeyUse.SIGN); + } + public static List loadJsonWebKeys(Message m, Properties props, KeyOperation keyOper) { diff --git a/rt/rs/security/jose-parent/jose/src/main/java/org/apache/cxf/rs/security/jose/jws/JwsUtils.java b/rt/rs/security/jose-parent/jose/src/main/java/org/apache/cxf/rs/security/jose/jws/JwsUtils.java index 9aaf81e00e9..ba206f2b0d1 100644 --- a/rt/rs/security/jose-parent/jose/src/main/java/org/apache/cxf/rs/security/jose/jws/JwsUtils.java +++ b/rt/rs/security/jose-parent/jose/src/main/java/org/apache/cxf/rs/security/jose/jws/JwsUtils.java @@ -402,6 +402,8 @@ public static JwsSignatureVerifier loadSignatureVerifier(Message m, JwsHeaders inHeaders) { JwsSignatureVerifier theVerifier = null; String inHeaderKid = null; + boolean jwkStore = + JoseConstants.HEADER_JSON_WEB_KEY.equals(props.get(JoseConstants.RSSEC_KEY_STORE_TYPE)); if (inHeaders != null) { inHeaderKid = inHeaders.getKeyId(); //TODO: optionally validate inHeaders.getAlgorithm against a property in props @@ -413,6 +415,11 @@ public static JwsSignatureVerifier loadSignatureVerifier(Message m, } return getSignatureVerifier(publicJwk, inHeaders.getSignatureAlgorithm()); + } else if (jwkStore) { + // The x5c, x5t and x5t#S256 headers are resolved against a Java KeyStore, + // which can not be loaded when the configured store type is "jwk". + // Fall through to loading the verification key from the JWK set below. + LOG.fine("Ignoring X.509 headers as the configured key store type is jwk"); } else if (inHeaders.getHeader(JoseConstants.HEADER_X509_CHAIN) != null) { List chain = KeyManagementUtils.toX509CertificateChain(inHeaders.getX509Chain()); KeyManagementUtils.validateCertificateChain(props, chain); @@ -439,7 +446,7 @@ public static JwsSignatureVerifier loadSignatureVerifier(Message m, } } - if (JoseConstants.HEADER_JSON_WEB_KEY.equals(props.get(JoseConstants.RSSEC_KEY_STORE_TYPE))) { + if (jwkStore) { JsonWebKey jwk = JwkUtils.loadJsonWebKey(m, props, KeyOperation.VERIFY, inHeaderKid); if (jwk != null) { SignatureAlgorithm signatureAlgo = getSignatureAlgorithm(m, props, diff --git a/rt/rs/security/jose-parent/jose/src/test/java/org/apache/cxf/rs/security/jose/jws/JwsUtilsTest.java b/rt/rs/security/jose-parent/jose/src/test/java/org/apache/cxf/rs/security/jose/jws/JwsUtilsTest.java index 7565c83e99a..682b3b9a5ec 100644 --- a/rt/rs/security/jose-parent/jose/src/test/java/org/apache/cxf/rs/security/jose/jws/JwsUtilsTest.java +++ b/rt/rs/security/jose-parent/jose/src/test/java/org/apache/cxf/rs/security/jose/jws/JwsUtilsTest.java @@ -18,6 +18,10 @@ */ package org.apache.cxf.rs.security.jose.jws; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.interfaces.RSAPublicKey; +import java.util.Arrays; import java.util.List; import java.util.Properties; @@ -32,7 +36,10 @@ import org.apache.cxf.rs.security.jose.jwa.SignatureAlgorithm; import org.apache.cxf.rs.security.jose.jwk.JsonWebKey; import org.apache.cxf.rs.security.jose.jwk.JsonWebKeys; +import org.apache.cxf.rs.security.jose.jwk.JwkUtils; +import org.apache.cxf.rs.security.jose.jwk.KeyOperation; import org.apache.cxf.rs.security.jose.jwk.KeyType; +import org.apache.cxf.rs.security.jose.jwk.PublicKeyUse; import org.junit.Test; @@ -86,6 +93,22 @@ public void testLoadSignatureVerifierFromJKS() throws Exception { assertNotNull(jws); } @Test + public void testLoadSignatureVerifierFromJwkStoreWithX509Thumbprint() throws Exception { + Properties p = new Properties(); + p.put(JoseConstants.RSSEC_KEY_STORE_TYPE, "jwk"); + p.put(JoseConstants.RSSEC_KEY_STORE_FILE, "jwk/pubKeys.jwks"); + p.put(JoseConstants.RSSEC_KEY_STORE_ALIAS, "rsas001"); + p.put(JoseConstants.RSSEC_SIGNATURE_ALGORITHM, "RS256"); + + JwsHeaders headers = new JwsHeaders(SignatureAlgorithm.RS256); + headers.setX509Thumbprint("dGh1bWJwcmludA"); + assertNotNull(JwsUtils.loadSignatureVerifier(createMessage(), p, headers)); + + headers = new JwsHeaders(SignatureAlgorithm.RS256); + headers.setX509ThumbprintSHA256("dGh1bWJwcmludA"); + assertNotNull(JwsUtils.loadSignatureVerifier(createMessage(), p, headers)); + } + @Test public void testLoadSignatureVerifierFromProperties() throws Exception { JwsSignatureVerifier jws = JwsUtils.loadSignatureVerifier("classpath:/jws/signature.properties", null); assertEquals(SignatureAlgorithm.NONE, jws.getAlgorithm()); @@ -132,6 +155,95 @@ public void testLoadVerificationKeyWithCert() throws Exception { assertEquals(2, chain.size()); } + @Test + public void testLoadSignatureVerifierFromJwkStoreUsesHeaderKeyId() throws Exception { + KeyPair oldKey = createRsaKeyPair(); + KeyPair newKey = createRsaKeyPair(); + KeyPair encKey = createRsaKeyPair(); + KeyPair opsKey = createRsaKeyPair(); + KeyPair unmarkedKey = createRsaKeyPair(); + JsonWebKey opsJwk = createPublicJwk(opsKey, "key-ops", null); + opsJwk.setKeyOperation(Arrays.asList(KeyOperation.VERIFY)); + JsonWebKeys jwks = new JsonWebKeys(Arrays.asList( + createPublicJwk(oldKey, "key-old", PublicKeyUse.SIGN), + createPublicJwk(newKey, "key-new", PublicKeyUse.SIGN), + createPublicJwk(encKey, "key-enc", PublicKeyUse.ENCRYPT), + opsJwk, + createPublicJwk(unmarkedKey, "key-unmarked", null))); + + Properties p = new Properties(); + p.put(JoseConstants.RSSEC_KEY_STORE_TYPE, "jwk"); + p.put(JoseConstants.RSSEC_KEY_STORE_JWKSET, JwkUtils.jwkSetToJson(jwks)); + p.put(JoseConstants.RSSEC_SIGNATURE_ALGORITHM, "RS256"); + + // Without an alias, the key id in the headers selects the key, so a rotated key is picked up + assertTrue(verifyWithLoadedVerifier(p, sign(oldKey, "key-old"), false)); + assertTrue(verifyWithLoadedVerifier(p, sign(newKey, "key-new"), false)); + assertTrue(verifyWithLoadedVerifier(p, sign(opsKey, "key-ops"), false)); + // Only a key explicitly marked for signatures is selected by the key id, otherwise + // the single key marked for verification ("key-ops") is used + assertFalse(verifyWithLoadedVerifier(p, sign(encKey, "key-enc"), false)); + assertFalse(verifyWithLoadedVerifier(p, sign(unmarkedKey, "key-unmarked"), false)); + } + + @Test + public void testLoadSignatureVerifierFromJwkStoreAliasPinsKey() throws Exception { + KeyPair oldKey = createRsaKeyPair(); + KeyPair newKey = createRsaKeyPair(); + KeyPair unmarkedKey = createRsaKeyPair(); + JsonWebKeys jwks = new JsonWebKeys(Arrays.asList( + createPublicJwk(oldKey, "key-old", PublicKeyUse.SIGN), + createPublicJwk(newKey, "key-new", PublicKeyUse.SIGN), + createPublicJwk(unmarkedKey, "key-unmarked", null))); + + Properties p = new Properties(); + p.put(JoseConstants.RSSEC_KEY_STORE_TYPE, "jwk"); + p.put(JoseConstants.RSSEC_KEY_STORE_JWKSET, JwkUtils.jwkSetToJson(jwks)); + p.put(JoseConstants.RSSEC_KEY_STORE_ALIAS, "key-old"); + p.put(JoseConstants.RSSEC_SIGNATURE_ALGORITHM, "RS256"); + + // A configured alias pins the key, whatever the key id in the headers + assertTrue(verifyWithLoadedVerifier(p, sign(oldKey, null), false)); + assertTrue(verifyWithLoadedVerifier(p, sign(oldKey, "key-unknown"), false)); + assertFalse(verifyWithLoadedVerifier(p, sign(newKey, "key-new"), false)); + assertFalse(verifyWithLoadedVerifier(p, sign(unmarkedKey, "key-unmarked"), false)); + // unless accepting public keys has been explicitly enabled, which selects any key by key id as before + assertTrue(verifyWithLoadedVerifier(p, sign(newKey, "key-new"), true)); + assertTrue(verifyWithLoadedVerifier(p, sign(unmarkedKey, "key-unmarked"), true)); + } + + private static KeyPair createRsaKeyPair() throws Exception { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + return kpg.generateKeyPair(); + } + + private static JsonWebKey createPublicJwk(KeyPair keyPair, String kid, PublicKeyUse use) { + JsonWebKey jwk = JwkUtils.fromRSAPublicKey((RSAPublicKey)keyPair.getPublic(), "RS256", kid); + if (use != null) { + jwk.setPublicKeyUse(use); + } + return jwk; + } + + private static String sign(KeyPair keyPair, String kid) { + JwsHeaders headers = new JwsHeaders(SignatureAlgorithm.RS256); + if (kid != null) { + headers.setKeyId(kid); + } + JwsCompactProducer producer = new JwsCompactProducer(headers, "payload"); + return producer.signWith( + JwsUtils.getPrivateKeySignatureProvider(keyPair.getPrivate(), SignatureAlgorithm.RS256)); + } + + private static boolean verifyWithLoadedVerifier(Properties props, String jws, boolean acceptPublicKey) { + JwsCompactConsumer consumer = new JwsCompactConsumer(jws); + Message m = createMessage(); + m.put(JoseConstants.RSSEC_ACCEPT_PUBLIC_KEY, acceptPublicKey); + JwsSignatureVerifier verifier = JwsUtils.loadSignatureVerifier(m, props, consumer.getJwsHeaders()); + return consumer.verifySignatureWith(verifier); + } + private static Message createMessage() { Message m = new MessageImpl(); Exchange e = new ExchangeImpl();