This is an automated email from the ASF dual-hosted git repository.
ffang 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 3934fca0312 Use the configured keystore alias for JWS verification
with X.509 hea… (#3688)
3934fca0312 is described below
commit 3934fca0312a5dc4be5a9cb74e7b23fd2aaabce7
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
---
.../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 | 96 +++++++++++++++++++++
.../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, 146 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 3c1dac23595..625351e1a14 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 ba206f2b0d1..f6341cb8324 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
@@ -406,6 +406,7 @@ public final class JwsUtils {
JoseConstants.HEADER_JSON_WEB_KEY.equals(props.get(JoseConstants.RSSEC_KEY_STORE_TYPE));
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();
@@ -421,10 +422,15 @@ public final class JwsUtils {
// 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) {
+ // 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(),
@@ -474,6 +480,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 682b3b9a5ec..7b6f7856ddc 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,8 +18,12 @@
*/
package org.apache.cxf.rs.security.jose.jws;
+import java.io.InputStream;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPublicKey;
import java.util.Arrays;
import java.util.List;
@@ -27,11 +31,14 @@ 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;
@@ -40,6 +47,7 @@ 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.apache.cxf.rt.security.crypto.MessageDigestUtils;
import org.junit.Test;
@@ -212,6 +220,94 @@ public class JwsUtilsTest {
assertTrue(verifyWithLoadedVerifier(p, sign(unmarkedKey,
"key-unmarked"), true));
}
+ @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 KeyPair createRsaKeyPair() throws Exception {
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(2048);
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