[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