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

ffang pushed a commit to branch 3.6.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git


The following commit(s) were added to refs/heads/3.6.x-fixes by this push:
     new 2b36bd5b7c8 Use the configured keystore alias for JWS verification 
with X.509 hea… (#3688)
2b36bd5b7c8 is described below

commit 2b36bd5b7c823b64202e3e348eea9cc6b8a17d7c
Author: Freeman(Yue) Fang <[email protected]>
AuthorDate: Fri Nov 6 12:13:02 2026 -0500

    Use the configured keystore alias for JWS verification with X.509 hea… 
(#3688)
    
    * Use the configured keystore alias for JWS verification with X.509 headers
    
    When rs.security.keystore.alias is set, the verification certificate is
    taken from it also when the x5c, x5t or x5t#S256 headers are present, as
    the alias already selects the key with a "jwk" key store. An x5c
    certificate issued by the certificate of the alias is accepted.
    
    The new rs.security.accept.x509.key.headers property keeps the previous
    key selection, and JWTTokenValidator sets it when allowX509KeyHeaders is
    enabled.
    
    The x5c certificate chain validation now uses the message passed in rather
    than the current message.
    
    * Compute the pinned alias once in JwsUtils.loadSignatureVerifier
    
    (cherry picked from commit 3934fca0312a5dc4be5a9cb74e7b23fd2aaabce7)
    (cherry picked from commit b71886dbd55182b5f0f765c04f89e9c4722ccd27)
---
 .../cxf/rs/security/jose/common/JoseConstants.java |   7 ++
 .../security/jose/common/KeyManagementUtils.java   |  31 +++++--
 .../apache/cxf/rs/security/jose/jws/JwsUtils.java  |  15 +++-
 .../cxf/rs/security/jose/jws/JwsUtilsTest.java     |  97 +++++++++++++++++++++
 .../apache/cxf/rs/security/jose/jws/signers.p12    | Bin 0 -> 7900 bytes
 .../org/apache/cxf/rs/security/jose/jws/trust.p12  | Bin 0 -> 2710 bytes
 .../sts/token/validator/jwt/JWTTokenValidator.java |   3 +
 7 files changed, 147 insertions(+), 6 deletions(-)

diff --git 
a/rt/rs/security/jose-parent/jose/src/main/java/org/apache/cxf/rs/security/jose/common/JoseConstants.java
 
b/rt/rs/security/jose-parent/jose/src/main/java/org/apache/cxf/rs/security/jose/common/JoseConstants.java
index 017f814411d..65e498b24b7 100644
--- 
a/rt/rs/security/jose-parent/jose/src/main/java/org/apache/cxf/rs/security/jose/common/JoseConstants.java
+++ 
b/rt/rs/security/jose-parent/jose/src/main/java/org/apache/cxf/rs/security/jose/common/JoseConstants.java
@@ -73,6 +73,13 @@ public final class JoseConstants extends RSSecurityConstants 
{
      */
     public static final String RSSEC_ACCEPT_PUBLIC_KEY = 
"rs.security.accept.public.key";
 
+    /**
+     * Whether the x5c, x5t and x5t#S256 headers received for signature 
validation select the certificate
+     * even when a key store alias is configured. The default is "false", 
meaning the certificate of the
+     * alias is used, or an x5c certificate issued by it.
+     */
+    public static final String RSSEC_ACCEPT_X509_KEY_HEADERS = 
"rs.security.accept.x509.key.headers";
+
     /**
      * TODO documentation for these
      */
diff --git 
a/rt/rs/security/jose-parent/jose/src/main/java/org/apache/cxf/rs/security/jose/common/KeyManagementUtils.java
 
b/rt/rs/security/jose-parent/jose/src/main/java/org/apache/cxf/rs/security/jose/common/KeyManagementUtils.java
index 8f55c685473..9edc2c14419 100644
--- 
a/rt/rs/security/jose-parent/jose/src/main/java/org/apache/cxf/rs/security/jose/common/KeyManagementUtils.java
+++ 
b/rt/rs/security/jose-parent/jose/src/main/java/org/apache/cxf/rs/security/jose/common/KeyManagementUtils.java
@@ -35,10 +35,12 @@ import java.security.cert.Certificate;
 import java.security.cert.CertificateEncodingException;
 import java.security.cert.CollectionCertStoreParameters;
 import java.security.cert.PKIXBuilderParameters;
+import java.security.cert.TrustAnchor;
 import java.security.cert.X509CertSelector;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.Enumeration;
 import java.util.List;
 import java.util.Properties;
@@ -357,21 +359,40 @@ public final class KeyManagementUtils {
 
     //TODO: enhance the certificate validation code
     public static void validateCertificateChain(Properties storeProperties, 
List<X509Certificate> inCerts) {
-        Message message = PhaseInterceptorChain.getCurrentMessage();
+        validateCertificateChain(PhaseInterceptorChain.getCurrentMessage(), 
storeProperties, inCerts, null);
+    }
+
+    /**
+     * Validate the certificate chain against the trusted certificates of the 
key store, or, if trustedAlias
+     * is not null, against the certificate of that alias only.
+     */
+    public static void validateCertificateChain(Message message, Properties 
storeProperties,
+                                                List<X509Certificate> inCerts, 
String trustedAlias) {
         KeyStore ks = loadPersistKeyStore(message, storeProperties);
         String enableRevocationProp = 
storeProperties.getProperty(RSSecurityConstants.RSSEC_ENABLE_REVOCATION);
-        if (enableRevocationProp == null) {
+        if (enableRevocationProp == null && message != null) {
             enableRevocationProp = 
(String)message.getContextualProperty(JoseConstants.RSSEC_ENABLE_REVOCATION);
         }
         boolean enableRevocation = enableRevocationProp != null && 
Boolean.parseBoolean(enableRevocationProp);
-        validateCertificateChain(ks, inCerts, enableRevocation);
+        validateCertificateChain(ks, trustedAlias, inCerts, enableRevocation);
     }
-    private static void validateCertificateChain(KeyStore ks, 
List<X509Certificate> inCerts, boolean enableRevocation) {
+    private static void validateCertificateChain(KeyStore ks, String 
trustedAlias, List<X509Certificate> inCerts,
+                                                 boolean enableRevocation) {
         // Initial chain validation, to be enhanced as needed
         try {
             X509CertSelector certSelect = new X509CertSelector();
             certSelect.setCertificate(inCerts.get(0));
-            PKIXBuilderParameters pbParams = new PKIXBuilderParameters(ks, 
certSelect);
+            final PKIXBuilderParameters pbParams;
+            if (trustedAlias == null) {
+                pbParams = new PKIXBuilderParameters(ks, certSelect);
+            } else {
+                X509Certificate trustedCert = 
(X509Certificate)ks.getCertificate(trustedAlias);
+                if (trustedCert == null) {
+                    throw new JoseException("No certificate found for alias " 
+ trustedAlias);
+                }
+                pbParams = new PKIXBuilderParameters(
+                    Collections.singleton(new TrustAnchor(trustedCert, null)), 
certSelect);
+            }
             pbParams.addCertStore(CertStore.getInstance("Collection",
                                                         new 
CollectionCertStoreParameters(inCerts)));
             pbParams.setMaxPathLength(-1);
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..995f67e80a7 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
@@ -404,6 +404,7 @@ public final class JwsUtils {
         String inHeaderKid = null;
         if (inHeaders != null) {
             inHeaderKid = inHeaders.getKeyId();
+            final String pinnedAlias = getPinnedAlias(m, props, inHeaders);
             //TODO: optionally validate inHeaders.getAlgorithm against a 
property in props
             if (inHeaders.getHeader(JoseConstants.HEADER_JSON_WEB_KEY) != 
null) {
                 JsonWebKey publicJwk = inHeaders.getJsonWebKey();
@@ -414,10 +415,15 @@ public final class JwsUtils {
                 return getSignatureVerifier(publicJwk,
                                             inHeaders.getSignatureAlgorithm());
             } else if (inHeaders.getHeader(JoseConstants.HEADER_X509_CHAIN) != 
null) {
+                // With an alias configured, the x5c certificate must be the 
certificate of the alias
+                // or be issued by it, unless the X.509 headers have been 
explicitly accepted
                 List<X509Certificate> chain = 
KeyManagementUtils.toX509CertificateChain(inHeaders.getX509Chain());
-                KeyManagementUtils.validateCertificateChain(props, chain);
+                KeyManagementUtils.validateCertificateChain(m, props, chain, 
pinnedAlias);
                 return getPublicKeySignatureVerifier(chain.get(0),
                                                      
inHeaders.getSignatureAlgorithm());
+            } else if (pinnedAlias != null) {
+                // With an alias configured, the certificate of the alias is 
used
+                LOG.fine("Ignoring X.509 thumbprint headers as a key store 
alias is configured");
             } else if 
(inHeaders.getHeader(JoseConstants.HEADER_X509_THUMBPRINT) != null) {
                 X509Certificate foundCert =
                     
KeyManagementUtils.getCertificateFromThumbprint(inHeaders.getX509Thumbprint(),
@@ -467,6 +473,13 @@ public final class JwsUtils {
         return theVerifier;
     }
 
+    private static String getPinnedAlias(Message m, Properties props, 
JwsHeaders inHeaders) {
+        if (JoseUtils.checkBooleanProperty(inHeaders, props, m, 
JoseConstants.RSSEC_ACCEPT_X509_KEY_HEADERS)) {
+            return null;
+        }
+        return props.getProperty(JoseConstants.RSSEC_KEY_STORE_ALIAS);
+    }
+
     public static Properties loadJwsProperties(Message m, String propLoc) {
         try {
             return JoseUtils.loadProperties(propLoc, m.getExchange().getBus());
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..8df6b71356a 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,21 +18,30 @@
  */
 package org.apache.cxf.rs.security.jose.jws;
 
+import java.io.InputStream;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Properties;
 
 import org.apache.cxf.Bus;
 import org.apache.cxf.BusFactory;
+import org.apache.cxf.common.util.Base64UrlUtility;
 import org.apache.cxf.message.Exchange;
 import org.apache.cxf.message.ExchangeImpl;
 import org.apache.cxf.message.Message;
 import org.apache.cxf.message.MessageImpl;
 import org.apache.cxf.rs.security.jose.common.JoseConstants;
+import org.apache.cxf.rs.security.jose.common.JoseException;
+import org.apache.cxf.rs.security.jose.common.KeyManagementUtils;
 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.KeyType;
+import org.apache.cxf.rt.security.crypto.MessageDigestUtils;
 
 import org.junit.Test;
 
@@ -132,6 +141,94 @@ public class JwsUtilsTest {
         assertEquals(2, chain.size());
     }
 
+    @Test
+    public void testLoadSignatureVerifierWithX509HeadersUsesAlias() throws 
Exception {
+        // The trust store holds the certificates of alice, bob and a CA which 
issued carol's certificate
+        Properties p = trustStoreProperties("alice");
+
+        assertTrue(verifyWithLoadedVerifier(p, signWithX509Headers("alice", 
false, false)));
+        assertTrue(verifyWithLoadedVerifier(p, signWithX509Headers("alice", 
true, false)));
+        assertTrue(verifyWithLoadedVerifier(p, signWithX509Headers("alice", 
false, true)));
+        // The certificate of the configured alias is used
+        assertFalse(verifyWithLoadedVerifier(p, signWithX509Headers("bob", 
false, false)));
+        assertFalse(verifyWithLoadedVerifier(p, signWithX509Headers("bob", 
true, false)));
+        assertFalse(verifyWithLoadedVerifier(p, signWithX509Headers("bob", 
true, true)));
+        assertFalse(verifyWithLoadedVerifier(p, signWithX509Headers("bob", 
false, true)));
+        assertFalse(verifyWithLoadedVerifier(p, signWithX509Headers("carol", 
false, true)));
+        // unless the X.509 headers have been explicitly accepted, which 
select the certificate as before
+        p.put(JoseConstants.RSSEC_ACCEPT_X509_KEY_HEADERS, "true");
+        assertTrue(verifyWithLoadedVerifier(p, signWithX509Headers("bob", 
true, false)));
+        assertTrue(verifyWithLoadedVerifier(p, signWithX509Headers("bob", 
false, true)));
+        assertTrue(verifyWithLoadedVerifier(p, signWithX509Headers("carol", 
false, true)));
+        assertTrue(verifyWithLoadedVerifier(p, signWithX509Headers("alice", 
false, false)));
+    }
+
+    @Test
+    public void testLoadSignatureVerifierWithX509ChainIssuedByAlias() throws 
Exception {
+        // An x5c certificate issued by the certificate of the configured 
alias is accepted
+        Properties p = trustStoreProperties("ca");
+
+        assertTrue(verifyWithLoadedVerifier(p, signWithX509Headers("carol", 
false, true)));
+        assertFalse(verifyWithLoadedVerifier(p, signWithX509Headers("bob", 
false, true)));
+    }
+
+    @Test
+    public void testLoadSignatureVerifierWithX509HeadersWithoutAlias() throws 
Exception {
+        // Without an alias, the x5c and x5t headers select the certificate as 
before
+        Properties p = trustStoreProperties(null);
+
+        assertTrue(verifyWithLoadedVerifier(p, signWithX509Headers("alice", 
true, false)));
+        assertTrue(verifyWithLoadedVerifier(p, signWithX509Headers("bob", 
true, false)));
+        assertTrue(verifyWithLoadedVerifier(p, signWithX509Headers("bob", 
false, true)));
+        assertTrue(verifyWithLoadedVerifier(p, signWithX509Headers("carol", 
false, true)));
+    }
+
+    private static Properties trustStoreProperties(String alias) {
+        Properties p = new Properties();
+        p.put(JoseConstants.RSSEC_KEY_STORE_TYPE, "PKCS12");
+        p.put(JoseConstants.RSSEC_KEY_STORE_FILE, 
"org/apache/cxf/rs/security/jose/jws/trust.p12");
+        p.put(JoseConstants.RSSEC_KEY_STORE_PSWD, "password");
+        p.put(JoseConstants.RSSEC_SIGNATURE_ALGORITHM, "RS256");
+        if (alias != null) {
+            p.put(JoseConstants.RSSEC_KEY_STORE_ALIAS, alias);
+        }
+        return p;
+    }
+
+    private static String signWithX509Headers(String signer, boolean 
thumbprint, boolean chain) throws Exception {
+        KeyStore signers = KeyStore.getInstance("PKCS12");
+        try (InputStream is = 
JwsUtilsTest.class.getResourceAsStream("signers.p12")) {
+            signers.load(is, "password".toCharArray());
+        }
+        X509Certificate[] certs = 
Arrays.copyOf(signers.getCertificateChain(signer),
+            signers.getCertificateChain(signer).length, 
X509Certificate[].class);
+
+        JwsHeaders headers = new JwsHeaders(SignatureAlgorithm.RS256);
+        if (thumbprint) {
+            headers.setX509Thumbprint(Base64UrlUtility.encode(
+                MessageDigestUtils.createDigest(certs[0].getEncoded(), 
MessageDigestUtils.ALGO_SHA_1)));
+            headers.setX509ThumbprintSHA256(Base64UrlUtility.encode(
+                MessageDigestUtils.createDigest(certs[0].getEncoded(), 
MessageDigestUtils.ALGO_SHA_256)));
+        }
+        if (chain) {
+            
headers.setX509Chain(KeyManagementUtils.encodeX509CertificateChain(certs));
+        }
+        JwsCompactProducer producer = new JwsCompactProducer(headers, 
"payload");
+        return producer.signWith(JwsUtils.getPrivateKeySignatureProvider(
+            (PrivateKey)signers.getKey(signer, "password".toCharArray()), 
SignatureAlgorithm.RS256));
+    }
+
+    private static boolean verifyWithLoadedVerifier(Properties props, String 
jws) {
+        JwsCompactConsumer consumer = new JwsCompactConsumer(jws);
+        try {
+            JwsSignatureVerifier verifier =
+                JwsUtils.loadSignatureVerifier(createMessage(), props, 
consumer.getJwsHeaders());
+            return consumer.verifySignatureWith(verifier);
+        } catch (JoseException ex) {
+            return false;
+        }
+    }
+
     private static Message createMessage() {
         Message m = new MessageImpl();
         Exchange e = new ExchangeImpl();
diff --git 
a/rt/rs/security/jose-parent/jose/src/test/java/org/apache/cxf/rs/security/jose/jws/signers.p12
 
b/rt/rs/security/jose-parent/jose/src/test/java/org/apache/cxf/rs/security/jose/jws/signers.p12
new file mode 100644
index 00000000000..8f25bb8e6e7
Binary files /dev/null and 
b/rt/rs/security/jose-parent/jose/src/test/java/org/apache/cxf/rs/security/jose/jws/signers.p12
 differ
diff --git 
a/rt/rs/security/jose-parent/jose/src/test/java/org/apache/cxf/rs/security/jose/jws/trust.p12
 
b/rt/rs/security/jose-parent/jose/src/test/java/org/apache/cxf/rs/security/jose/jws/trust.p12
new file mode 100644
index 00000000000..781a6ff1aa3
Binary files /dev/null and 
b/rt/rs/security/jose-parent/jose/src/test/java/org/apache/cxf/rs/security/jose/jws/trust.p12
 differ
diff --git 
a/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/jwt/JWTTokenValidator.java
 
b/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/jwt/JWTTokenValidator.java
index d35c7e84766..bf788cda41e 100644
--- 
a/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/jwt/JWTTokenValidator.java
+++ 
b/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/jwt/JWTTokenValidator.java
@@ -130,6 +130,9 @@ public class JWTTokenValidator implements TokenValidator {
         }
         KeyStore keystore = ((Merlin)signatureCrypto).getKeyStore();
         verificationProperties.put(JoseConstants.RSSEC_KEY_STORE, keystore);
+        if (allowX509KeyHeaders) {
+            
verificationProperties.put(JoseConstants.RSSEC_ACCEPT_X509_KEY_HEADERS, "true");
+        }
 
         // Unless allowX509KeyHeaders is enabled, the token headers are not 
passed on, so the token
         // is verified with the configured signature certificate only. 
Otherwise x5c, x5t and

Reply via email to