[java-idp-plugin-oidc-op-oidfed] 01/03: Exploit locally trusted anchor credentials for validating immediate subordinate

Codeberg noreply at shibboleth.net
Thu Jan 22 11:41:10 UTC 2026


This is an automated email from the git hooks/post-receive script.

codeberg pushed a commit to branch dev/CACHE-REFACTOR
in repository java-idp-plugin-oidc-op-oidfed.

View the commit online:
https://codeberg.org/Shibboleth/java-idp-plugin-oidc-op-oidfed/commit/d5ff6c9e5adf13361bab986106b95be3733d000e

commit d5ff6c9e5adf13361bab986106b95be3733d000e
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Wed Jan 21 09:20:13 2026 +0200

    Exploit locally trusted anchor credentials for validating immediate subordinate
    
    - The section 4 of the spec mandates that in addition to validating the trust anchor configuration
    - Included placeholders for the new trust chain cache tests
      - Not yet activated as some changes in the commond module are required (JCOMOIDC-154)
---
 ...rustChainSignatureValidationFilterStrategy.java |  24 ++-
 .../DefaultLocalTrustAnchorCredentialResolver.java |   2 +-
 .../flow/oidfed/AbstractFederationFlowTest.java    |  10 +-
 .../oidfed/cache/TrustChainMetadataCacheTest.java  | 166 ++++++---------------
 4 files changed, 76 insertions(+), 126 deletions(-)

diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/remote/trustchain/DefaultRemoteTrustChainSignatureValidationFilterStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/remote/trustchain/DefaultRemoteTrustChainSignatureValidationFilterStrategy.java
index cf2e8b6..10a571c 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/remote/trustchain/DefaultRemoteTrustChainSignatureValidationFilterStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/remote/trustchain/DefaultRemoteTrustChainSignatureValidationFilterStrategy.java
@@ -94,7 +94,7 @@ public class DefaultRemoteTrustChainSignatureValidationFilterStrategy
         }
 
         trustChainLoop: for (final List<EntityStatement<?>> trustChain : container.getTrustChains()) {
-            for (int i = 0; i < trustChain.size() - 1; i++) {
+            for (int i = 0; i < trustChain.size() - 2; i++) {
                 final EntityStatement<?> trustChainEntry = trustChain.get(i);
                 final SignedJWT keyContainer = trustChain.get(i + 1).getJwt();
                 final CriteriaSet criteria =
@@ -110,14 +110,30 @@ public class DefaultRemoteTrustChainSignatureValidationFilterStrategy
                     continue trustChainLoop;
                 }
             }
+
+            final EntityStatement<?> trustAnchorSubordinateStatement = trustChain.get(trustChain.size() - 2);
+            final String subjectId = trustAnchorSubordinateStatement.getSubject();
+            final String trustAnchorId = trustAnchorSubordinateStatement.getIssuer();
+            final EntityStatement<?> filteredSubordinateStatement =
+                    entityStatementSignatureValidationFilterStrategy.apply(trustAnchorSubordinateStatement,
+                            filterContext);
+            if (filteredSubordinateStatement != null) {
+                log.debug("Successfully validated subordinate statement for {}, issued by trust anchor {}",
+                        subjectId, trustAnchorId);
+            } else {
+                log.warn("Signature validation failed for subordinate statement for {}, issued by trust anchor {}",
+                        subjectId, trustAnchorId);
+                container.removeTrustChain(trustChain);
+                continue trustChainLoop;
+            }
+
             final EntityStatement<?> trustAnchorStatement = trustChain.get(trustChain.size() - 1);
-            final String entityId = trustAnchorStatement.getSubject();
             final EntityStatement<?> filteredStatement =
                     entityStatementSignatureValidationFilterStrategy.apply(trustAnchorStatement, filterContext);
             if (filteredStatement != null) {
-                log.debug("Successfully validated entity statement for trust anchor {}", entityId);
+                log.debug("Successfully validated entity statement for trust anchor {}", trustAnchorId);
             } else {
-                log.debug("Signature validation failed for trust anchor {}", entityId);
+                log.warn("Signature validation failed for trust anchor {}", trustAnchorId);
                 container.removeTrustChain(trustChain);
             }
         }
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/DefaultLocalTrustAnchorCredentialResolver.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/DefaultLocalTrustAnchorCredentialResolver.java
index fa78c9e..2b3379e 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/DefaultLocalTrustAnchorCredentialResolver.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/DefaultLocalTrustAnchorCredentialResolver.java
@@ -73,7 +73,7 @@ public class DefaultLocalTrustAnchorCredentialResolver extends BasicJOSEObjectCr
             throw new ResolverException(
                     "Credential criteria set did not contain an instance of SubjectEntityStatementCriterion");
         }
-        final String entityId = subjectCriterion.getValue().getSubject();
+        final String entityId = subjectCriterion.getValue().getIssuer();
         log.debug("Attempting to find trusted keys for {}", entityId);
         
         final List<Map<String, LocalKeyContainer>> keyContainers;
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AbstractFederationFlowTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AbstractFederationFlowTest.java
index c9a5ead..17d76b2 100644
--- a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AbstractFederationFlowTest.java
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AbstractFederationFlowTest.java
@@ -336,12 +336,18 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
 
     protected String trustedAnchorConfiguration(final Map<String, Object> constraints,
             final Map<String,List<String>> trustMarkIssuers) {
+        return trustedAnchorConfiguration(constraints, trustMarkIssuers, trustedAnchorKey,
+                new JWKSet(trustedAnchorKey));
+    }
+
+    protected String trustedAnchorConfiguration(final Map<String, Object> constraints,
+            final Map<String,List<String>> trustMarkIssuers, final JWK signerKey, final JWKSet jwks) {
 
         final String anchorId = "https://trust-anchor.federation.local";
         final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(anchorId)
                 .issueTime(Date.from(Instant.now()))
                 .expirationTime(Date.from(Instant.now().plusSeconds(300)))
-                .claim("jwks", new JWKSet(trustedAnchorKey).toJSONObject(true))
+                .claim("jwks", jwks.toJSONObject(true))
                 .claim("metadata", Map.of("federation_entity", Map.of("federation_fetch_endpoint",
                         anchorFetchEndpoint, "federation_resolve_endpoint", anchorResolveEndpoint)));
         if (constraints != null) {
@@ -351,7 +357,7 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
             builder.claim("trust_mark_issuers", trustMarkIssuers);
         }
         final EntityStatement<?> anchorConfiguration =
-                TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, trustedAnchorKey, builder.build());
+                TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, signerKey, builder.build());
         return anchorConfiguration.getJwt().serialize();
     }
 
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/TrustChainMetadataCacheTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/TrustChainMetadataCacheTest.java
index af56106..35d2bb7 100644
--- a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/TrustChainMetadataCacheTest.java
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/TrustChainMetadataCacheTest.java
@@ -96,165 +96,93 @@ public class TrustChainMetadataCacheTest extends AbstractFederationFlowTest {
             Assert.fail("Could not resolve entity configuration", e);
         }
     }
-/*
-    @Test
-    public void testSignatureWithNonMathchingKey()
+
+    @Test(enabled = false)
+    //TODO: enable once the entity configuration cache can be cleaned before/after the test
+    //otherwise the non-compatible trust anchor configuration may be in the cache
+    public void testTrustAnchorConfigurationSignatureWithUntrustedKey()
             throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
         final String entityId = uniqueClientId();
-        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(entityId)
+        
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
                 .issueTime(Date.from(Instant.now()))
                 .expirationTime(Date.from(Instant.now().plusSeconds(300)))
                 .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
-                .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
-        final String subordinateStatement = TrustChainTestUtil.signedJwt(
+                .claim("metadata", Map.of("federation_entity", Collections.emptyMap()))
+                .claim("authority_hints", List.of(anchorId));
+        final String entityConfiguration = TrustChainTestUtil.signedJwt(
                 JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
 
-        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
-        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
-                mockResponse(subordinateStatement));
-        assertNoSubordinateStatement(entityId);
-    }
+        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
 
-    @Test
-    public void testValidClaims_nonMatchingSubject()
-            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
-        final String entityId = uniqueClientId();
-        final String claimsEntityId = uniqueClientId();
-        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(claimsEntityId)
+        final JWTClaimsSet.Builder builder2 = new JWTClaimsSet.Builder().issuer(anchorId).subject(entityId)
                 .issueTime(Date.from(Instant.now()))
                 .expirationTime(Date.from(Instant.now().plusSeconds(300)))
                 .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
                 .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
         final String subordinateStatement = TrustChainTestUtil.signedJwt(
-                JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
+                JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder2.build()).serialize();
 
-        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+        mapResponse(entityConfigurationUrl(anchorId),
+                mockResponse(trustedAnchorConfiguration(null, null, anchorKey, new JWKSet(anchorKey))));
         mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
                 mockResponse(subordinateStatement));
-        assertNoSubordinateStatement(entityId);
+        try {
+            final List<RemoteTrustChainsContainer> result =
+                    trustChainCache.get(new CriteriaSet(new SubjectEntityIDCriterion(entityId),
+                            new ResponseContainerExpirationCriterion(Instant.now().plusSeconds(300)), 
+                            new IssuerEntityIDCriterion(anchorId)));
+            Assert.assertNotNull(result);
+            Assert.assertEquals(result.size(), 1);
+            Assert.assertNotNull(result.get(0).getTrustChains());
+            Assert.assertEquals(result.get(0).getTrustChains().size(), 0);
+        } catch (MetadataCacheException e) {
+            Assert.fail("Could not resolve entity configuration", e);
+        }
     }
 
-    @Test
-    public void testValidClaims_invalidTypeHeader()
+    @Test(enabled = false)
+    //TODO: enable once the trust anchor cache can be cleaned before/after the test
+    //otherwise the non-compatible trust anchor configuration may be in the cache
+    public void testTrustAnchorSubordinateSignatureWithUntrustedKey()
             throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
         final String entityId = uniqueClientId();
-        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(entityId)
+        
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
                 .issueTime(Date.from(Instant.now()))
                 .expirationTime(Date.from(Instant.now().plusSeconds(300)))
                 .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
-                .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
-        final String subordinateStatement = TrustChainTestUtil.signedJwt(
-                JWSAlgorithm.RS256, trustedAnchorKey, "jwt", builder.build()).serialize();
-
-        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
-        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
-                mockResponse(subordinateStatement));
-
-        assertNoSubordinateStatement(entityId);
-    }
-
-    @Test
-    public void testIssuedInFuture()
-            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
-        final String entityId = uniqueClientId();
-        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(entityId)
-                .issueTime(Date.from(Instant.now().plusSeconds(300)))
-                .expirationTime(Date.from(Instant.now().plusSeconds(300)))
-                .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
-                .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
-        final String subordinateStatement = TrustChainTestUtil.signedJwt(
-                JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
+                .claim("metadata", Map.of("federation_entity", Collections.emptyMap()))
+                .claim("authority_hints", List.of(anchorId));
+        final String entityConfiguration = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
 
-        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
-        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
-                mockResponse(subordinateStatement));
-        assertNoSubordinateStatement(entityId);
-    }
+        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
 
-    @Test
-    public void testMissingSub()
-            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
-        final String entityId = uniqueClientId();
-        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId)
+        final JWTClaimsSet.Builder builder2 = new JWTClaimsSet.Builder().issuer(anchorId).subject(entityId)
                 .issueTime(Date.from(Instant.now()))
                 .expirationTime(Date.from(Instant.now().plusSeconds(300)))
                 .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
                 .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
         final String subordinateStatement = TrustChainTestUtil.signedJwt(
-                JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
-
-        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
-        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
-                mockResponse(subordinateStatement));
-        assertNoSubordinateStatement(entityId);
-    }
-
-    @Test
-    public void testExpired()
-            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
-        final String entityId = uniqueClientId();
-        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(entityId)
-                .issueTime(Date.from(Instant.now()))
-                .expirationTime(Date.from(Instant.now().minusSeconds(300)))
-                .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
-                .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
-        final String subordinateStatement = TrustChainTestUtil.signedJwt(
-                JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
+                JWSAlgorithm.RS256, anchorKey, "entity-statement+jwt", builder2.build()).serialize();
 
-        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+        mapResponse(entityConfigurationUrl(anchorId),
+                mockResponse(trustedAnchorConfiguration(null, null, trustedAnchorKey,
+                        new JWKSet(List.of(trustedAnchorKey, anchorKey)))));
         mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
                 mockResponse(subordinateStatement));
-        assertNoSubordinateStatement(entityId);
-    }
-
-    @Test
-    public void testMissingJwks()
-            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
-        final String entityId = uniqueClientId();
-        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(entityId)
-                .issueTime(Date.from(Instant.now()))
-                .expirationTime(Date.from(Instant.now().plusSeconds(300)))
-                .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
-        final String subordinateStatement = TrustChainTestUtil.signedJwt(
-                JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
-
-        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
-        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
-                mockResponse(subordinateStatement));
-        assertNoSubordinateStatement(entityId);
-    }
-
-    @Test
-    public void testInvalidJwksClaim()
-            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
-        final String entityId = uniqueClientId();
-        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(entityId)
-                .issueTime(Date.from(Instant.now()))
-                .expirationTime(Date.from(Instant.now().plusSeconds(300)))
-                .claim("jwks", Map.of("federation_entity", Collections.emptyMap()))
-                .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
-        final String subordinateStatement = TrustChainTestUtil.signedJwt(
-                JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
-
-        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
-        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
-                mockResponse(subordinateStatement));
-        assertNoSubordinateStatement(entityId);
-    }
-
-    protected void assertNoSubordinateStatement(final String entityId) {
         try {
-            final List<RemoteSubordinateStatementContainer> result =
-                    subordinateStatementCache.get(new CriteriaSet(new SubjectEntityIDCriterion(entityId),
+            final List<RemoteTrustChainsContainer> result =
+                    trustChainCache.get(new CriteriaSet(new SubjectEntityIDCriterion(entityId),
                             new ResponseContainerExpirationCriterion(Instant.now().plusSeconds(300)), 
                             new IssuerEntityIDCriterion(anchorId)));
             Assert.assertNotNull(result);
             Assert.assertEquals(result.size(), 1);
-            Assert.assertNull(result.get(0).getEntityStatement());
+            Assert.assertNotNull(result.get(0).getTrustChains());
+            Assert.assertEquals(result.get(0).getTrustChains().size(), 0);
         } catch (MetadataCacheException e) {
             Assert.fail("Could not resolve entity configuration", e);
         }
-        
     }
-    */
 }

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list