[java-idp-plugin-oidc-op-oidfed] 02/02: Improved and harmonised the trust chain signature verification logic
Henri Mikkonen
henri.mikkonen at iki.fi
Fri Oct 31 14:21:55 UTC 2025
This is an automated email from the git hooks/post-receive script.
hjmikkon pushed a commit to branch main
in repository java-idp-plugin-oidc-op-oidfed.
View the commit online:
https://git.shibboleth.net/view/?p=java-idp-plugin-oidc-op-oidfed.git;a=commit;h=ed95f10f1c4019f9f7c85cdffc2174ef1070a324
commit ed95f10f1c4019f9f7c85cdffc2174ef1070a324
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri Oct 31 16:21:25 2025 +0200
Improved and harmonised the trust chain signature verification logic
The same function is used for validating provided and resolved trust chains
- DefaultTrustChainSignatureValidationFilterStrategy, removed obsolete DefaultTrustAnchorSignatureValidationFilterStrategy
- The new one also validates the entity statement signatures with the public key found from the next statement in the chain
- Draft 10.2: "For each j = 0,...,i-1, verify that the signature of ES[j] validates with a public key in ES[j+1]["jwks"]."
- Improved testing
---
...actTrustEngineSignatureValidationComponent.java | 7 +-
...yloadSignatureValidationCredentialResolver.java | 78 ++++++++++++++++
...efaultProvidedTrustChainValidationStrategy.java | 58 ++++--------
...ustChainSignatureValidationFilterStrategy.java} | 38 ++++++--
...ignatureValidationKeyContainerJwtCriterion.java | 101 +++++++++++++++++++++
.../META-INF/net.shibboleth.idp/postconfig.xml | 12 ++-
.../idp/flows/oidfed/register/register-beans.xml | 33 +++++--
.../flow/oidfed/AbstractFederationFlowTest.java | 35 ++++++-
.../AuthorizeFlowAutomaticRegistrationTest.java | 19 ++++
.../profile/flow/oidfed/RegistrationFlowTest.java | 25 +++--
.../profile/flow/oidfed/ResolveEntityFlowTest.java | 11 +++
.../UserInfoFlowAutomaticRegistrationTest.java | 8 +-
12 files changed, 349 insertions(+), 76 deletions(-)
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/AbstractTrustEngineSignatureValidationComponent.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/AbstractTrustEngineSignatureValidationComponent.java
index 73ec509..463dcf9 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/AbstractTrustEngineSignatureValidationComponent.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/AbstractTrustEngineSignatureValidationComponent.java
@@ -14,6 +14,8 @@
package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
+import java.text.ParseException;
+
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -91,10 +93,11 @@ public class AbstractTrustEngineSignatureValidationComponent extends AbstractIde
log.debug("Successfully validated entity statement for {}", entityId);
return true;
}
- } catch (final SecurityException e) {
+ log.warn("Trust Engine validation failed for {}, issued by {}", entityId,
+ jwt.getJWTClaimsSet().getIssuer());
+ } catch (final SecurityException | ParseException e) {
log.debug("Could not validate entity statement for {}", entityId, e);
}
- log.warn("Trust Engine validation failed for {}", entityId);
return false;
}
}
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultPayloadSignatureValidationCredentialResolver.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultPayloadSignatureValidationCredentialResolver.java
new file mode 100644
index 0000000..af1ad36
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultPayloadSignatureValidationCredentialResolver.java
@@ -0,0 +1,78 @@
+/*
+ * 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;
+
+import java.text.ParseException;
+import java.util.Map;
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.credential.Credential;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+
+/**
+ * Resolves credentials from the jwks-claim located in the signed JWT (entity statement) payload. The JWT is fetched
+ * via {@link SignatureValidationKeyContainerJwtCriterion}.
+ */
+public class DefaultPayloadSignatureValidationCredentialResolver extends BasicJOSEObjectCredentialResolver {
+
+ /** Logger. */
+ @Nonnull
+ private final Logger log = LoggerFactory.getLogger(DefaultPayloadSignatureValidationCredentialResolver.class);
+
+ /** {@inheritDoc} */
+ @Override
+ protected Iterable<Credential> resolveFromSource(@Nullable final CriteriaSet criteriaSet) throws ResolverException {
+ if (criteriaSet == null || !criteriaSet.contains(SignatureValidationKeyContainerJwtCriterion.class)) {
+ throw new ResolverException("CriteriaSet does not contain SignatureValidationKeyContainerJwtCriterion");
+ }
+
+ final SignatureValidationKeyContainerJwtCriterion keyContainer =
+ criteriaSet.get(SignatureValidationKeyContainerJwtCriterion.class);
+ final SignedJWT jwt = keyContainer.getJwt();
+ if (jwt == null) {
+ throw new ResolverException(
+ "SignatureValidationKeyContainerJwtCriterion did not contain an instance of SignedJWT");
+ }
+ try {
+ final Map<String, Object> rawJwks = jwt.getJWTClaimsSet().getJSONObjectClaim("jwks");
+ if (rawJwks == null || rawJwks.isEmpty()) {
+ log.debug("No jwks found from the payload");
+ return CollectionSupport.emptyList();
+ }
+ final JWKSet jwks = JWKSet.parse(rawJwks);
+ return jwks.getKeys().stream()
+ .filter(Objects::nonNull)
+ .map(jwk -> buildJWKCredential(jwk, null))
+ .filter(Objects::nonNull)
+ .map(Credential.class::cast)
+ .toList();
+ } catch (final ParseException e) {
+ throw new ResolverException("Could not parse JWKSet from JOSEObject", e);
+ }
+ }
+
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultProvidedTrustChainValidationStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultProvidedTrustChainValidationStrategy.java
index cd48e81..0bd362a 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultProvidedTrustChainValidationStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultProvidedTrustChainValidationStrategy.java
@@ -23,7 +23,6 @@ import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.opensaml.profile.context.ProfileRequestContext;
-import org.opensaml.security.trust.TrustEngine;
import org.slf4j.Logger;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -33,25 +32,24 @@ import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.constraints.FederationP
import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.constraints.FederationPolicyConstraintHelper;
import net.shibboleth.oidc.metadata.filter.MetadataFilterContext;
import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
import net.shibboleth.shared.component.ComponentInitializationException;
import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.primitive.LoggerFactory;
-import net.shibboleth.shared.resolver.CriteriaSet;
/**
- * Default strategy for validating provided trust chain signatures via {@link TrustEngine} and configurable
- * trust anchor signature validation filter.
+ * Default strategy for validating provided trust chain signatures via configurable signature validation filter and
+ * applying the constraints.
*/
-public class DefaultProvidedTrustChainValidationStrategy
- extends AbstractEntityStatementSignatureValidationComponent
+public class DefaultProvidedTrustChainValidationStrategy extends AbstractIdentifiableInitializableComponent
implements BiPredicate<ProfileRequestContext, List<EntityStatement>> {
/** Class logger. */
@Nonnull private Logger log = LoggerFactory.getLogger(DefaultProvidedTrustChainValidationStrategy.class);
/** Strategy for validating trust anchor's entity configuration signature. */
- @NonnullAfterInit BiFunction<EntityStatement, MetadataFilterContext, EntityStatement>
- trustAnchorSignatureValidationFilterStrategy;
+ @NonnullAfterInit BiFunction<List<List<EntityStatement>>, MetadataFilterContext, List<List<EntityStatement>>>
+ trustChainSignatureValidationFilterStrategy;
/** Map of supported federation policy constraints. */
@NonnullAfterInit private Map<String, FederationPolicyConstraint> federationPolicyConstraints;
@@ -60,15 +58,15 @@ public class DefaultProvidedTrustChainValidationStrategy
@NonnullAfterInit private ObjectMapper objectMapper;
/**
- * Set the strategy for validating trust anchor's entity configuration signature.
+ * Set the strategy for validating trust chain signatures.
*
* @param strategy validation strategy
*/
- public void setTrustAnchorSignatureValidationFilterStrategy(
- @Nonnull final BiFunction<EntityStatement, MetadataFilterContext, EntityStatement> strategy) {
+ public void setTrustChainSignatureValidationFilterStrategy(
+ @Nonnull final BiFunction<List<List<EntityStatement>>, MetadataFilterContext, List<List<EntityStatement>>> strategy) {
checkSetterPreconditions();
- trustAnchorSignatureValidationFilterStrategy = Constraint.isNotNull(strategy,
- "TrustAnchorSignatureValidationFilterStrategy cannot be null");
+ trustChainSignatureValidationFilterStrategy = Constraint.isNotNull(strategy,
+ "TrustChainSignatureValidationFilterStrategy cannot be null");
}
/**
@@ -96,8 +94,8 @@ public class DefaultProvidedTrustChainValidationStrategy
@Override
protected void doInitialize() throws ComponentInitializationException {
super.doInitialize();
- if (trustAnchorSignatureValidationFilterStrategy == null) {
- throw new ComponentInitializationException("TrustAnchorSignatureValidationFilterStrategy cannot be null");
+ if (trustChainSignatureValidationFilterStrategy == null) {
+ throw new ComponentInitializationException("TrustChainSignatureValidationFilterStrategy cannot be null");
}
if (federationPolicyConstraints == null) {
throw new ComponentInitializationException("Map of policy constraints cannot be null");
@@ -117,28 +115,12 @@ public class DefaultProvidedTrustChainValidationStrategy
return false;
}
- final EntityStatement entityConfiguration = trustChain.get(0);
- assert entityConfiguration != null;
- if (!validateStatement(entityConfiguration,
- new CriteriaSet(new SubjectEntityStatementCriterion(entityConfiguration)),
- entityConfiguration.getEntityID().getValue())) {
- log.debug("Entity configuration signature validation failed");
+ final List<List<EntityStatement>> validatedChains =
+ trustChainSignatureValidationFilterStrategy.apply(List.of(trustChain), null);
+ if (validatedChains == null || validatedChains.size() != 1) {
+ log.debug("The trust chain did not pass the signature validation");
return false;
}
- for (int i = 1; i < trustChain.size() - 2; i++) {
- final EntityStatement subordinateStatement = trustChain.get(i);
- final EntityStatement issuerStatement = trustChain.get(i + 1);
- assert subordinateStatement != null;
- assert issuerStatement != null;
- final CriteriaSet criteria = new CriteriaSet();
- criteria.add(new IssuerEntityStatementCriterion(issuerStatement));
- criteria.add(new SubjectEntityStatementCriterion(subordinateStatement));
- if (!validateStatement(subordinateStatement, criteria, subordinateStatement.getEntityID().getValue())) {
- log.debug("Subordinate statement {} signature validation failed", subordinateStatement.getEntityID());
- return false;
- }
- }
-
for (int i = 1; i < trustChain.size() - 1; i++) {
if (!FederationPolicyConstraintHelper.verifyPolicyConstraints(
objectMapper, trustChain.get(i), trustChain.subList(0, i), federationPolicyConstraints)) {
@@ -148,12 +130,6 @@ public class DefaultProvidedTrustChainValidationStrategy
}
}
- final EntityStatement trustAnchor = trustChain.get(trustChain.size() - 1);
- if (!trustAnchor.equals(trustAnchorSignatureValidationFilterStrategy.apply(trustAnchor, null))) {
- log.debug("Trust anchor {} validation failed", trustAnchor.getEntityID());
- return false;
- }
-
return true;
}
}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustAnchorSignatureValidationFilterStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainSignatureValidationFilterStrategy.java
similarity index 61%
rename from idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustAnchorSignatureValidationFilterStrategy.java
rename to idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainSignatureValidationFilterStrategy.java
index b07d2c9..71ee250 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustAnchorSignatureValidationFilterStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainSignatureValidationFilterStrategy.java
@@ -21,30 +21,39 @@ import java.util.function.BiFunction;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
+import org.opensaml.security.trust.TrustEngine;
import org.slf4j.Logger;
+import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
import net.shibboleth.oidc.metadata.filter.MetadataFilterContext;
import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
-import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
import net.shibboleth.shared.component.ComponentInitializationException;
import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
/**
- * Default signature validating filter trust chains. The signature validation is performed by passing the trust anchor
- * entity statement of each trust chain to the configurable validation filter strategy. All the chains whose anchor
- * doesn't pass the validation are filtered out from the result.
+ * Default signature validating filter for trust chains. The signature for each entity statement within trust chain is
+ * verified by using the contents of the jwks-claim of the payload of the next entity statement in the chain until the
+ * final subordinate statement of the chain. The configurable {@link TrustEngine} is used for validating the signature.
+ * The trust engine is fed with the JWT to be verified and the {@link SignatureValidationKeyContainerJwtCriterion}
+ * containing the JWT with the keyset used for validation.
+ *
+ * The trust anchor signature validation is performed by passing the trust anchor entity statement of each trust chain
+ * to the configurable validation filter strategy.
+ *
+ * All the chains that don't pass the full validation are filtered out from the result.
*/
@ThreadSafeAfterInit
-public class DefaultTrustAnchorSignatureValidationFilterStrategy extends AbstractIdentifiableInitializableComponent
+public class DefaultTrustChainSignatureValidationFilterStrategy extends AbstractTrustEngineSignatureValidationComponent
implements BiFunction<List<List<EntityStatement>>, MetadataFilterContext, List<List<EntityStatement>>> {
/** Class logger. */
@Nonnull private Logger log =
- LoggerFactory.getLogger(DefaultTrustAnchorSignatureValidationFilterStrategy.class);
+ LoggerFactory.getLogger(DefaultTrustChainSignatureValidationFilterStrategy.class);
/** Signature validation filter strategy for trust anchor entity statements. */
@NonnullAfterInit private BiFunction<EntityStatement, MetadataFilterContext, EntityStatement>
@@ -82,7 +91,22 @@ public class DefaultTrustAnchorSignatureValidationFilterStrategy extends Abstrac
}
final List<List<EntityStatement>> result = new ArrayList<>();
- for (final List<EntityStatement> trustChain : trustChains) {
+ trustChainLoop: for (final List<EntityStatement> trustChain : trustChains) {
+ for (int i = 0; i < trustChain.size() - 1; i++) {
+ final EntityStatement trustChainEntry = trustChain.get(i);
+ final SignedJWT keyContainer = trustChain.get(i + 1).getSignedStatement();
+ final CriteriaSet criteria =
+ new CriteriaSet(new SignatureValidationKeyContainerJwtCriterion(keyContainer));
+ if (!validateJwt(trustChainEntry.getSignedStatement(), criteria,
+ trustChain.get(i).getEntityID().getValue())) {
+ log.warn("The signature check for {} failed, ignoring trust chain anchored by {}",
+ i == 0 ? "leaf entity configuration" : "subordinate statement",
+ trustChain.get(trustChain.size() - 1).getEntityID().getValue());
+ log.trace("Key container {} could not validate the trust chain entry {}", keyContainer.serialize(),
+ trustChainEntry.getSignedStatement().serialize());
+ continue trustChainLoop;
+ }
+ }
final EntityStatement trustAnchorStatement = trustChain.get(trustChain.size() - 1);
final String entityId = trustAnchorStatement.getEntityID().getValue();
final EntityStatement filteredStatement =
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/SignatureValidationKeyContainerJwtCriterion.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/SignatureValidationKeyContainerJwtCriterion.java
new file mode 100644
index 0000000..c1018c4
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/SignatureValidationKeyContainerJwtCriterion.java
@@ -0,0 +1,101 @@
+/*
+ * 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;
+
+import java.util.Objects;
+
+import javax.annotation.Nullable;
+
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.shared.resolver.Criterion;
+
+/**
+ * An implementation of {@link Criterion} which specifies criteria based on the contents of a {@link SignedJWT} element
+ * containing signature validation keys.
+ */
+public final class SignatureValidationKeyContainerJwtCriterion implements Criterion {
+
+ /** The JWT which serves as the source for credential criteria. */
+ @Nullable private SignedJWT jwt;
+
+ /**
+ * Constructor.
+ *
+ * @param keyContainer the key container criteria to use
+ */
+ public SignatureValidationKeyContainerJwtCriterion(@Nullable final SignedJWT keyContainer) {
+ setJwt(keyContainer);
+ }
+
+ /**
+ * Gets the JWT which is the source of credential criteria.
+ *
+ * @return the JWT credential criteria
+ */
+ @Nullable public SignedJWT getJwt() {
+ return jwt;
+ }
+
+ /**
+ * Sets the JWT which is the source of credential criteria.
+ *
+ * @param keyContainer the key container criteria to use
+ *
+ */
+ public void setJwt(@Nullable final SignedJWT keyContainer) {
+ // Note: we allow JOSEObject to be null to handle case where application context,
+ // other accompanying criteria, etc should be used to resolve credentials.
+ jwt = keyContainer;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("SignatureValidationKeyContainerJwtCriterion [jwt=");
+ builder.append("<contents not displayable>");
+ builder.append("]");
+ return builder.toString();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode() {
+ if (jwt != null) {
+ return jwt.hashCode();
+ }
+ return super.hashCode();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+
+ if (obj == null) {
+ return false;
+ }
+
+ if (obj instanceof SignatureValidationKeyContainerJwtCriterion other) {
+ return Objects.equals(jwt, other.jwt);
+ }
+
+ return false;
+ }
+
+}
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index 43dd825..020249d 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -184,7 +184,17 @@
<bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainExpirationTimeStrategy"/>
</property>
<property name="metadataFilterStrategy">
- <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustAnchorSignatureValidationFilterStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainSignatureValidationFilterStrategy">
+ <property name="trustEngine">
+ <bean class="net.shibboleth.oidc.security.impl.ExplicitKeySignedJWTTrustEngine">
+ <constructor-arg index="0">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultPayloadSignatureValidationCredentialResolver" />
+ </constructor-arg>
+ <constructor-arg index="1">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultPayloadJOSEObjectCredentialResolver" />
+ </constructor-arg>
+ </bean>
+ </property>
<property name="entityStatementSignatureValidationFilterStrategy">
<bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultEntityStatementSignatureValidationFilterStrategy">
<property name="trustEngine">
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-beans.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-beans.xml
index bfcdba0..0781d06 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-beans.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-beans.xml
@@ -47,19 +47,32 @@
<property name="providedTrustChainValidationStrategy">
<bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultProvidedTrustChainValidationStrategy"
p:federationPolicyConstraints-ref="%{idp.oidfed.FederationPolicyConstraints:shibboleth.oidfed.DefaultFederationPolicyConstraints}"
- p:objectMapper-ref="shibboleth.oidfed.policy.JSONObjectMapper"
- p:trustEngine-ref="shibboleth.oidfed.DefaultEntityConfigurationTrustEngine">
- <property name="trustAnchorSignatureValidationFilterStrategy">
- <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultEntityStatementSignatureValidationFilterStrategy">
+ p:objectMapper-ref="shibboleth.oidfed.policy.JSONObjectMapper">
+ <property name="trustChainSignatureValidationFilterStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainSignatureValidationFilterStrategy">
<property name="trustEngine">
<bean class="net.shibboleth.oidc.security.impl.ExplicitKeySignedJWTTrustEngine">
<constructor-arg index="0">
- <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultLocalTrustAnchorCredentialResolver"
- c:cache-ref="shibboleth.oidfed.LocalTrustAnchorsMetadataCache" />
- </constructor-arg>
- <constructor-arg index="1">
- <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultPayloadJOSEObjectCredentialResolver" />
- </constructor-arg>
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultPayloadSignatureValidationCredentialResolver" />
+ </constructor-arg>
+ <constructor-arg index="1">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultPayloadJOSEObjectCredentialResolver" />
+ </constructor-arg>
+ </bean>
+ </property>
+ <property name="entityStatementSignatureValidationFilterStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultEntityStatementSignatureValidationFilterStrategy">
+ <property name="trustEngine">
+ <bean class="net.shibboleth.oidc.security.impl.ExplicitKeySignedJWTTrustEngine">
+ <constructor-arg index="0">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultLocalTrustAnchorCredentialResolver"
+ c:cache-ref="shibboleth.oidfed.LocalTrustAnchorsMetadataCache" />
+ </constructor-arg>
+ <constructor-arg index="1">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultPayloadJOSEObjectCredentialResolver" />
+ </constructor-arg>
+ </bean>
+ </property>
</bean>
</property>
</bean>
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 eaa0934..68b83a4 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
@@ -228,13 +228,22 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
return rpEntityConfiguration(clientId, metadata, authorityHints);
}
+ protected String rpEntityConfiguration(final String clientId, final JWK leafKey,
+ final String... authorityHints) throws URISyntaxException {
+ final OIDCClientMetadata metadata = new OIDCClientMetadata();
+ metadata.setRedirectionURI(new URI(redirectUri));
+ metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
+ return rpEntityConfiguration(clientId, metadata, null, leafKey, authorityHints);
+ }
+
protected String rpEntityConfiguration(final String clientId, final OIDCClientMetadata metadata,
final String... authorityHints) throws URISyntaxException {
- return rpEntityConfiguration(clientId, metadata, null, authorityHints);
+ return rpEntityConfiguration(clientId, metadata, null, leafKey, authorityHints);
}
protected String rpEntityConfiguration(final String clientId, final OIDCClientMetadata metadata,
- final List<Map<String, String>> trustMarks, final String... authorityHints) throws URISyntaxException {
+ final List<Map<String, String>> trustMarks, final JWK leafKey, final String... authorityHints)
+ throws URISyntaxException {
final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(clientId).subject(clientId)
.issueTime(Date.from(Instant.now()))
.expirationTime(Date.from(Instant.now().plusSeconds(300)))
@@ -359,10 +368,15 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
}
protected String subordinateStatement(final String issuer, final Map<String, Object> metadata) {
+ return subordinateStatement(issuer, metadata, leafKey);
+ }
+
+ protected String subordinateStatement(final String issuer, final Map<String, Object> metadata,
+ final JWK subjectKey) {
final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(anchorId).subject(issuer)
.issueTime(Date.from(Instant.now()))
.expirationTime(Date.from(Instant.now().plusSeconds(300)))
- .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+ .claim("jwks", new JWKSet(subjectKey).toJSONObject(true))
.claim("metadata", metadata)
.claim("authority_hints", new String[] { anchorId })
.build();
@@ -449,6 +463,19 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
}
}
+ protected void rpConfigureMockHttpClient(final String clientId, final JWK leafKey) {
+ try {
+ mapResponse(entityConfigurationUrl(clientId),
+ mockResponse(rpEntityConfiguration(clientId, leafKey)));
+ mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+ mapResponse(subordinateStatementUrl(anchorFetchEndpoint, clientId),
+ mockResponse(subordinateStatement(clientId, Map.of("openid_relying_party",
+ new OIDCClientMetadata().toJSONObject()))));
+ } catch (UnsupportedOperationException | IOException | URISyntaxException e) {
+ Assert.fail("Could not initialize mock HTTP client", e);
+ }
+ }
+
protected void rpConfigureMockHttpClient(final String clientId, final OIDCClientMetadata metadata) {
try {
mapResponse(entityConfigurationUrl(clientId), mockResponse(rpEntityConfiguration(clientId, metadata)));
@@ -547,7 +574,7 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
mockResponse(rpSubordinateStatement(anchorId, trustedAnchorKey, intermediateKey, intermediateId,
Map.of("openid_relying_party", (Map<String, Object>) testVector.get("TA")), anchorId)));
mapResponse(subordinateStatementUrl(intermediateId + "/fetch", clientId),
- mockResponse(rpSubordinateStatement(intermediateId, intermediateKey, rpKey, clientId,
+ mockResponse(rpSubordinateStatement(intermediateId, intermediateKey, leafKey, clientId,
Map.of("openid_relying_party", (Map<String, Object>) testVector.get("INT")),
intermediateId)));
} catch (UnsupportedOperationException | IOException | URISyntaxException | ParseException e) {
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java
index 8e7de38..4ce2b70 100644
--- a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java
@@ -19,6 +19,7 @@ import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
+import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.time.Duration;
import java.time.Instant;
@@ -141,6 +142,24 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
Assert.assertEquals(unwrapTrustChainFromAuthorizeCode(successResponse), List.of(clientId, anchorId));
}
+ @Test
+ public void testWithInvalidTrustChain_subordinateKeyNotMatchingEntityConfiguration_signedRequestObject()
+ throws IOException, UnsupportedOperationException, URISyntaxException, NoSuchAlgorithmException {
+ final String clientId = uniqueClientId();
+ rpConfigureMockHttpClient(clientId, initializeNewJwk("RSA", 2048, "mockNewLeafKey"));
+ final FlowExecutionResult result =
+ launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
+ "iss", clientId,
+ "client_id", clientId,
+ "aud", issuer,
+ "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+ "jti", UUID.randomUUID(),
+ "response_type", "code",
+ "scope", "openid profile",
+ "redirect_uri", redirectUri)));
+ Assert.assertEquals(result.getOutcome().getId(), "ErrorView");
+ }
+
@Test
public void testWithValidTrustChain_resolveApiFailsNoFaildback_signedRequestObject()
throws IOException, UnsupportedOperationException, URISyntaxException {
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/RegistrationFlowTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/RegistrationFlowTest.java
index eddf551..e3551ec 100644
--- a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/RegistrationFlowTest.java
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/RegistrationFlowTest.java
@@ -272,7 +272,7 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
public void testValidTrustChain_validMaxLengthInAnchor() throws Exception {
final String clientId = uniqueClientId();
final String trustChain = "[\"" + rpEntityConfiguration(clientId) + "\", \"" +
- subordinateStatement(anchorId, anchorKey, leafKey, clientId, Collections.emptyMap(),
+ subordinateStatement(anchorId, trustedAnchorKey, leafKey, clientId, Collections.emptyMap(),
Map.of("max_path_length", Integer.valueOf(0))) + "\", \"" + trustedAnchorConfiguration() + "\"]";
setRequest("POST", trustChain, "application/trust-chain+json");
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
@@ -291,7 +291,7 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
final String trustChain = "[\"" + rpEntityConfiguration(clientId) + "\", \"" +
subordinateStatement(intermediateId, intermediateKey, leafKey, clientId, Collections.emptyMap(),
Collections.emptyMap()) + "\", \"" +
- subordinateStatement(anchorId, anchorKey, intermediateKey, intermediateId, Collections.emptyMap(),
+ subordinateStatement(anchorId, trustedAnchorKey, intermediateKey, intermediateId, Collections.emptyMap(),
Map.of("max_path_length", Integer.valueOf(0))) + "\", \"" + trustedAnchorConfiguration() + "\"]";
setRequest("POST", trustChain, "application/trust-chain+json");
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
@@ -302,7 +302,7 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
public void testValidTrustChain_validNamingConstraintInAnchor() throws Exception {
final String clientId = uniqueClientId();
final String trustChain = "[\"" + rpEntityConfiguration(clientId) + "\", \"" +
- subordinateStatement(anchorId, anchorKey, leafKey, clientId, Collections.emptyMap(),
+ subordinateStatement(anchorId, trustedAnchorKey, leafKey, clientId, Collections.emptyMap(),
Map.of("naming_constraints", Map.of("permitted", List.of(".federation.local")))) +
"\", \"" + trustedAnchorConfiguration() + "\"]";
setRequest("POST", trustChain, "application/trust-chain+json");
@@ -319,7 +319,7 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
public void testValidTrustChain_invalidNamingConstraintInAnchor() throws Exception {
final String clientId = uniqueClientId();
final String trustChain = "[\"" + rpEntityConfiguration(clientId) + "\", \"" +
- subordinateStatement(anchorId, anchorKey, leafKey, clientId, Collections.emptyMap(),
+ subordinateStatement(anchorId, trustedAnchorKey, leafKey, clientId, Collections.emptyMap(),
Map.of("naming_constraints", Map.of("permitted", List.of(".wrongfederation.local")))) +
"\", \"" + trustedAnchorConfiguration() + "\"]";
setRequest("POST", trustChain, "application/trust-chain+json");
@@ -331,7 +331,7 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
public void testValidTrustChain_validEntityTypeInAnchor() throws Exception {
final String clientId = uniqueClientId();
final String trustChain = "[\"" + rpEntityConfiguration(clientId) + "\", \"" +
- subordinateStatement(anchorId, anchorKey, leafKey, clientId, Collections.emptyMap(),
+ subordinateStatement(anchorId, trustedAnchorKey, leafKey, clientId, Collections.emptyMap(),
Map.of("allowed_entity_types", List.of("openid_relying_party"))) + "\", \"" +
trustedAnchorConfiguration() + "\"]";
setRequest("POST", trustChain, "application/trust-chain+json");
@@ -348,7 +348,7 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
public void testValidTrustChain_invalidEmptyEntityTypeInAnchor() throws Exception {
final String clientId = uniqueClientId();
final String trustChain = "[\"" + rpEntityConfiguration(clientId) + "\", \"" +
- subordinateStatement(anchorId, anchorKey, leafKey, clientId, Collections.emptyMap(),
+ subordinateStatement(anchorId, trustedAnchorKey, leafKey, clientId, Collections.emptyMap(),
Map.of("allowed_entity_types", Collections.emptyList())) + "\", \"" +
trustedAnchorConfiguration() + "\"]";
setRequest("POST", trustChain, "application/trust-chain+json");
@@ -360,7 +360,7 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
public void testValidTrustChain_invalidEntityTypeInAnchor() throws Exception {
final String clientId = uniqueClientId();
final String trustChain = "[\"" + rpEntityConfiguration(clientId) + "\", \"" +
- subordinateStatement(anchorId, anchorKey, leafKey, clientId, Collections.emptyMap(),
+ subordinateStatement(anchorId, trustedAnchorKey, leafKey, clientId, Collections.emptyMap(),
Map.of("allowed_entity_types", List.of("openid_provider"))) + "\", \"" +
trustedAnchorConfiguration() + "\"]";
setRequest("POST", trustChain, "application/trust-chain+json");
@@ -368,6 +368,17 @@ public class RegistrationFlowTest extends AbstractFederationFlowTest {
assertErrorCode(result, "invalid_request");
}
+ @Test
+ public void testInvalidTrustChain_subordinateKeyNotMatchingEntityConfiguration() throws Exception {
+ final String clientId = uniqueClientId();
+ final String trustChain = "[\"" + rpEntityConfiguration(clientId, initializeNewJwk("RSA", 2048, "mockNewKey"))
+ + "\", \"" + subordinateStatement(clientId, Map.of("openid_relying_party",
+ new OIDCClientMetadata().toJSONObject())) + "\", \"" + trustedAnchorConfiguration() + "\"]";
+ setRequest("POST", trustChain, "application/trust-chain+json");
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, "invalid_request");
+ }
+
@Test
public void testInvalidTrustChain_wrongRpEntityConfigurationSignerKey() throws Exception {
final String clientId = uniqueClientId();
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/ResolveEntityFlowTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/ResolveEntityFlowTest.java
index 00551b5..c2049c0 100644
--- a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/ResolveEntityFlowTest.java
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/ResolveEntityFlowTest.java
@@ -81,6 +81,17 @@ public class ResolveEntityFlowTest extends AbstractFederationFlowTest {
Assert.assertNotNull(response.getJWTClaimsSet().getClaim("metadata"));
}
+ @Test
+ public void testRPWithTrustedTrustAnchor_subordinateKeyNotMatchingEntityConfiguration() throws Exception {
+ request.setMethod("GET");
+ final String clientId = uniqueClientId();
+ rpConfigureMockHttpClient(clientId, initializeNewJwk("RSA", 2048, "mockNewLeafKey"));
+ request.setQueryString("sub=" + clientId + "&trust_anchor=" + anchorId + "&entity_type=openid_relying_party");
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, "invalid_request");
+ assertErrorDescriptionContains(result, "NoTrustChainsResolved");
+ }
+
@Test
public void testRPWithTrustedTrustAnchorInvalidMetadata() throws Exception {
request.setMethod("GET");
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/UserInfoFlowAutomaticRegistrationTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/UserInfoFlowAutomaticRegistrationTest.java
index 33b4d69..ec302d1 100644
--- a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/UserInfoFlowAutomaticRegistrationTest.java
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/UserInfoFlowAutomaticRegistrationTest.java
@@ -94,7 +94,7 @@ public class UserInfoFlowAutomaticRegistrationTest extends AbstractFederationFlo
metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
final String rpEntityConfiguration = rpEntityConfiguration(clientId, metadata, List.of(Map.of(
"trust_mark_type", "https://example.org/email-allowing-trust-mark",
- "trust_mark", trustMark)));
+ "trust_mark", trustMark)), leafKey);
final String trustAnchorConfiguration = trustedAnchorConfiguration();
rpConfigureMockHttpClient(clientId, rpEntityConfiguration, trustAnchorConfiguration);
try {
@@ -102,7 +102,7 @@ public class UserInfoFlowAutomaticRegistrationTest extends AbstractFederationFlo
mockResponse(trustMarkIssuerConfiguration(trustMarkIssuerId)));
mapResponse(subordinateStatementUrl(anchorFetchEndpoint, trustMarkIssuerId),
mockResponse(subordinateStatement(trustMarkIssuerId,
- Map.of("federation_entity", CollectionSupport.emptyMap()))));
+ Map.of("federation_entity", CollectionSupport.emptyMap()), trustMarkIssuerKey)));
} catch (UnsupportedOperationException | IOException e) {
Assert.fail("Could not initialize mock HTTP client", e);
}
@@ -142,7 +142,7 @@ public class UserInfoFlowAutomaticRegistrationTest extends AbstractFederationFlo
final OIDCClientMetadata metadata = new OIDCClientMetadata();
metadata.setRedirectionURI(URI.create(redirectUri));
metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
- final String trustMark = TrustChainTestUtil.trustMark(JWSAlgorithm.RS256, anchorKey, anchorId,
+ final String trustMark = TrustChainTestUtil.trustMark(JWSAlgorithm.RS256, trustMarkIssuerKey, trustMarkIssuerId,
clientId, "https://example.org/email-allowing-trust-mark", Instant.now().plusSeconds(300)).serialize();
rpResolveEntityConfigureMockHttpClient(clientId, metadata, List.of(Map.of(
"trust_mark_type", "https://example.org/email-allowing-trust-mark",
@@ -164,7 +164,7 @@ public class UserInfoFlowAutomaticRegistrationTest extends AbstractFederationFlo
final OIDCClientMetadata metadata = new OIDCClientMetadata();
metadata.setRedirectionURI(URI.create(redirectUri));
metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
- final String trustMark = TrustChainTestUtil.trustMark(JWSAlgorithm.RS256, anchorKey, anchorId,
+ final String trustMark = TrustChainTestUtil.trustMark(JWSAlgorithm.RS256, trustMarkIssuerKey, trustMarkIssuerId,
anchorId, "https://example.org/email-allowing-trust-mark", Instant.now().plusSeconds(300)).serialize();
rpResolveEntityConfigureMockHttpClient(clientId, metadata, List.of(Map.of(
"trust_mark_type", "https://example.org/email-allowing-trust-mark",
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list