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();