This is an automated email from the ASF dual-hosted git repository.

coheigea pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cxf.git


The following commit(s) were added to refs/heads/main by this push:
     new e2ff6c9c2a4 CXF-9032 - Fix JWS verification with a "jwk" key store 
(#3531)
e2ff6c9c2a4 is described below

commit e2ff6c9c2a4dc1cf521446af83879350e4f4233c
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Wed Sep 30 19:35:33 2026 +0100

    CXF-9032 - Fix JWS verification with a "jwk" key store (#3531)
---
 .../apache/cxf/rs/security/jose/jwk/JwkUtils.java  |  37 +++++--
 .../apache/cxf/rs/security/jose/jws/JwsUtils.java  |   9 +-
 .../cxf/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 final class JwkUtils {
     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 final class JwkUtils {
         return null;
     }
 
+    private static boolean isKeyMarkedFor(JsonWebKey jwk, KeyOperation 
keyOper) {
+        if (keyOper == null) {
+            return false;
+        }
+        List<KeyOperation> 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<JsonWebKey> 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 final class JwsUtils {
                                                               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 final class JwsUtils {
                 }
                 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<X509Certificate> chain = 
KeyManagementUtils.toX509CertificateChain(inHeaders.getX509Chain());
                 KeyManagementUtils.validateCertificateChain(props, chain);
@@ -439,7 +446,7 @@ public final class JwsUtils {
             }
         }
 
-        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.AlgorithmUtils;
 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 class JwsUtilsTest {
         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 class JwsUtilsTest {
         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();

Reply via email to