[java-oidfed-common] branch main updated: Improved the trust chain resolution and validation
Codeberg
noreply at shibboleth.net
Mon Sep 28 15:54:35 UTC 2026
This is an automated email from the git hooks/post-receive script.
codeberg pushed a commit to branch main
in repository java-oidfed-common.
View the commit online:
https://codeberg.org/Shibboleth/java-oidfed-common/commit/90dd88ec0630fac965fbbfa93c57acdf7cac1443
The following commit(s) were added to refs/heads/main by this push:
new 90dd88e Improved the trust chain resolution and validation
90dd88e is described below
commit 90dd88ec0630fac965fbbfa93c57acdf7cac1443
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Mon Sep 28 18:18:26 2026 +0300
Improved the trust chain resolution and validation
- Exploit expiration times from statements to calculate statement and trust chain validity in the caches
- Filter out the expired chains, exploit the clockSkew (idp.policy.clockSkew)
- Require expiration claim via claims validators for entity configurations and subordinate statements
- Verify the subject/issuer links in trust chain before signature validation
- Included some basic tests (subject, issuer, anchors) to the remote resolver response
---
.../META-INF/net.shibboleth.idp/postconfig.xml | 7 ++-
...DefaultEntityConfigurationFetchingStrategy.java | 9 +++-
...efaultSubordinateStatementFetchingStrategy.java | 9 +++-
.../DefaultTrustChainFetchingStrategy.java | 50 +++++++++++++++++++++-
...rustChainSignatureValidationFilterStrategy.java | 8 ++++
.../metadata/util/EntityStatementHelper.java | 34 ++++++++++++++-
.../oidfed/profile/impl/CallResolveEntityApi.java | 22 +++++++++-
.../DefaultTrustChainFetchingStrategyTest.java | 3 ++
8 files changed, 136 insertions(+), 6 deletions(-)
diff --git a/oidfed-common-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/oidfed-common-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index 155c061..4c1797c 100644
--- a/oidfed-common-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/oidfed-common-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -144,6 +144,8 @@
p:requiredClaims="sub" />
<bean class="net.shibboleth.oidc.security.jwt.claims.impl.RequiredClaimsValidator"
p:requiredClaims="jwks" />
+ <bean class="net.shibboleth.oidc.security.jwt.claims.impl.RequiredClaimsValidator"
+ p:requiredClaims="exp" />
<bean class="net.shibboleth.oidc.security.jwt.claims.impl.ExpiryClaimsValidator"
p:clockSkew="%{idp.policy.clockSkew:PT1M}" />
<bean class="net.shibboleth.oidc.security.jwt.claims.impl.ProhibitedClaimsValidator"
@@ -312,6 +314,8 @@
p:requiredClaims="sub" />
<bean class="net.shibboleth.oidc.security.jwt.claims.impl.RequiredClaimsValidator"
p:requiredClaims="jwks" />
+ <bean class="net.shibboleth.oidc.security.jwt.claims.impl.RequiredClaimsValidator"
+ p:requiredClaims="exp" />
<bean class="net.shibboleth.oidc.security.jwt.claims.impl.ExpiryClaimsValidator"
p:clockSkew="%{idp.policy.clockSkew:PT1M}" />
<bean class="net.shibboleth.oidfed.security.jwt.claims.impl.CritClaimsValidator"
@@ -390,7 +394,8 @@
p:criteriaToSubjectEntityIdStrategy-ref="shibboleth.oidfed.DefaultSubjectEntityIDCriteriaToIdentifierStrategy"
p:entityConfigurationCache-ref="shibboleth.oidfed.EntityConfigurationMetadataCache"
p:subordinateStatementCache-ref="shibboleth.oidfed.SubordinateEntityStatementMetadataCache"
- p:localTrustAnchorsCache-ref="shibboleth.oidfed.LocalTrustAnchorsMetadataCache">
+ p:localTrustAnchorsCache-ref="shibboleth.oidfed.LocalTrustAnchorsMetadataCache"
+ p:clockSkew="%{idp.policy.clockSkew:PT1M}">
<property name="criteriaToValidContainerLifetimeStrategy">
<bean parent="shibboleth.Functions.Constant">
<constructor-arg>
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/metadata/cache/configuration/DefaultEntityConfigurationFetchingStrategy.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/metadata/cache/configuration/DefaultEntityConfigurationFetchingStrategy.java
index 5176257..838bf16 100644
--- a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/metadata/cache/configuration/DefaultEntityConfigurationFetchingStrategy.java
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/metadata/cache/configuration/DefaultEntityConfigurationFetchingStrategy.java
@@ -18,6 +18,9 @@ import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Instant;
+import java.util.Comparator;
+import java.util.Objects;
+import java.util.stream.Stream;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -115,8 +118,12 @@ public class DefaultEntityConfigurationFetchingStrategy
}
assert objectMapper != null;
final EntityConfiguration entityConfiguration = EntityConfigurationImpl.parse(jwt, objectMapper);
+ final Instant expiration =
+ Stream.of(validExpiration, entityConfiguration.getParsedPayload().getExpiration())
+ .filter(Objects::nonNull).min(Comparator.naturalOrder()).get();
+ assert expiration != null;
return new EntityConfigurationContainer(
- entityId, entityConfiguration, validExpiration, invalidExpiration);
+ entityId, entityConfiguration, expiration, invalidExpiration);
} catch (final JsonProcessingException e) {
log.warn("Could not deserialize entity statement payload from the response", e);
} catch (final ConstraintViolationException e) {
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/metadata/cache/subordinate/DefaultSubordinateStatementFetchingStrategy.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/metadata/cache/subordinate/DefaultSubordinateStatementFetchingStrategy.java
index 985af41..c7fdd45 100644
--- a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/metadata/cache/subordinate/DefaultSubordinateStatementFetchingStrategy.java
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/metadata/cache/subordinate/DefaultSubordinateStatementFetchingStrategy.java
@@ -20,10 +20,13 @@ import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.ArrayList;
+import java.util.Comparator;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
import java.util.function.BiConsumer;
+import java.util.stream.Stream;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -212,8 +215,12 @@ public class DefaultSubordinateStatementFetchingStrategy
}
assert objectMapper != null;
final SubordinateStatement statement = SubordinateStatementImpl.parse(jwt, objectMapper);
+ final Instant expiration =
+ Stream.of(validExpiration, statement.getParsedPayload().getExpiration())
+ .filter(Objects::nonNull).min(Comparator.naturalOrder()).get();
+ assert expiration != null;
log.trace("Subordinate statement successfully parsed and stored into the container");
- return new SubordinateStatementContainer(id, statement, validExpiration, invalidExpiration);
+ return new SubordinateStatementContainer(id, statement, expiration, invalidExpiration);
} catch (final JsonProcessingException e) {
log.warn("Could not deserialize entity statement payload from the response", e);
} catch (final ConstraintViolationException e) {
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/metadata/cache/trustchain/DefaultTrustChainFetchingStrategy.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/metadata/cache/trustchain/DefaultTrustChainFetchingStrategy.java
index 0072b9d..d2e4c36 100644
--- a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/metadata/cache/trustchain/DefaultTrustChainFetchingStrategy.java
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/metadata/cache/trustchain/DefaultTrustChainFetchingStrategy.java
@@ -17,6 +17,7 @@ package net.shibboleth.oidfed.metadata.cache.trustchain;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
+import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
@@ -87,6 +88,18 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
/** Strategy to fetch maximum amount of authority hints to be processed. */
@NonnullAfterInit private Function<CriteriaSet, Integer> criteriaToMaximumAuthorityHintsStrategy;
+ /** Positive clock skew adjustment to consider when checking trust chain expiration. */
+ @Nonnull private Duration clockSkew;
+
+ /**
+ * Constructor.
+ */
+ public DefaultTrustChainFetchingStrategy() {
+ final Duration sixtySeconds = Duration.ofSeconds(60);
+ assert sixtySeconds != null;
+ clockSkew = sixtySeconds;
+ }
+
/**
* Set the strategy for fetching entity ID from the criteria set.
*
@@ -174,6 +187,17 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
Constraint.isNotNull(strategy, "Criteria to maximum authority hints strategy cannot be null");
}
+ /**
+ * Set the clock skew.
+ *
+ * @param skew clock skew to set
+ */
+ public void setClockSkew(@Nonnull final Duration skew) {
+ checkSetterPreconditions();
+ clockSkew = Constraint.isNotNull(skew, "Clock skew cannot be null");
+ Constraint.isFalse(clockSkew.isNegative(), "Clock skew cannot be negative");
+ }
+
/** {@inheritDoc} */
@Override
protected void doInitialize() throws ComponentInitializationException {
@@ -276,9 +300,17 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
final List<List<EntityStatement<?>>> result = populatedWrappers.stream()
.filter(wrapper -> wrapper.isComplete())
.map(wrapper -> wrapper.getTrustChain())
+ .filter(chain -> parseTrustChainExpiration(chain).isAfter(Instant.now().minus(clockSkew)))
.toList();
assert result != null; assert invalidExpiration != null; assert validExpiration != null;
- return new TrustChainsContainer(result.isEmpty() ? invalidExpiration : validExpiration,
+ final Instant chainsExpiration = result.stream()
+ .map(chain -> parseTrustChainExpiration(chain))
+ .min(Comparator.naturalOrder())
+ .orElse(Instant.now());
+ final Instant shortestExpiration = chainsExpiration.plus(clockSkew).compareTo(validExpiration) > 0 ?
+ validExpiration : chainsExpiration.plus(clockSkew);
+ assert shortestExpiration != null;
+ return new TrustChainsContainer(result.isEmpty() ? invalidExpiration : shortestExpiration,
entityConfiguration.getSubject(), result);
}
@@ -466,6 +498,22 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
}
}
+ /**
+ * Parse the expiration time from the trust chain: the lowest expiration time value is taken from the chain.
+ *
+ * @param trustChain trust chain
+ * @return expiration instant
+ */
+ @Nonnull protected Instant parseTrustChainExpiration(@Nullable final List<EntityStatement<?>> trustChain) {
+ if (trustChain != null) {
+ return EntityStatementHelper.parseTrustChainExpiration(trustChain);
+ } else {
+ final Instant now = Instant.now();
+ assert now != null;
+ return now;
+ }
+ }
+
/**
* Wrapper class containing trust chain (potentially under construction) and a flag for indicating whether it's
* complete.
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/metadata/cache/trustchain/DefaultTrustChainSignatureValidationFilterStrategy.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/metadata/cache/trustchain/DefaultTrustChainSignatureValidationFilterStrategy.java
index 9f07dc5..d76c0be 100644
--- a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/metadata/cache/trustchain/DefaultTrustChainSignatureValidationFilterStrategy.java
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/metadata/cache/trustchain/DefaultTrustChainSignatureValidationFilterStrategy.java
@@ -29,6 +29,7 @@ import net.shibboleth.oidc.metadata.filter.MetadataFilterContext;
import net.shibboleth.oidfed.metadata.EntityStatement;
import net.shibboleth.oidfed.metadata.cache.AbstractTrustEngineSignatureValidationComponent;
import net.shibboleth.oidfed.metadata.cache.SignatureValidationKeyContainerJwtCriterion;
+import net.shibboleth.oidfed.metadata.util.EntityStatementHelper;
import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
import net.shibboleth.shared.collection.CollectionSupport;
@@ -116,6 +117,13 @@ public class DefaultTrustChainSignatureValidationFilterStrategy
}
trustChainLoop: for (final List<EntityStatement<?>> trustChain : container.getTrustChains()) {
+ assert trustChain != null;
+ if (!EntityStatementHelper.verifyTrustChainIssuerLinks(trustChain)) {
+ log.warn("The trust chain identity link validation failed, ignoring trust chain anchored by {}",
+ trustChain.get(trustChain.size() - 1).getSubject());
+ container.removeTrustChain(trustChain);
+ continue trustChainLoop;
+ }
for (int i = 0; i < trustChain.size() - 2; i++) {
final EntityStatement<?> trustChainEntry = trustChain.get(i);
final SignedJWT keyContainer = trustChain.get(i + 1).getJwt();
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/metadata/util/EntityStatementHelper.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/metadata/util/EntityStatementHelper.java
index 03d16ed..65f2351 100644
--- a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/metadata/util/EntityStatementHelper.java
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/metadata/util/EntityStatementHelper.java
@@ -141,6 +141,11 @@ public class EntityStatementHelper {
log.warn("Trust chain does not end with an entity configuration");
return null;
}
+ if (!verifyTrustChainIssuerLinks(result)) {
+ log.warn("The trust chain identity link validation failed, ignoring trust chain anchored by {}",
+ result.get(result.size() - 1).getSubject());
+ return null;
+ }
return result;
}
@@ -195,9 +200,36 @@ public class EntityStatementHelper {
@Nonnull public static Instant parseTrustChainExpiration(@Nonnull final List<EntityStatement<?>> trustChain) {
final Instant result = trustChain.stream()
.map(statement -> statement.getParsedPayload().getExpiration())
- .min(Comparator.comparing(Instant::toEpochMilli))
+ .filter(Objects::nonNull)
+ .min(Comparator.naturalOrder())
.orElse(Instant.now());
assert result != null;
return result;
}
+
+ /**
+ * Verifies the expected links between subjects and issuers in a standard trust chain.
+ *
+ * @param trustChain trust chain to be evaluated
+ * @return true if links are expected, false otherwise
+ */
+ public static boolean verifyTrustChainIssuerLinks(@Nonnull final List<EntityStatement<?>> trustChain) {
+ if (trustChain.size() < 3) {
+ return false;
+ }
+ final EntityStatement<?> leaf = trustChain.get(0);
+ if (!leaf.getSubject().equals(leaf.getIssuer())) {
+ return false;
+ }
+ for (int i = 0; i < trustChain.size() - 1; i++) {
+ if (!trustChain.get(i).getIssuer().equals(trustChain.get(i + 1).getSubject())) {
+ return false;
+ }
+ }
+ final EntityStatement<?> anchor = trustChain.get(trustChain.size() - 1);
+ if (!anchor.getSubject().equals(anchor.getIssuer())) {
+ return false;
+ }
+ return true;
+ }
}
\ No newline at end of file
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/CallResolveEntityApi.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/CallResolveEntityApi.java
index d31819b..f929ead 100644
--- a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/CallResolveEntityApi.java
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/CallResolveEntityApi.java
@@ -442,7 +442,17 @@ public class CallResolveEntityApi extends AbstractProfileAction {
}
final ResolveEntityResponse statement = cacheResult.get(0).getStatement();
if (statement != null) {
+ if (!statement.getIssuer().equals(trustedEntity.getEntityId())) {
+ log.warn("{} The resolve entity response issuer {} is not matching the expected trusted entity {}",
+ getLogPrefix(), statement.getIssuer(), trustedEntity.getEntityId());
+ continue;
+ }
final ResolveEntityResponsePayload payload = statement.getParsedPayload();
+ if (!nonNullEntityId.equals(payload.getSubject())) {
+ log.warn("{} The resolve entity response subject {} is not matching the requested entity {}",
+ getLogPrefix(), payload.getSubject(), nonNullEntityId);
+ continue;
+ }
final List<String> rawTrustChain = payload.getTrustChain();
final Metadata metadata = payload.getMetadata();
final List<Map<String,String>> rawTrustMarks = payload.getTrustMarks();
@@ -455,7 +465,7 @@ public class CallResolveEntityApi extends AbstractProfileAction {
final List<EntityStatement<?>> chain =
EntityStatementHelper.deserializeTrustChain(rawTrustChain, objectMapper);
if (chain == null) {
- log.warn("Could not parse the trust chain into list of entity statements");
+ log.warn("{} Could not parse the trust chain into list of entity statements", getLogPrefix());
continue;
}
if (!preSelectedChain.isEmpty() && !preSelectedChain.equals(trustChainIDsLookupStrategy.apply(chain))) {
@@ -463,6 +473,16 @@ public class CallResolveEntityApi extends AbstractProfileAction {
getLogPrefix());
continue;
}
+ if (!nonNullEntityId.equals(chain.get(0).getSubject())) {
+ log.warn("{} The trust chain subject {} is not matching the requested entity {}", getLogPrefix(),
+ chain.get(0).getSubject(), nonNullEntityId);
+ continue;
+ }
+ if (!trustedEntity.getTrustAnchors().contains(chain.get(chain.size() - 1).getSubject())) {
+ log.warn("{} The trust chain anchor {} is not among the requested anchors {}", getLogPrefix(),
+ chain.get(chain.size() - 1).getSubject(), trustedEntity.getTrustAnchors());
+ continue;
+ }
final List<VerifiedTrustChain> policyCompliantChains = new ArrayList<>();
policyCompliantChains.add(new VerifiedTrustChain(chain, metadata));
trustChainContext.setPolicyCompliantTrustChains(policyCompliantChains);
diff --git a/oidfed-common-impl/src/test/java/net/shibboleth/oidfed/metadata/cache/trustchain/DefaultTrustChainFetchingStrategyTest.java b/oidfed-common-impl/src/test/java/net/shibboleth/oidfed/metadata/cache/trustchain/DefaultTrustChainFetchingStrategyTest.java
index 124a893..77ae525 100644
--- a/oidfed-common-impl/src/test/java/net/shibboleth/oidfed/metadata/cache/trustchain/DefaultTrustChainFetchingStrategyTest.java
+++ b/oidfed-common-impl/src/test/java/net/shibboleth/oidfed/metadata/cache/trustchain/DefaultTrustChainFetchingStrategyTest.java
@@ -38,6 +38,7 @@ import net.shibboleth.oidfed.metadata.cache.local.LocalKeyContainer;
import net.shibboleth.oidfed.metadata.cache.subordinate.SubordinateStatementCacheIdentifier;
import net.shibboleth.oidfed.metadata.cache.subordinate.SubordinateStatementContainer;
import net.shibboleth.oidfed.metadata.payload.EntityConfigurationPayload;
+import net.shibboleth.oidfed.metadata.payload.SubordinateStatementPayload;
import net.shibboleth.oidfed.metadata.util.EntityStatementHelper;
import net.shibboleth.shared.collection.CollectionSupport;
import net.shibboleth.shared.component.ComponentInitializationException;
@@ -577,8 +578,10 @@ public class DefaultTrustChainFetchingStrategyTest {
protected SubordinateStatementContainer ssContainer(final String subject, final String issuer) {
final Instant expiration = Instant.now().plusSeconds(300);
final SubordinateStatement statement = mock(SubordinateStatement.class);
+ final SubordinateStatementPayload payload = mock(SubordinateStatementPayload.class);
when(statement.getSubject()).thenReturn(subject);
when(statement.getIssuer()).thenReturn(issuer);
+ when(statement.getParsedPayload()).thenReturn(payload);
return new SubordinateStatementContainer(
new SubordinateStatementCacheIdentifier(issuer, subject), statement, expiration, expiration);
}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list