[java-idp-plugin-oidc-op-oidfed] 03/03: Improved and simplified the default trust chain fetching strategy
Codeberg
noreply at shibboleth.net
Thu Jan 22 11:41:12 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/ea58669e0b2f649e126c46a4b9e75c76e002ca66
commit ea58669e0b2f649e126c46a4b9e75c76e002ca66
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Thu Jan 22 13:40:45 2026 +0200
Improved and simplified the default trust chain fetching strategy
- The function now recognizes locally trusted intermediate authorities with broken authority hints
- Previously solely entity configurations without authority hints were considered as trust anchors
- The locally trusted intermediate authorities were parsed at later stage
- Included initial (not complete) unit tests
---
.../DefaultTrustChainFetchingStrategy.java | 284 ++++++++-------
.../metadata/util/EntityStatementHelper.java | 32 ++
.../DefaultTrustChainFetchingStrategyTest.java | 380 +++++++++++++++++++++
.../flow/oidfed/AbstractFederationFlowTest.java | 19 +-
.../flow/oidfed/EntityConfigurationFlowTest.java | 2 +-
.../oidfed/cache/TrustChainMetadataCacheTest.java | 46 +++
.../credentials/fed-local-intermediate.jwk | 12 +
.../module/conf/oidfed/oidfed-trust-anchors.json | 11 +
8 files changed, 661 insertions(+), 125 deletions(-)
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/remote/trustchain/DefaultTrustChainFetchingStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/remote/trustchain/DefaultTrustChainFetchingStrategy.java
index 4b72d0c..6fed233 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/remote/trustchain/DefaultTrustChainFetchingStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/remote/trustchain/DefaultTrustChainFetchingStrategy.java
@@ -19,9 +19,7 @@ import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
-import java.util.Optional;
import java.util.function.Function;
-import java.util.stream.Stream;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -30,18 +28,18 @@ import org.slf4j.Logger;
import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityConfiguration;
import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubordinateStatement;
import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.IssuerEntityIDCriterion;
import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.PreSelectedTrustChainCriterion;
import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.SubjectEntityIDCriterion;
import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.local.LocalKeyContainer;
-import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.remote.BaseExpirableStatementContainer;
import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.remote.RemoteEntityConfigurationContainer;
import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.remote.RemoteSubordinateStatementContainer;
import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.remote.RemoteTrustChainsContainer;
import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.remote.SubjectEntityStatementCriterion;
import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.constraints.FederationPolicyConstraint;
import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.constraints.FederationPolicyConstraintHelper;
-import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.payload.EntityConfigurationPayload;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.util.EntityStatementHelper;
import net.shibboleth.oidc.metadata.cache.MetadataCache;
import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
@@ -52,6 +50,7 @@ import net.shibboleth.shared.collection.Pair;
import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
import net.shibboleth.shared.component.ComponentInitializationException;
import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.ConstraintViolationException;
import net.shibboleth.shared.primitive.LoggerFactory;
import net.shibboleth.shared.resolver.CriteriaSet;
@@ -234,10 +233,7 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
if (containers.isEmpty()) {
return null;
}
- entityConfiguration = Optional.ofNullable(containers.get(0).getStatement())
- .filter(EntityConfiguration.class::isInstance)
- .map(EntityConfiguration.class::cast)
- .orElse(null);
+ entityConfiguration = containers.get(0).getStatement();
} catch (final MetadataCacheException e) {
log.error("Could not fetch entity configuration for the trust chain", e);
return null;
@@ -251,77 +247,24 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
return null;
}
- final List<List<EntityStatement<?>>> rawChains = populateChain(
- CollectionSupport.listOf(CollectionSupport.listOf(entityConfiguration)), preSelectedChain);
- final List<List<EntityStatement<?>>> trustChains =
- stripIntermediateConfigurations(entityConfiguration, rawChains);
- final List<List<EntityStatement<?>>> result = Stream.concat(trustChains.stream(),
- resolveLocallyTrustedTrustChains(trustChains).stream()).toList();
+ final TrustChainWrapper initialWrapper =
+ new TrustChainWrapper(CollectionSupport.listOf(entityConfiguration));
+ if (initialWrapper.isComplete()) {
+ log.warn("Entity configuration for {} does not contain authority hints, cannot resolve trust chains",
+ entityConfiguration.getSubject());
+ return null;
+ }
+ final List<TrustChainWrapper> populatedWrappers = populateChain(
+ CollectionSupport.listOf(initialWrapper), preSelectedChain);
+ final List<List<EntityStatement<?>>> result = populatedWrappers.stream()
+ .filter(wrapper -> wrapper.isComplete())
+ .map(wrapper -> wrapper.getTrustChain())
+ .toList();
assert result != null; assert invalidExpiration != null; assert validExpiration != null;
return new RemoteTrustChainsContainer(result.isEmpty() ? invalidExpiration : validExpiration,
entityConfiguration.getSubject(), result);
}
- /**
- * Strips the intermediate entity configurations out from the list of trust chains.
- *
- * @param leaf the leaf entity configuration
- * @param chains the list of trust chains to be filtered
- * @return the filtered list of trust chains, intermediate configurations filtered out
- */
- @Nonnull
- private List<List<EntityStatement<?>>> stripIntermediateConfigurations(@Nonnull final EntityStatement<?> leaf,
- @Nonnull @NonnullElements final List<List<EntityStatement<?>>> chains) {
- final List<List<EntityStatement<?>>> result = new ArrayList<>();
- for (final List<EntityStatement<?>> chain : chains) {
- result.add(chain.stream()
- .filter(es -> (es.equals(leaf)
- || !es.getSubject().equals(es.getIssuer())
- || (es.getParsedPayload() instanceof EntityConfigurationPayload ecp
- && ecp.getAuthorityHints() == null)))
- .toList());
- }
- return result;
- }
-
- /**
- * Iterates the given trust chains and checks whether they contain locally trusted trust anchors as intermediate
- * authorities. Such trust chains are returned in a way that locally trusted anchors are the trust anchors (i.e.
- * the final items in the chain).
- *
- * @param chains trust chains to be iterated over
- * @return list of trust chains containing locally trusted trust anchors
- */
- @Nonnull private List<List<EntityStatement<?>>> resolveLocallyTrustedTrustChains(
- @Nonnull @NonnullElements final List<List<EntityStatement<?>>> chains) {
- final List<List<EntityStatement<?>>> result = new ArrayList<>();
- for (final List<EntityStatement<?>> chain : chains) {
- for (int i = 0; i < chain.size() - 1; i++) {
- final EntityStatement<?> statement = chain.get(i);
- assert statement != null;
- final String entityId = chain.get(i).getSubject();
- assert entityId != null;
- if (isLocallyTrusted(statement) && !isTrustAnchor(entityId, result)) {
- try {
- final CriteriaSet criteria = new CriteriaSet(new SubjectEntityIDCriterion(entityId));
- final EntityStatement<?> localAnchorConfiguration =
- getFirstIfFound(entityConfigurationCache.get(criteria));
- if (localAnchorConfiguration != null) {
- final List<EntityStatement<?>> localTrustChain =
- new ArrayList<>(chain.subList(0, chain.indexOf(statement)));
- localTrustChain.add(localAnchorConfiguration);
- result.add(CollectionSupport.copyToList(localTrustChain));
- log.debug("Included a trust chain based on a local trust anchor {}", entityId);
- }
- } catch (final MetadataCacheException e) {
- log.error("Could not resolve entity configuration for {}", entityId);
- }
- }
- }
- }
- return result;
- }
-
/**
* Checks whether the given entity is locally trusted via local trust anchors cache.
*
@@ -343,24 +286,10 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
log.trace("No locally trusted keys found for {}", entityId);
return false;
}
+ log.trace("Locally trusted keys configured for {}", entityId);
return true;
}
- /**
- * Checks whether the given entity is already a trust anchor in any of the given trust chains.
- *
- * @param entityId the entity ID to be verified
- * @param trustChains trust chains to be checked
- * @return true if the entity is already a trust anchor, false otherwise
- */
- private boolean isTrustAnchor(@Nonnull final String entityId,
- @Nonnull final List<List<EntityStatement<?>>> trustChains) {
- return trustChains.stream()
- .filter(chain -> entityId.equals(chain.get(chain.size() - 1).getSubject()))
- .findAny()
- .isPresent();
- }
-
/**
* Recursively populates the given list of trust chains until the last entity statement in each chain doesn't
* contain any authority hints.
@@ -369,21 +298,30 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
* @param preSelectedChain the pre-selected trust chain (containing entity IDs as String).
* @return the list of trust chains being populated
*/
- @Nonnull @NonnullElements private List<List<EntityStatement<?>>> populateChain(
- @Nonnull @NonnullElements final List<List<EntityStatement<?>>> entities,
+ @Nonnull @NonnullElements private List<TrustChainWrapper> populateChain(
+ @Nonnull @NonnullElements final List<TrustChainWrapper> entities,
@Nonnull final List<String> preSelectedChain) {
- final List<List<EntityStatement<?>>> result = new ArrayList<>();
+ final List<TrustChainWrapper> result = new ArrayList<>();
boolean hints = false;
- for (final List<EntityStatement<?>> chain : entities) {
+ for (final TrustChainWrapper chainWrapper : entities) {
+ if (chainWrapper.isComplete()) {
+ result.add(chainWrapper);
+ continue;
+ }
+ final List<EntityStatement<?>> chain = chainWrapper.getTrustChain();
final EntityStatement<?> entityStatement = chain.get(chain.size() - 1);
- final List<String> authorityHints =
- entityStatement.getParsedPayload() instanceof EntityConfigurationPayload ecp ?
- ecp.getAuthorityHints() : null;
- if (authorityHints == null || authorityHints.isEmpty()) {
- result.add(chain);
- } else {
- final List<Pair<EntityStatement<?>, EntityStatement<?>>> authorities = authorityHints.stream()
- .filter(id -> preSelectedChain.isEmpty() || preSelectedChain.contains(id))
+ assert entityStatement != null;
+ if (chain.size() > 2 && isLocallyTrusted(entityStatement)
+ && matchesPreSelectedIfSet(chain, preSelectedChain)) {
+ result.add(chainWrapper.complete());
+ }
+ final List<String> authorityHints = entityStatement instanceof EntityConfiguration configuration ?
+ configuration.getParsedPayload().getAuthorityHints() : null;
+ if (authorityHints != null && !authorityHints.isEmpty()) {
+ final List<Pair<EntityConfiguration, SubordinateStatement>> authorities = authorityHints.stream()
+ .filter(id -> verifyNoLoop(chain, id))
+ .filter(id -> verifyPreSelected(chain, id, preSelectedChain))
+ .filter(id -> !EntityStatementHelper.subjectAlreadyExists(id, chain))
.map(id -> fetchAuthority(entityStatement, id))
.filter(pair -> pair != null && pair.getFirst() != null && pair.getSecond() != null)
.filter(pair -> FederationPolicyConstraintHelper.verifyPolicyConstraints(
@@ -391,10 +329,11 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
.toList();
hints = !authorities.isEmpty();
authorities.forEach(authority -> {
- final ArrayList<EntityStatement<?>> newChain = new ArrayList<>(chain);
+ final ArrayList<EntityStatement<?>> newChain = new ArrayList<>(chain.size() > 2 ?
+ chain.subList(0, chain.size() - 1) : chain);
newChain.add(authority.getSecond());
newChain.add(authority.getFirst());
- result.add(newChain);
+ result.add(new TrustChainWrapper(newChain));
});
}
}
@@ -404,6 +343,59 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
return result;
}
+ /**
+ * Verifies that the given authority is not already included in the given trust chain.
+ *
+ * @param chain trust chain
+ * @param authority entity ID for authority
+ * @return true if not included, false otherwise
+ */
+ protected boolean verifyNoLoop(@Nonnull final List<EntityStatement<?>> chain, @Nullable final String authority) {
+ final List<String> entityIds = EntityStatementHelper.getEntityIds(chain);
+ if (entityIds.contains(authority)) {
+ log.warn("Loop detected: authority hint {} already in the trust chain {} resolved so far", authority,
+ entityIds);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Verifies that the given authority is compliant with the pre-defined trust chain.
+ *
+ * @param chain trust chain
+ * @param authority entity ID for authority
+ * @param preSelectedChain pre-defined trust chain
+ * @return true if authority is compliant, false otherwise
+ */
+ protected boolean verifyPreSelected(@Nonnull final List<EntityStatement<?>> chain, @Nullable final String authority,
+ @Nonnull final List<String> preSelectedChain) {
+ if (preSelectedChain.isEmpty()) {
+ return true;
+ }
+ if (preSelectedChain.size() <= chain.size() || !preSelectedChain.get(chain.size()).equals(authority)) {
+ log.debug("Ignoring authority {} as it doesn't match with the preselected chain {}", authority,
+ preSelectedChain);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Checks if the given trust chain matches with the pre-selected trust chain if if it has been set.
+ *
+ * @param chain trust chain
+ * @param preSelectedChain pre-selected trust chain entity IDs
+ * @return true if pre-selected trust chain is empty or is matching the given trust chain
+ */
+ protected boolean matchesPreSelectedIfSet(@Nonnull final List<EntityStatement<?>> chain,
+ @Nonnull final List<String> preSelectedChain) {
+ if (preSelectedChain.isEmpty()) {
+ return true;
+ }
+ return preSelectedChain.equals(EntityStatementHelper.getEntityIds(chain));
+ }
+
/**
* Fetch authority's entity configuration and subordinate statement for the given entity statement and
* authority hint.
@@ -413,21 +405,24 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
* @return pair consisting of authority's entity configuration and subordinate statement, or null if they could not
* be fetched.
*/
- @Nullable protected Pair<EntityStatement<?>, EntityStatement<?>> fetchAuthority(
+ @Nullable protected Pair<EntityConfiguration, SubordinateStatement> fetchAuthority(
@Nonnull final EntityStatement<?> entity, @Nullable final String authorityHint) {
final String entityId = entity.getSubject();
if (authorityHint == null) {
log.error("Entity ID ({}) or authority hint ({}) may not be null", entityId, authorityHint);
return null;
}
- final CriteriaSet criteria = new CriteriaSet(new SubjectEntityIDCriterion(entityId),
- new IssuerEntityIDCriterion(authorityHint));
try {
- final EntityStatement<?> authorityConfiguration = getFirstIfFound(
- entityConfigurationCache.get(new CriteriaSet(new SubjectEntityIDCriterion(authorityHint))));
- final EntityStatement<?> metadata = getFirstIfFound(subordinateStatementCache.get(criteria));
- return new Pair<>(authorityConfiguration, metadata);
+ final List<RemoteEntityConfigurationContainer> authorityConfigurations =
+ entityConfigurationCache.get(new CriteriaSet(new SubjectEntityIDCriterion(authorityHint)));
+ log.trace("Fetched comfigurations {} for authority {}", authorityConfigurations, authorityHint);
+ final List<RemoteSubordinateStatementContainer> subordinateStatements =
+ subordinateStatementCache.get(new CriteriaSet(new SubjectEntityIDCriterion(entityId),
+ new IssuerEntityIDCriterion(authorityHint)));
+ log.trace("Fetched subordinate statements {} for authority {}", subordinateStatements, authorityHint);
+ return new Pair<>(authorityConfigurations.isEmpty() ? null : authorityConfigurations.get(0).getStatement(),
+ subordinateStatements.isEmpty() ? null : subordinateStatements.get(0).getStatement());
} catch (final MetadataCacheException e) {
log.error("Could not resolve authority hint {} for {}", authorityHint, entityId);
return null;
@@ -435,17 +430,66 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
}
/**
- * Get the first entity statement from the given list if it's not empty.
- *
- * @param statements list of entity statements
- * @return first entity statemenet, or null the list was empty.
+ * Wrapper class containing trust chain (potentially under construction) and a flag for indicating whether it's
+ * complete.
*/
- @Nullable private EntityStatement<?> getFirstIfFound(
- @Nonnull final List<? extends BaseExpirableStatementContainer<?>> statements) {
- if (statements.size() > 0 && statements.get(0).getStatement() instanceof EntityStatement<?> statement) {
- return statement;
+ private class TrustChainWrapper {
+
+ /** Trust chain (potentially incomplete). */
+ private List<EntityStatement<?>> trustChain;
+
+ /** Flag indicating whether the trust chain is complete. */
+ private boolean complete;
+
+ /**
+ * Constructor.
+ *
+ * @param chain trust chain (potentially incomplete) - if the trust anchor (last item at the chain) does not
+ * contain authority hints, the chain is considered as complete
+ * @throws ConstraintViolationException if the last item is not an {@link EntityConfiguration}
+ */
+ protected TrustChainWrapper(@Nonnull final List<EntityStatement<?>> chain)
+ throws ConstraintViolationException {
+ trustChain = chain;
+ if (chain.get(chain.size() - 1) instanceof EntityConfiguration entityConfiguration) {
+ final List<String> authorityHints = entityConfiguration.getParsedPayload().getAuthorityHints();
+ if (authorityHints == null || authorityHints.isEmpty()) {
+ complete = true;
+ } else {
+ complete = false;
+ }
+ } else {
+ throw new ConstraintViolationException("Unexpected entity statement type as last item: "
+ + chain.get(chain.size() - 1));
+ }
+ }
+
+ /**
+ * Get the trust chain.
+ *
+ * @return trust chain
+ */
+ protected List<EntityStatement<?>> getTrustChain() {
+ return trustChain;
}
- return null;
- }
+ /**
+ * Set this wrapper as complete.
+ *
+ * @return this wrapper
+ */
+ protected TrustChainWrapper complete() {
+ complete = true;
+ return this;
+ }
+
+ /**
+ * Get whether this wrapper is complete.
+ *
+ * @return true if complete, false otherwise
+ */
+ protected boolean isComplete() {
+ return complete;
+ }
+ }
}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/util/EntityStatementHelper.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/util/EntityStatementHelper.java
index aac6274..d46e9a1 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/util/EntityStatementHelper.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/util/EntityStatementHelper.java
@@ -16,6 +16,7 @@ package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.util;
import java.net.URI;
import java.util.List;
+import java.util.Objects;
import java.util.Optional;
import javax.annotation.Nonnull;
@@ -93,4 +94,35 @@ public class EntityStatementHelper {
log.warn("Could not fetch entity configuration for {}", entityId);
return null;
}
+
+ /**
+ * Get the list of entity IDs of the entities in the given trust chain. The first item is the entity ID of the
+ * leaf entity.
+ *
+ * @param trustChain trust chain
+ * @return list of entity IDs
+ */
+ @Nonnull public static List<String> getEntityIds(@Nonnull final List<EntityStatement<?>> trustChain) {
+ final List<String> result = trustChain.subList(1, trustChain.size()).stream()
+ .filter(Objects::nonNull)
+ .map(statement -> statement.getSubject())
+ .toList();
+ assert result != null;
+ return result;
+ }
+
+ /**
+ * Checks if the given subject already exists in the trust chain.
+ *
+ * @param subject subject to be looked after
+ * @param trustChain trust chain
+ * @return true if subject already exists in the trust chain, false otherwise
+ */
+ public static boolean subjectAlreadyExists(@Nullable final String subject,
+ @Nonnull final List<EntityStatement<?>> trustChain) {
+ return subject == null ? false : trustChain.stream()
+ .filter(Objects::nonNull)
+ .map(statement -> statement.getSubject())
+ .anyMatch(id -> subject.equals(id));
+ }
}
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/remote/trustchain/DefaultTrustChainFetchingStrategyTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/remote/trustchain/DefaultTrustChainFetchingStrategyTest.java
new file mode 100644
index 0000000..d4f7571
--- /dev/null
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/remote/trustchain/DefaultTrustChainFetchingStrategyTest.java
@@ -0,0 +1,380 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.remote.trustchain;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityConfiguration;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubordinateStatement;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.PreSelectedTrustChainCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.local.LocalKeyContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.remote.RemoteEntityConfigurationContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.remote.RemoteSubordinateStatementContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.remote.RemoteTrustChainsContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.remote.SubordinateStatementCacheIdentifier;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.constraints.FederationPolicyConstraint;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.payload.EntityConfigurationPayload;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.util.EntityStatementHelper;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Unit tests for {@link DefaultTrustChainFetchingStrategy}
+ */
+ at SuppressWarnings("null")
+public class DefaultTrustChainFetchingStrategyTest {
+
+ DefaultTrustChainFetchingStrategy function;
+
+ Function<CriteriaSet, String> criteriaToSubjectEntityIdStrategy;
+ MetadataCache<RemoteEntityConfigurationContainer> entityConfigurationCache;
+ MetadataCache<RemoteSubordinateStatementContainer> subordinateStatementCache;
+ MetadataCache<Map<String, LocalKeyContainer>> localTrustAnchorsCache;
+ Function<CriteriaSet, Duration> criteriaToValidContainerLifetimeStrategy;
+ Function<CriteriaSet, Duration> criteriaToInvalidContainerLifetimeStrategy;
+
+ @SuppressWarnings("unchecked")
+ @BeforeMethod
+ public void initMocks() {
+ criteriaToSubjectEntityIdStrategy = mock(Function.class);
+ entityConfigurationCache = mock(MetadataCache.class);
+ subordinateStatementCache = mock(MetadataCache.class);
+ localTrustAnchorsCache = mock(MetadataCache.class);
+ criteriaToValidContainerLifetimeStrategy = mock(Function.class);
+ criteriaToInvalidContainerLifetimeStrategy = mock(Function.class);
+ }
+
+ public void setup(final Map<String, FederationPolicyConstraint> federationPolicyConstraints) {
+ function = new DefaultTrustChainFetchingStrategy();
+ function.setCriteriaToSubjectEntityIdStrategy(criteriaToSubjectEntityIdStrategy);
+ function.setEntityConfigurationCache(entityConfigurationCache);
+ function.setSubordinateStatementCache(subordinateStatementCache);
+ function.setLocalTrustAnchorsCache(localTrustAnchorsCache);
+ function.setCriteriaToValidContainerLifetimeStrategy(criteriaToValidContainerLifetimeStrategy);
+ function.setCriteriaToInvalidContainerLifetimeStrategy(criteriaToInvalidContainerLifetimeStrategy);
+ function.setFederationPolicyConstraints(federationPolicyConstraints);
+ function.setId("mockFunction");
+ try {
+ function.initialize();
+ } catch (ComponentInitializationException e) {
+ Assert.fail("Could not initialize the function", e);
+ }
+ }
+
+ @Test
+ public void nullCriteria_returnsNull() {
+ setup(new HashMap<>());
+ Assert.assertEquals(function.apply(null), null);
+ }
+
+ @Test
+ public void noValidLifetimeCriteria_returnsNull() {
+ setup(new HashMap<>());
+ when(criteriaToValidContainerLifetimeStrategy.apply(any())).thenReturn(null);
+ when(criteriaToInvalidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ Assert.assertEquals(function.apply(new CriteriaSet()), null);
+ }
+
+ @Test
+ public void noInvalidLifetimeCriteria_returnsNull() {
+ setup(new HashMap<>());
+ when(criteriaToValidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ when(criteriaToInvalidContainerLifetimeStrategy.apply(any())).thenReturn(null);
+ Assert.assertEquals(function.apply(new CriteriaSet()), null);
+ }
+
+ @Test
+ public void noEntityConfigurationResolved_returnsNull() throws MetadataCacheException {
+ setup(new HashMap<>());
+ when(criteriaToValidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ when(criteriaToInvalidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ when(entityConfigurationCache.get(any())).thenReturn(CollectionSupport.emptyList());
+ Assert.assertEquals(function.apply(new CriteriaSet()), null);
+ }
+
+ @Test
+ public void entityConfigurationException_returnsNull() throws MetadataCacheException {
+ setup(new HashMap<>());
+ when(criteriaToValidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ when(criteriaToInvalidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ when(entityConfigurationCache.get(any())).thenThrow(MetadataCacheException.class);
+ Assert.assertEquals(function.apply(new CriteriaSet()), null);
+ }
+
+ @Test
+ public void entityConfigurationNoHints_returnsNull() throws MetadataCacheException {
+ setup(new HashMap<>());
+ when(criteriaToValidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ when(criteriaToInvalidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ final RemoteEntityConfigurationContainer ecContainer =
+ ecContainer("mockEntityId", CollectionSupport.emptyList());
+ when(entityConfigurationCache.get(any())).thenReturn(CollectionSupport.listOf(ecContainer));
+ Assert.assertEquals(function.apply(new CriteriaSet()), null);
+ }
+
+ @Test
+ public void entityConfigurationOneUnresolvableHint_returnsEmptyContainer() throws MetadataCacheException {
+ setup(new HashMap<>());
+ when(criteriaToValidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ when(criteriaToInvalidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ final RemoteEntityConfigurationContainer ecContainer =
+ ecContainer("mockEntityId", CollectionSupport.listOf("https://federation.local/immediate"));
+ when(entityConfigurationCache.get(any())).thenReturn(CollectionSupport.listOf(ecContainer));
+ assertEmptyResult(function.apply(new CriteriaSet()));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void entityConfigurationOneResolvableHint_returnsContainerWithOneChain() throws MetadataCacheException {
+ setup(new HashMap<>());
+ final String leaf = "https://federation.local/leaf";
+ final String anchor = "https://federation.local/immediate";
+ when(criteriaToValidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ when(criteriaToInvalidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ final RemoteEntityConfigurationContainer ecContainer =
+ ecContainer(leaf, CollectionSupport.listOf(anchor, "https://federation.local/other"));
+ final RemoteEntityConfigurationContainer authorityContainer =
+ ecContainer(anchor, CollectionSupport.emptyList());
+ when(entityConfigurationCache.get(any()))
+ .thenReturn(CollectionSupport.listOf(ecContainer), CollectionSupport.listOf(authorityContainer),
+ CollectionSupport.emptyList());
+ final RemoteSubordinateStatementContainer ssContainer = ssContainer(leaf, anchor);
+ when(subordinateStatementCache.get(any())).thenReturn(CollectionSupport.listOf(ssContainer),
+ CollectionSupport.emptyList());
+ final RemoteTrustChainsContainer result = function.apply(new CriteriaSet());
+ Assert.assertNotNull(result);
+ assert result != null;
+ Assert.assertEquals(result.getTrustChains().size(), 1);
+ Assert.assertEquals(EntityStatementHelper.getEntityIds(result.getTrustChains().get(0)),
+ List.of(leaf, anchor));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void entityConfigurationOneResolvableLocalAuthorityWithBrokenHint_returnsContainerWithOneChain()
+ throws MetadataCacheException {
+ setup(new HashMap<>());
+ final String leaf = "https://federation.local/leaf";
+ final String immediate = "https://federation.local/immediate";
+ final String anchor = "https://federation.local/anchor";
+ when(criteriaToValidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ when(criteriaToInvalidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ final RemoteEntityConfigurationContainer ecContainer =
+ ecContainer(leaf, CollectionSupport.listOf(immediate, "https://federation.local/other"));
+ final RemoteEntityConfigurationContainer immediateContainer =
+ ecContainer(immediate, CollectionSupport.listOf(anchor));
+ when(entityConfigurationCache.get(any()))
+ .thenReturn(CollectionSupport.listOf(ecContainer), CollectionSupport.listOf(immediateContainer),
+ CollectionSupport.emptyList());
+ final RemoteSubordinateStatementContainer ssContainer = ssContainer(leaf, immediate);
+ when(subordinateStatementCache.get(any())).thenReturn(CollectionSupport.listOf(ssContainer),
+ CollectionSupport.emptyList());
+ when(localTrustAnchorsCache.get(any())).thenReturn(
+ CollectionSupport.listOf(CollectionSupport.singletonMap(immediate, mock(LocalKeyContainer.class))));
+ final RemoteTrustChainsContainer result = function.apply(new CriteriaSet());
+ Assert.assertNotNull(result);
+ assert result != null;
+ Assert.assertEquals(result.getTrustChains().size(), 1);
+ Assert.assertEquals(EntityStatementHelper.getEntityIds(result.getTrustChains().get(0)),
+ List.of(leaf, immediate));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void entityConfigurationOneResolvableLocalAuthorityWithWorkingHint_returnsContainerWithTwoChains()
+ throws MetadataCacheException {
+ setup(new HashMap<>());
+ final String leaf = "https://federation.local/leaf";
+ final String immediate = "https://federation.local/immediate";
+ final String anchor = "https://federation.local/anchor";
+ when(criteriaToValidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ when(criteriaToInvalidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ final RemoteEntityConfigurationContainer ecContainer =
+ ecContainer(leaf, CollectionSupport.listOf(immediate));
+ final RemoteEntityConfigurationContainer immediateContainer =
+ ecContainer(immediate, CollectionSupport.listOf(anchor));
+ final RemoteEntityConfigurationContainer authorityContainer =
+ ecContainer(anchor, CollectionSupport.emptyList());
+ when(entityConfigurationCache.get(any()))
+ .thenReturn(CollectionSupport.listOf(ecContainer), CollectionSupport.listOf(immediateContainer),
+ CollectionSupport.listOf(authorityContainer), CollectionSupport.emptyList());
+ final RemoteSubordinateStatementContainer ssContainer1 = ssContainer(leaf, immediate);
+ final RemoteSubordinateStatementContainer ssContainer2 = ssContainer(immediate, anchor);
+ when(subordinateStatementCache.get(any())).thenReturn(CollectionSupport.listOf(ssContainer1),
+ CollectionSupport.listOf(ssContainer2), CollectionSupport.emptyList());
+ when(localTrustAnchorsCache.get(any())).thenReturn(
+ CollectionSupport.listOf(CollectionSupport.singletonMap(immediate, mock(LocalKeyContainer.class))),
+ CollectionSupport.emptyList());
+ final RemoteTrustChainsContainer result = function.apply(new CriteriaSet());
+ Assert.assertNotNull(result);
+ assert result != null;
+ Assert.assertEquals(result.getTrustChains().size(), 2);
+ Assert.assertEquals(EntityStatementHelper.getEntityIds(result.getTrustChains().get(0)),
+ List.of(leaf, immediate));
+ Assert.assertEquals(EntityStatementHelper.getEntityIds(result.getTrustChains().get(1)),
+ List.of(leaf, immediate, anchor));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void entityConfigurationOneResolvableHint_matchPreSelected_returnsContainerWithOneChain()
+ throws MetadataCacheException {
+ setup(new HashMap<>());
+ final String leaf = "https://federation.local/leaf";
+ final String anchor = "https://federation.local/immediate";
+ final PreSelectedTrustChainCriterion preSelectedCriterion =
+ new PreSelectedTrustChainCriterion(CollectionSupport.listOf(leaf, anchor));
+ when(criteriaToValidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ when(criteriaToInvalidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ final RemoteEntityConfigurationContainer ecContainer =
+ ecContainer(leaf, CollectionSupport.listOf(anchor, "https://federation.local/other"));
+ final RemoteEntityConfigurationContainer authorityContainer =
+ ecContainer(anchor, CollectionSupport.emptyList());
+ when(entityConfigurationCache.get(any()))
+ .thenReturn(CollectionSupport.listOf(ecContainer), CollectionSupport.listOf(authorityContainer),
+ CollectionSupport.emptyList());
+ final RemoteSubordinateStatementContainer ssContainer = ssContainer(leaf, anchor);
+ when(subordinateStatementCache.get(any())).thenReturn(CollectionSupport.listOf(ssContainer),
+ CollectionSupport.emptyList());
+ final RemoteTrustChainsContainer result = function.apply(new CriteriaSet(preSelectedCriterion));
+ Assert.assertNotNull(result);
+ assert result != null;
+ Assert.assertEquals(result.getTrustChains().size(), 1);
+ Assert.assertEquals(EntityStatementHelper.getEntityIds(result.getTrustChains().get(0)),
+ List.of(leaf, anchor));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void entityConfigurationOneResolvableHint_notMatchingPreSelected_returnsEmptyContainer()
+ throws MetadataCacheException {
+ setup(new HashMap<>());
+ final String leaf = "https://federation.local/leaf";
+ final String anchor = "https://federation.local/immediate";
+ final PreSelectedTrustChainCriterion preSelectedCriterion =
+ new PreSelectedTrustChainCriterion(CollectionSupport.listOf(leaf, "https://federation.local/not"));
+ when(criteriaToValidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ when(criteriaToInvalidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ final RemoteEntityConfigurationContainer ecContainer =
+ ecContainer(leaf, CollectionSupport.listOf(anchor, "https://federation.local/other"));
+ final RemoteEntityConfigurationContainer authorityContainer =
+ ecContainer(anchor, CollectionSupport.emptyList());
+ when(entityConfigurationCache.get(any()))
+ .thenReturn(CollectionSupport.listOf(ecContainer), CollectionSupport.listOf(authorityContainer),
+ CollectionSupport.emptyList());
+ final RemoteSubordinateStatementContainer ssContainer = ssContainer(leaf, anchor);
+ when(subordinateStatementCache.get(any())).thenReturn(CollectionSupport.listOf(ssContainer),
+ CollectionSupport.emptyList());
+ assertEmptyResult(function.apply(new CriteriaSet(preSelectedCriterion)));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void entityConfigurationTwoResolvableHints_returnsContainerWithTwoChains() throws MetadataCacheException {
+ setup(new HashMap<>());
+ final String leaf = "https://federation.local/leaf";
+ final String anchor1 = "https://federation.local/immediate1";
+ final String anchor2 = "https://federation.local/immediate2";
+ when(criteriaToValidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ when(criteriaToInvalidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ final RemoteEntityConfigurationContainer ecContainer =
+ ecContainer(leaf, CollectionSupport.listOf(anchor1, anchor2));
+ final RemoteEntityConfigurationContainer authorityContainer1 =
+ ecContainer(anchor1, CollectionSupport.emptyList());
+ final RemoteEntityConfigurationContainer authorityContainer2 =
+ ecContainer(anchor2, CollectionSupport.emptyList());
+ when(entityConfigurationCache.get(any()))
+ .thenReturn(CollectionSupport.listOf(ecContainer), CollectionSupport.listOf(authorityContainer1),
+ CollectionSupport.listOf(authorityContainer2));
+ final RemoteSubordinateStatementContainer ssContainer1 = ssContainer(leaf, anchor1);
+ final RemoteSubordinateStatementContainer ssContainer2 = ssContainer(leaf, anchor2);
+ when(subordinateStatementCache.get(any())).thenReturn(CollectionSupport.listOf(ssContainer1),
+ CollectionSupport.listOf(ssContainer2));
+ final RemoteTrustChainsContainer result = function.apply(new CriteriaSet());
+ Assert.assertNotNull(result);
+ assert result != null;
+ Assert.assertEquals(result.getTrustChains().size(), 2);
+ Assert.assertEquals(EntityStatementHelper.getEntityIds(result.getTrustChains().get(0)),
+ List.of(leaf, anchor1));
+ Assert.assertEquals(EntityStatementHelper.getEntityIds(result.getTrustChains().get(1)),
+ List.of(leaf, anchor2));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void entityConfigurationAuthorityLoop_returnsEmptyContantainer() throws MetadataCacheException {
+ setup(new HashMap<>());
+ final String leaf = "https://federation.local/leaf";
+ final String anchor1 = "https://federation.local/immediate1";
+ final String anchor2 = "https://federation.local/immediate2";
+ when(criteriaToValidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ when(criteriaToInvalidContainerLifetimeStrategy.apply(any())).thenReturn(Duration.ofMinutes(5));
+ final RemoteEntityConfigurationContainer ecContainer =
+ ecContainer(leaf, CollectionSupport.listOf(anchor1));
+ final RemoteEntityConfigurationContainer authorityContainer1 =
+ ecContainer(anchor1, CollectionSupport.listOf(anchor2));
+ final RemoteEntityConfigurationContainer authorityContainer2 =
+ ecContainer(anchor2, CollectionSupport.listOf(anchor1));
+ when(entityConfigurationCache.get(any()))
+ .thenReturn(CollectionSupport.listOf(ecContainer), CollectionSupport.listOf(authorityContainer1),
+ CollectionSupport.listOf(authorityContainer2));
+ final RemoteSubordinateStatementContainer ssContainer1 = ssContainer(leaf, anchor1);
+ final RemoteSubordinateStatementContainer ssContainer2 = ssContainer(anchor1, anchor2);
+ final RemoteSubordinateStatementContainer ssContainer3 = ssContainer(anchor2, anchor1);
+ when(subordinateStatementCache.get(any())).thenReturn(CollectionSupport.listOf(ssContainer1),
+ CollectionSupport.listOf(ssContainer2), CollectionSupport.listOf(ssContainer3));
+ assertEmptyResult(function.apply(new CriteriaSet()));
+ }
+
+ protected void assertEmptyResult(final RemoteTrustChainsContainer container) {
+ Assert.assertNotNull(container);
+ Assert.assertEquals(container.getTrustChains(), CollectionSupport.emptyList());
+ }
+
+ protected RemoteEntityConfigurationContainer ecContainer(final String id, final List<String> authorityHints) {
+ final Instant expiration = Instant.now().plusSeconds(300);
+ final EntityConfiguration configuration = mock(EntityConfiguration.class);
+ final EntityConfigurationPayload payload = mock(EntityConfigurationPayload.class);
+ when(payload.getAuthorityHints()).thenReturn(authorityHints);
+ when(configuration.getParsedPayload()).thenReturn(payload);
+ when(configuration.getSubject()).thenReturn(id);
+ return new RemoteEntityConfigurationContainer(id, configuration, expiration, expiration);
+ }
+
+ protected RemoteSubordinateStatementContainer ssContainer(final String subject, final String issuer) {
+ final Instant expiration = Instant.now().plusSeconds(300);
+ final SubordinateStatement statement = mock(SubordinateStatement.class);
+ when(statement.getSubject()).thenReturn(subject);
+ when(statement.getIssuer()).thenReturn(issuer);
+ return new RemoteSubordinateStatementContainer(
+ new SubordinateStatementCacheIdentifier(issuer, subject), statement, expiration, expiration);
+ }
+}
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 5cb4e4f..b56da5c 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
@@ -95,6 +95,7 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
protected final String redirectUri = "https://rp.federation.local/cb";
protected final String clientIdPattern = "https://testrp%s.federation.local";
protected final String intermediateIdPattern = "https://intermediate-authority%s.federation.local";
+ protected final String trustedIntermediateId = "https://local-trusted-intermediate-authority.federation.local";
protected final String anchorId = "https://trust-anchor.federation.local";
protected final String anchorFetchEndpoint = anchorId + "/fetch";
protected final String anchorResolveEndpoint = anchorId + "/resolve";
@@ -107,6 +108,7 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
protected JWK anchorKey;
protected JWK trustedAnchorKey;
protected JWK intermediateKey;
+ protected JWK trustedIntermediateKey;
protected JWK trustMarkIssuerKey;
protected String subject = "jdoe";
@@ -129,6 +131,11 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
.privateKey(localAnchor.getPrivateKey())
.keyID("locallyTrustedAnchorKey")
.build();
+ final BasicJWKCredential localIntermediate = loadCredential("/credentials/fed-local-intermediate.jwk");
+ trustedIntermediateKey = new RSAKey.Builder((RSAPublicKey) localIntermediate.getPublicKey())
+ .privateKey(localIntermediate.getPrivateKey())
+ .keyID("locallyTrustedIntermediateKey")
+ .build();
intermediateKey = initializeNewJwk("RSA", 2048, "mockIntermediateKey");
trustMarkIssuerKey = initializeNewJwk("RSA", 2048, "mockTrustMarkIssuerKey");
}
@@ -362,17 +369,21 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
}
protected String intermediateConfiguration(final String intermediateId) {
+ return intermediateConfiguration(intermediateId, new JWKSet(intermediateKey), intermediateKey);
+ }
+
+ protected String intermediateConfiguration(final String intermediateId, final JWKSet jwks, final JWK signerKey) {
final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(intermediateId).subject(intermediateId)
.issueTime(Date.from(Instant.now()))
.expirationTime(Date.from(Instant.now().plusSeconds(300)))
- .claim("jwks", new JWKSet(intermediateKey).toJSONObject(true))
+ .claim("jwks", jwks.toJSONObject(true))
.claim("metadata", Map.of("federation_entity", Map.of("federation_fetch_endpoint",
intermediateId + "/fetch")))
.claim("authority_hints", new String[] { anchorId })
.build();
- final EntityStatement<?> anchorConfiguration =
- TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, intermediateKey, claimsSet);
- return anchorConfiguration.getJwt().serialize();
+ final EntityStatement<?> intermediateConfiguration =
+ TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, signerKey, claimsSet);
+ return intermediateConfiguration.getJwt().serialize();
}
protected String subordinateStatement(final String issuer, final Map<String, Object> metadata) {
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/EntityConfigurationFlowTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/EntityConfigurationFlowTest.java
index fae03f6..18e8fda 100644
--- a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/EntityConfigurationFlowTest.java
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/EntityConfigurationFlowTest.java
@@ -161,7 +161,7 @@ public class EntityConfigurationFlowTest extends AbstractFederationFlowTest {
final EntityConfigurationPayload payload = (EntityConfigurationPayload) entityStatement.getParsedPayload();
Assert.assertEquals(payload.getAuthorityHints(),
List.of("https://anchor1.example.org","https://anchor2.example.org"));
- Assert.assertEquals(payload.getTrustAnchorHints(), List.of(anchorId));
+ Assert.assertEquals(payload.getTrustAnchorHints(), List.of(anchorId, trustedIntermediateId));
final List<Map<String,String>> trustMarkMap = payload.getTrustMarks();
Assert.assertNotNull(trustMarkMap);
assert trustMarkMap != null;
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 35d2bb7..432e0c8 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
@@ -185,4 +185,50 @@ public class TrustChainMetadataCacheTest extends AbstractFederationFlowTest {
Assert.fail("Could not resolve entity configuration", e);
}
}
+
+ @Test
+ public void testValidTrustChainResolved_intermediateWithBrokenAuthorityHint()
+ throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+ final String entityId = uniqueClientId();
+
+ 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()))
+ .claim("authority_hints", List.of(trustedIntermediateId));
+ final String entityConfiguration = TrustChainTestUtil.signedJwt(
+ JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+ mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+ mapResponse(entityConfigurationUrl(trustedIntermediateId),
+ mockResponse(intermediateConfiguration(trustedIntermediateId,
+ new JWKSet(List.of(intermediateKey, trustedIntermediateKey)), trustedIntermediateKey)));
+
+ final JWTClaimsSet.Builder builder2 = new JWTClaimsSet.Builder().issuer(trustedIntermediateId)
+ .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, trustedIntermediateKey, "entity-statement+jwt", builder2.build()).serialize();
+
+ mapResponse(subordinateStatementUrl(trustedIntermediateId + "/fetch", entityId),
+ mockResponse(subordinateStatement));
+
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+
+ try {
+ final List<RemoteTrustChainsContainer> result =
+ trustChainCache.get(new CriteriaSet(new SubjectEntityIDCriterion(entityId)));
+ Assert.assertNotNull(result);
+ Assert.assertEquals(result.size(), 1);
+ Assert.assertNotNull(result.get(0).getTrustChains());
+ Assert.assertEquals(result.get(0).getTrustChains().size(), 1);
+ } catch (MetadataCacheException e) {
+ Assert.fail("Could not resolve entity configuration", e);
+ }
+ }
+
}
diff --git a/idp-oidfed-op-impl/src/test/resources/credentials/fed-local-intermediate.jwk b/idp-oidfed-op-impl/src/test/resources/credentials/fed-local-intermediate.jwk
new file mode 100644
index 0000000..7ba5321
--- /dev/null
+++ b/idp-oidfed-op-impl/src/test/resources/credentials/fed-local-intermediate.jwk
@@ -0,0 +1,12 @@
+{
+ "p":"vGxtVhQO5fnZ3FRA48ECw8y0Oojketpf0kM8dtTZpao3BYR5xZDJ7tt8ebGCpRfMWmy48PJqIR2-hAAe_i6GnX9mmlIx_fkur0aoMAx-M1kNHY3Dkr_NL6ulBUBj0b-1G_36a4BoLybflhNaFxmL2mznqUgELOPbu5oDyTyCqMk",
+ "kty":"RSA",
+ "q":"xbJas9_2ntoPXuLNX-MUFj6uxmuU1atmHO6VtC6tL7wp3uuHq1QuPiBt6Ik0Emh0ShuES9mrnXrBbc8vVw9Z_HeeGrW6t26tYN10mNXJsO69_nOiN1IL1Sb_sPRsYcxPi02gZAPKxXcqg4IfKY2JGh5Z-_UukGjUlPVENK3eM-s",
+ "d":"JGCJMehmnZScKLpK2IXtRsGGIbSvb0CG5kP8L8w-CN-K6FlbeKRGqHyNY7fojVjnbi-hR5O_TmFvhXmwgJOwGfX550HRU3sMXyi7WhAXzYfoJQivElFqUosWForIETHaU5mhZGpT-emIcxgOpPUw_3zGdUzzGFhxn4IIFEELX0C5JzZqi8fAx-d3UCp7bHJKgh8qF_agkXnbU4aHdHpq9NEuATTY8U9Qos6clGfAK3BlRMnb5I-uFGOyrbjUYnbDVyQi26yUhJ6yQtttFGWam6yDAjpLFJYXpGdcbf4ibGPtl-32b7eDRuQsOqSU_kXBktT_8DlIZ1n-U7BrhrQB",
+ "e":"AQAB",
+ "kid":"locallyTrustedIntermediateKey",
+ "qi":"l1mhiM0lvZMZongGLwv9r-GOzu1_LhYxq3KBXi_BHgoM7DR3aVGyCEOthFQTNnKb2laALaxVyqrTGGl4zaPGani1D7YzvIWOk4610U1V7Tykr86H7geud7PC8stF_XzwDwx9pYu-sMH7EdmTPQQPo3OQdHECEoVHD36Gp609RUo",
+ "dp":"FV-PP3ZjAj9HMTD1c2BPefpcb09b63ud5vHth-U5Ewut1hhi38A-x7Np-Tvjf-qlKZSvndVBqKQBGmQRH_ATIQZ0kwjD1vVPEF7JcTnZjuWJEVMlXh4XnlKwE1pseDxwxM4Ye91C9CZKsnFbhMdHD-3OkWsz3guyvyMHTupoP9E",
+ "dq":"BArNh_gAVucmgU4p3NgLxRirAiuY83V5tQW70d9SczNpt8EhGQOznlmTZbnIcsfn-MvvPI3K0IF-Cvy9fw41TNA1T15_3thIez6L78QPR6rZ_6XlnzPyQf12JOwaezVuOu7vZJwfPUJegCnc8UCPmRUdyeUNeq5qClGpf1o-vNk",
+ "n":"kYK2ScZiF-zm9VnOS0MXMrszjOtEj7_YmyuJzyG3VHZPH5EPR_79MwzXgRUxFQXTHFw93NQbiIL3m6TX6RDCtphPdO7iVTF_aNRqCy3Hdn7Ws8NQbAHyj9WihoN6b-euFc_T-C7hkPYJcoIG9A9X0rcXyE2hlS4n386eWSR-gTHjvoZQ_M63Eoo4FZVx_8213Qmx0Pwypi-iZ1mW745TI1FvuYzZfZk0xGdfxsgJzQLnXPm_hcQJKMcOFPxdqw0xLqKdByp2LKjrqlOIoZKgTOeWvFr_TV21vKC5ijp1Nh4POwkI-urAl6fmELx_Mn_z7ZvwUhefzznnRgL6xt77gw"
+}
diff --git a/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed-trust-anchors.json b/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed-trust-anchors.json
index 5464750..ff5a6b9 100644
--- a/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed-trust-anchors.json
+++ b/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed-trust-anchors.json
@@ -9,5 +9,16 @@
"n":"n0-NFV06ZDKLo1v8KrSJsQ8bbLEffVJw1F5jGXqrKh_4PpBt9FmyWY3gIA9aK1p1WneMaWRNlM1EObierCr0EdXCQbgpKorrPqxiwyl6cOMIH4fN_9uWGqD2HlyGcjcESrNjZz75tNr_9oegh6fWSMgrxyySpU38ALWUX1ZuNS8A4tj8XdJSbSHqftf7qOdgzuy0yaD5h7NwoBCRPOIY88vOLHkcQ4nYdkk8GLSIf5GgGb7JFiPuFHN7pK---LNnFBifag2wbEZ9nnAcAol4jc2gF7zq2mqhMSlbIVmTRj4Y9wxh3DPbmC8xZ-8nbhPmgi4vlij9JWJGEvfLuXaMGQ"
}
]
+ },
+ "https://local-trusted-intermediate-authority.federation.local": {
+ "keys": [
+ {
+ "alg": "RS256",
+ "kty":"RSA",
+ "e":"AQAB",
+ "kid":"locallyTrustedIntermediateKey",
+ "n":"kYK2ScZiF-zm9VnOS0MXMrszjOtEj7_YmyuJzyG3VHZPH5EPR_79MwzXgRUxFQXTHFw93NQbiIL3m6TX6RDCtphPdO7iVTF_aNRqCy3Hdn7Ws8NQbAHyj9WihoN6b-euFc_T-C7hkPYJcoIG9A9X0rcXyE2hlS4n386eWSR-gTHjvoZQ_M63Eoo4FZVx_8213Qmx0Pwypi-iZ1mW745TI1FvuYzZfZk0xGdfxsgJzQLnXPm_hcQJKMcOFPxdqw0xLqKdByp2LKjrqlOIoZKgTOeWvFr_TV21vKC5ijp1Nh4POwkI-urAl6fmELx_Mn_z7ZvwUhefzznnRgL6xt77gw"
+ }
+ ]
}
}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list