[java-idp-plugin-oidc-op-oidfed] 01/03: Profile configuration option to control remote trust mark validation

Codeberg noreply at shibboleth.net
Wed May 6 12:57:04 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/567d19b477eed55752b457e859cdc37ee969de12

commit 567d19b477eed55752b457e859cdc37ee969de12
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Wed May 6 14:38:01 2026 +0300

    Profile configuration option to control remote trust mark validation
    
    - OIDFederationTrustMarkValidatingProfileConfiguration contains 'remoteTrustMarkValidation' flag
      - Initially implemented in AbstractOIDFederationRegistrationProfileConfiguration, defaults to true
    - RelyingPartyTrustChainContext contains a map of verifiedTrustMarkIssuers (entity configurations of trust mark issuers)
      - Populated during the trust mark resolution
      - Exploited by the metadata cache for Trust Mark Status
---
 ...ionTrustMarkValidatingProfileConfiguration.java |  10 +
 .../RemoteTrustMarkValidationPredicate.java        |  43 ++++
 .../context/RelyingPartyTrustChainContext.java     |  21 ++
 .../cache/BaseExpirableStatementContainer.java     |   5 +-
 ...tEntityStatementContentValidationCondition.java |  29 ++-
 .../cache/trustmark/TrustMarkStatusContainer.java  |  16 +-
 ...FederationRegistrationProfileConfiguration.java |  32 +++
 .../op/oidfed/profile/impl/ResolveTrustMarks.java  |  13 +-
 .../op/oidfed/profile/impl/ValidateTrustMarks.java | 247 +++++++++++++++++++++
 .../DefaultTrustMarkStatusCredentialResolver.java  | 102 +++++++++
 .../META-INF/net.shibboleth.idp/postconfig.xml     |  42 +++-
 .../oidfed/metadata-lookup-ext-oidfed-beans.xml    |   5 +
 .../oidfed/metadata-lookup-ext-oidfed-flow.xml     |   3 +
 .../idp/flows/oidfed/register/register-beans.xml   |   3 +
 .../flow/oidfed/AbstractFederationFlowTest.java    |  23 +-
 .../UserInfoFlowAutomaticRegistrationTest.java     |  14 ++
 16 files changed, 589 insertions(+), 19 deletions(-)

diff --git a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/OIDFederationTrustMarkValidatingProfileConfiguration.java b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/OIDFederationTrustMarkValidatingProfileConfiguration.java
index b2ab1fe..a047dfc 100644
--- a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/OIDFederationTrustMarkValidatingProfileConfiguration.java
+++ b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/OIDFederationTrustMarkValidatingProfileConfiguration.java
@@ -61,4 +61,14 @@ public interface OIDFederationTrustMarkValidatingProfileConfiguration extends OA
     @Positive @Nonnull
     Duration getMaximumTrustMarkLifetime(@Nullable final ProfileRequestContext profileRequestContext);
 
+    /**
+     * Get whether trust marks should be remotely validated.
+     * 
+     * @param profileRequestContext profile request context
+     * 
+     * @return whether trust marks should be remotely validated
+     */
+    @ConfigurationSetting(name="remoteTrustMarkValidation")
+    boolean isRemoteTrustMarkValidation(@Nullable final ProfileRequestContext profileRequestContext);
+
 }
diff --git a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/navigate/RemoteTrustMarkValidationPredicate.java b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/navigate/RemoteTrustMarkValidationPredicate.java
new file mode 100644
index 0000000..5ef0467
--- /dev/null
+++ b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/navigate/RemoteTrustMarkValidationPredicate.java
@@ -0,0 +1,43 @@
+/*
+ * 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.profile.config.navigate;
+
+import java.util.Optional;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.OIDFederationTrustMarkValidatingProfileConfiguration;
+import net.shibboleth.profile.context.logic.AbstractRelyingPartyPredicate;
+
+/**
+ * A predicate implementation that forwards to
+ * {@link OIDFederationTrustMarkValidatingProfileConfiguration#isRemoteTrustMarkValidation(ProfileRequestContext)},
+ * defaults to true.
+ */
+public class RemoteTrustMarkValidationPredicate extends AbstractRelyingPartyPredicate {
+    
+    /** {@inheritDoc} */
+    @Override
+    public boolean test(@Nullable final ProfileRequestContext input) {
+        return Optional.ofNullable(getRelyingPartyContextLookupStrategy().apply(input))
+                .map(rpc -> rpc.getProfileConfig())
+                .filter(OIDFederationTrustMarkValidatingProfileConfiguration.class::isInstance)
+                .map(OIDFederationTrustMarkValidatingProfileConfiguration.class::cast)
+                .map(pc -> pc.isRemoteTrustMarkValidation(input))
+                .orElse(true);
+    }
+}
diff --git a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/context/RelyingPartyTrustChainContext.java b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/context/RelyingPartyTrustChainContext.java
index c850ea7..5677288 100644
--- a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/context/RelyingPartyTrustChainContext.java
+++ b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/context/RelyingPartyTrustChainContext.java
@@ -15,6 +15,7 @@
 package net.shibboleth.idp.plugin.oidc.op.oidfed.profile.context;
 
 import java.time.Instant;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -27,6 +28,7 @@ import com.nimbusds.jwt.SignedJWT;
 
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.TrustedRemoteResolverEntity;
+import net.shibboleth.shared.annotation.constraint.Live;
 
 /**
  * Subcontext carrying information for trust chains related to a relying party.
@@ -54,12 +56,22 @@ public final class RelyingPartyTrustChainContext extends BaseContext {
     /** Verified trust marks for the selected trust chain. */
     @Nullable private Map<String, List<SignedJWT>> verifiedTrustMarks;
 
+    /** Verified trust mark issuers. */
+    @Nonnull @Live private Map<String, EntityStatement<?>> verifiedTrustMarkIssuers;
+
     /** All previously selected but rejected trust chains. */
     @Nullable private List<List<EntityStatement<?>>> rejectedTrustChains;
 
     /** All already attempted trusted remote resolver entities. */
     @Nullable private List<TrustedRemoteResolverEntity> attemptedTrustedRemoteResolverEntities;
 
+    /**
+     * Constructor.
+     */
+    public RelyingPartyTrustChainContext() {
+        verifiedTrustMarkIssuers = new HashMap<>();
+    }
+
     /**
      * Get the trust chain provided within the request.
      * 
@@ -213,6 +225,15 @@ public final class RelyingPartyTrustChainContext extends BaseContext {
         return this;
     }
 
+    /**
+     * Get the verified trust mark issuers.
+     * 
+     * @return verified trust mark issuers
+     */
+    @Nonnull @Live public Map<String, EntityStatement<?>> getVerifiedTrustMarkIssuers() {
+        return verifiedTrustMarkIssuers;
+    }
+
     /**
      * Get the previously selected but rejected trust chains for the relying party.
      * 
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/BaseExpirableStatementContainer.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/BaseExpirableStatementContainer.java
index d5bb2dd..d975cfa 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/BaseExpirableStatementContainer.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/BaseExpirableStatementContainer.java
@@ -21,7 +21,7 @@ import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.BaseJWTWrapper;
-import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.payload.BaseExpirableSubjectPayload;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.BasePayload;
 import net.shibboleth.shared.logic.Constraint;
 
 /**
@@ -29,8 +29,7 @@ import net.shibboleth.shared.logic.Constraint;
  * 
  * @param <T> wrapped statement
  */
-public abstract class BaseExpirableStatementContainer
-    <T extends BaseJWTWrapper<? extends BaseExpirableSubjectPayload>>
+public abstract class BaseExpirableStatementContainer<T extends BaseJWTWrapper<? extends BasePayload>>
     extends BaseExpirableMetadataContainer
     implements Serializable {
 
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/DefaultEntityStatementContentValidationCondition.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/DefaultEntityStatementContentValidationCondition.java
index c071569..8d9a03a 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/DefaultEntityStatementContentValidationCondition.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/DefaultEntityStatementContentValidationCondition.java
@@ -28,6 +28,8 @@ import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.SignedJWT;
 
 import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.BaseJWTWrapper;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.BasePayload;
 import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
 import net.shibboleth.oidc.jwt.claims.JWTValidationException;
 import net.shibboleth.oidc.metadata.filter.MetadataFilterContext;
@@ -64,6 +66,10 @@ public class DefaultEntityStatementContentValidationCondition
     /** Supplier for the Current HTTP request, if available. */
     @NonnullAfterInit private NonnullSupplier<HttpServletRequest> httpServletRequestSupplier;
 
+    @NonnullAfterInit
+    private BiPredicate<BaseExpirableStatementContainer<?>, BaseJWTWrapper<? extends BasePayload>>
+        containerIdValidationStrategy;
+
     /**
      * Set the claims validator to use for validating the entity statement claims.
      * 
@@ -91,7 +97,20 @@ public class DefaultEntityStatementContentValidationCondition
      */
     public void setHttpServletRequestSupplier(@Nonnull final NonnullSupplier<HttpServletRequest> requestSupplier) {
         checkSetterPreconditions();
-        httpServletRequestSupplier = Constraint.isNotNull(requestSupplier, "Http servlet request supplier cannot be null");
+        httpServletRequestSupplier = Constraint.isNotNull(requestSupplier,
+                "Http servlet request supplier cannot be null");
+    }
+
+    /**
+     * Set the validation strategy for the container identifier.
+     * 
+     * @param strategy validation strategy
+     */
+    public void setContainerIdValidationStrategy(@Nonnull final
+            BiPredicate<BaseExpirableStatementContainer<?>, BaseJWTWrapper<? extends BasePayload>> strategy) {
+        checkSetterPreconditions();
+        containerIdValidationStrategy = Constraint.isNotNull(strategy,
+                "Container ID validation strategy cannot be null");
     }
 
     /** {@inheritDoc} */
@@ -107,6 +126,9 @@ public class DefaultEntityStatementContentValidationCondition
         if (httpServletRequestSupplier == null) {
             throw new ComponentInitializationException("Http servlet request supplier cannot be null");
         }
+        if (containerIdValidationStrategy == null) {
+            throw new ComponentInitializationException("Container ID validation strategy cannot be null");
+        }
     }
 
     /** {@inheritDoc} */
@@ -121,9 +143,8 @@ public class DefaultEntityStatementContentValidationCondition
 
         final var wrapper = responseContainer.getStatement();
         assert wrapper != null;
-        if (!responseContainer.getEntityId().equals(wrapper.getParsedPayload().getSubject())) {
-            log.warn("Entity statement subject {} does not match with the requested entity ID {}",
-                    wrapper.getParsedPayload().getSubject(), responseContainer.getEntityId());
+        if (!containerIdValidationStrategy.test(responseContainer, wrapper)) {
+            log.warn("Container ID validation failed  with the requested ID {}", responseContainer.getEntityId());
             return false;
         }
         final ProfileRequestContext profileRequestContext =
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/trustmark/TrustMarkStatusContainer.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/trustmark/TrustMarkStatusContainer.java
index f461aa7..0193772 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/trustmark/TrustMarkStatusContainer.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/trustmark/TrustMarkStatusContainer.java
@@ -15,20 +15,24 @@
 package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.trustmark;
 
 import java.io.Serializable;
+import java.text.ParseException;
 import java.time.Instant;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
+import com.nimbusds.jwt.JWTClaimsSet;
+
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.TrustMarkStatus;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.BaseExpirableStatementContainer;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
 import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.ConstraintViolationException;
 
 /**
  * A container class for metadata caches carrying ID and statement details related to Trust Mark status.
  */
-public class TrustMarkStatusContainer extends BaseExpirableStatementContainer implements Serializable {
+public class TrustMarkStatusContainer extends BaseExpirableStatementContainer<TrustMarkStatus> implements Serializable {
 
     /** Serial version UID. */
     private static final long serialVersionUID = 756269369356884270L;
@@ -62,7 +66,15 @@ public class TrustMarkStatusContainer extends BaseExpirableStatementContainer im
 
     /** {@inheritDoc} */
     @Nonnull @NotEmpty public String getEntityId() {
-        return identifier.getEndpoint(); //TODO: revisit
+        try {
+            final JWTClaimsSet claimsSet = identifier.getTrustMark().getJWTClaimsSet();
+            if (claimsSet != null) {
+                return Constraint.isNotEmpty(claimsSet.getIssuer(), "Issuer of the trust mark cannot be empty");
+            }
+        } catch (final ParseException e) {
+            // no op
+        }
+        throw new ConstraintViolationException("Could not resolve issuer of the trust mark");
     }
 
 }
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/impl/AbstractOIDFederationRegistrationProfileConfiguration.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/impl/AbstractOIDFederationRegistrationProfileConfiguration.java
index 2547df6..2ac713e 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/impl/AbstractOIDFederationRegistrationProfileConfiguration.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/config/impl/AbstractOIDFederationRegistrationProfileConfiguration.java
@@ -18,6 +18,7 @@ import java.time.Duration;
 import java.util.List;
 import java.util.Map;
 import java.util.function.Function;
+import java.util.function.Predicate;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -36,6 +37,7 @@ import net.shibboleth.shared.annotation.constraint.Unmodifiable;
 import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.logic.FunctionSupport;
+import net.shibboleth.shared.logic.PredicateSupport;
 
 /**
  * Abstract implementation class for profile configurations related OpenID Federation client registration.
@@ -52,6 +54,9 @@ public class AbstractOIDFederationRegistrationProfileConfiguration extends Abstr
     /** Lookup function to supply maximum trust mark lifetime. */
     @Nonnull private Function<ProfileRequestContext,Duration> maximumTrustMarkLifetimeLookupStrategy;
 
+    /** Whether trust marks should be remotely validated. */
+    @Nonnull private Predicate<ProfileRequestContext> remoteTrustMarkValidationCondition;
+
     /**
      * Constructor.
      * 
@@ -63,6 +68,7 @@ public class AbstractOIDFederationRegistrationProfileConfiguration extends Abstr
         localMetadataPolicyLookupStrategy = FunctionSupport.constant(CollectionSupport.emptyMap());
         mandatoryTrustMarksLookupStrategy = FunctionSupport.constant(CollectionSupport.emptyList());
         maximumTrustMarkLifetimeLookupStrategy = FunctionSupport.constant(Duration.ofDays(365));
+        remoteTrustMarkValidationCondition = PredicateSupport.alwaysTrue();
     }
 
     /** {@inheritDoc} */
@@ -161,4 +167,30 @@ public class AbstractOIDFederationRegistrationProfileConfiguration extends Abstr
             @Nullable final Function<ProfileRequestContext,Duration> strategy) {
         maximumTrustMarkLifetimeLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
     }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isRemoteTrustMarkValidation(@Nullable final ProfileRequestContext profileRequestContext) {
+        return remoteTrustMarkValidationCondition.test(profileRequestContext);
+    }
+
+   /**
+    * Set whether trust marks should be remotely validated.
+    * 
+    * @param flag flag to set
+    */
+    public void setRemoteTrustMarkValidation(final boolean flag) {
+        remoteTrustMarkValidationCondition = flag ? PredicateSupport.alwaysTrue() : PredicateSupport.alwaysFalse();
+    }
+
+   /**
+    * Set condition for whether trust marks should be remotely validated.
+    * 
+    * @param condition condition to set
+    */
+    public void setRemoteTrustMarkValidationPredicate(@Nonnull final Predicate<ProfileRequestContext> condition) {
+        remoteTrustMarkValidationCondition = Constraint.isNotNull(condition, "Condition cannot be null");
+    }
+
+
 }
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ResolveTrustMarks.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ResolveTrustMarks.java
index 9b2feff..9ea5e9b 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ResolveTrustMarks.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ResolveTrustMarks.java
@@ -52,6 +52,7 @@ import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
 import net.shibboleth.oidc.jwt.claims.JWTValidationException;
 import net.shibboleth.oidc.metadata.cache.MetadataCache;
 import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.shared.annotation.constraint.Live;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
 import net.shibboleth.shared.collection.CollectionSupport;
@@ -361,15 +362,16 @@ public class ResolveTrustMarks extends AbstractProfileAction {
                     trustMarks.stream()
                         .filter(entry -> onlyTrustedIssuers ?
                                 checkTrustedIssuer(entry, trustedIssuers, trustedOwners) : true)
-                        .filter(entry -> verifyTrustMark(entry, trustedOwners, profileRequestContext))
+                        .filter(entry -> verifyTrustMark(entry, trustedOwners, profileRequestContext,
+                                trustChainContext.getVerifiedTrustMarkIssuers()))
                         .filter(Objects::nonNull)
-                        .toList());
+                        .collect(Collectors.toList()));
         }
         trustChainContext.setVerifiedTrustMarks(verifiedTrustMarks);
         final Map<String, List<String>> verifiedTrustMarkIds = verifiedTrustMarks.entrySet().stream()
                 .collect(Collectors.toMap(entry -> entry.getKey(), entry -> entry.getValue().stream()
                         .map(list -> getTrustMarkId(list))
-                        .toList()));
+                        .collect(Collectors.toList())));
         log.debug("{} The following trust marks are validated: {}", getLogPrefix(), verifiedTrustMarkIds);
         trustChainContext.setVerifiedTrustMarkIds(verifiedTrustMarkIds);
     }
@@ -450,11 +452,13 @@ public class ResolveTrustMarks extends AbstractProfileAction {
      * @param jwt the trust mark to be verified
      * @param trustedOwners the trusted trust mark owners
      * @param profileRequestContext the profile request context
+     * @param trustedTrustMarkIssuers the map of trusted issuers that will be populated if issuer was verified
      * @return true if trust mark verification was successful, false otherwise
      */
     protected boolean verifyTrustMark(@Nullable final SignedJWT jwt,
             @Nonnull final Map<String, TrustMarkOwner> trustedOwners,
-            @Nonnull final ProfileRequestContext profileRequestContext) {
+            @Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull @Live final Map<String, EntityStatement<?>> trustedTrustMarkIssuers) {
         if (jwt == null) {
             return false;
         }
@@ -483,6 +487,7 @@ public class ResolveTrustMarks extends AbstractProfileAction {
         final EntityStatement<?> trustMarkIssuer = trustMarkChain.get(0);
         assert trustMarkIssuer != null;
         final CriteriaSet criteria = new CriteriaSet(new SubjectEntityStatementCriterion(trustMarkIssuer));
+        trustedTrustMarkIssuers.put(trustMarkIssuer.getSubject(), trustMarkIssuer);
         log.trace("{} Validating entity statement {}", getLogPrefix(), trustMarkIssuer.getJwt().serialize());
         try {
             if (trustEngine.validate(jwt, criteria)) {
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateTrustMarks.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateTrustMarks.java
new file mode 100644
index 0000000..0cb57ae
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateTrustMarks.java
@@ -0,0 +1,247 @@
+/*
+ * 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.profile.impl;
+
+import java.text.ParseException;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.trustmark.TrustMarkStatusCacheIdentifier;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.trustmark.TrustMarkStatusContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.trustmark.TrustMarkStatusIdentifierCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.navigate.RemoteTrustMarkValidationPredicate;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.context.RelyingPartyTrustChainContext;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.context.VerifiedTrustChain;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Validates the trust marks for the selected trust chain and updates the info to {@link RelyingPartyTrustChainContext}.
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ */
+public class ValidateTrustMarks extends AbstractProfileAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(ValidateTrustMarks.class);
+
+    /** Strategy used to lookup the trust chain context. */
+    @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextLookupStrategy;
+
+    /** Metadata cache for trust mark status. */
+    @NonnullAfterInit private MetadataCache<TrustMarkStatusContainer> trustMarkStatusCache;
+
+    /** Condition for whether trust marks should be remotely validated. */
+    @Nonnull private Predicate<ProfileRequestContext> remoteTrustMarkValidationCondition;
+
+    /** Trust chain context to operate on. */
+    @NonnullBeforeExec private RelyingPartyTrustChainContext trustChainContext;
+
+    /** The selected trust chain to resolve trust marks from. */
+    @NonnullBeforeExec private List<EntityStatement<?>> selectedTrustChain;
+
+    /**
+     * Constructor.
+     */
+    public ValidateTrustMarks() {
+        final Function<ProfileRequestContext, RelyingPartyTrustChainContext> tcls =
+                new ChildContextLookup<>(RelyingPartyTrustChainContext.class).compose(
+                        new InboundMessageContextLookup());
+        assert tcls != null;
+        trustChainContextLookupStrategy = tcls;
+        remoteTrustMarkValidationCondition = new RemoteTrustMarkValidationPredicate();
+    }
+
+    /**
+     * Set the strategy used to lookup the trust chain context.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setTrustChainContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, RelyingPartyTrustChainContext> strategy) {
+        checkSetterPreconditions();
+        trustChainContextLookupStrategy =
+                Constraint.isNotNull(strategy, "TrustChainContextLookupStrategy cannot be null");
+    }
+    
+    /**
+     * Set the metadata cache for trust mark status.
+     * 
+     * @param cache metadata cache
+     */
+    public void setTrustMarkStatusCache(@Nonnull final MetadataCache<TrustMarkStatusContainer> cache) {
+        checkSetterPreconditions();
+        trustMarkStatusCache = Constraint.isNotNull(cache, "TrustMarkStatusCache cannot be null");
+    }
+
+    /**
+     * Set condition for whether trust marks should be remotely validated.
+     * 
+     * @param condition condition to set
+     */
+     public void setRemoteTrustMarkValidationPredicate(@Nonnull final Predicate<ProfileRequestContext> condition) {
+         remoteTrustMarkValidationCondition = Constraint.isNotNull(condition, "Condition cannot be null");
+     }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (trustMarkStatusCache == null) {
+            throw new ComponentInitializationException("TrustMarkStatusCache cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+
+        if (!remoteTrustMarkValidationCondition.test(profileRequestContext)) {
+            log.debug("{} Remote trust mark validation condition returned false, nothing to do", getLogPrefix());
+            return false;
+        }
+        trustChainContext = trustChainContextLookupStrategy.apply(profileRequestContext);
+        if (trustChainContext == null || trustChainContext.getPolicyCompliantTrustChains() == null) {
+            log.error("{} Unable to locate policy-compliant trust chains", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        }
+
+        final VerifiedTrustChain selectedChain = trustChainContext.getSelectedTrustChain();
+
+        if (selectedChain == null) {
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            log.error("{} No selected trust chain could be resolved", getLogPrefix());
+            return false;
+        }
+
+        selectedTrustChain = selectedChain.getTrustChain();
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        final Map<String, List<SignedJWT>> verifiedTrustMarks = trustChainContext.getVerifiedTrustMarks();
+        final Map<String, List<String>> verifiedTrustMarkIds = trustChainContext.getVerifiedTrustMarkIds();
+        final String subject = selectedTrustChain.get(0).getSubject();
+        final List<SignedJWT> subjectTrustMarks = verifiedTrustMarks != null ? verifiedTrustMarks.get(subject) : null;
+        final List<String> subjectTrustMarkIds =
+                verifiedTrustMarkIds != null ? verifiedTrustMarkIds.get(subject) : null;
+        if (subjectTrustMarks == null || subjectTrustMarks.isEmpty()) {
+            log.debug("{} No trust marks to validate for {}", getLogPrefix(), subject);
+            return;
+        }
+        if (subjectTrustMarkIds == null || subjectTrustMarks.size() != subjectTrustMarkIds.size()) {
+            log.error("{} Unexpected contents for the verified trust mark IDs", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return;
+        }
+        log.trace("{} Subject trust marks before validation {}", getLogPrefix(), subjectTrustMarks);
+        for (final SignedJWT trustMark : CollectionSupport.copyToList(subjectTrustMarks)) {
+            assert trustMark != null;
+            try {
+                final String issuer = trustMark.getJWTClaimsSet().getIssuer();
+                final String trustMarkSubject = trustMark.getJWTClaimsSet().getSubject();
+                final String trustMarkType = trustMark.getJWTClaimsSet().getStringClaim("trust_mark_type");
+                final EntityStatement<?> issuerStatement = trustChainContext.getVerifiedTrustMarkIssuers().get(issuer);
+                if (issuerStatement == null) {
+                    log.warn("{} Could not resolve trust mark issuer statement for {}", getLogPrefix(), issuer);
+                    ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+                    return;
+                }
+                final String uri = Optional.ofNullable(issuerStatement.getParsedPayload().getMetadata())
+                        .map(metadata -> metadata.getFederationEntityMetadata())
+                        .map(entityMetadata -> entityMetadata.get("federation_trust_mark_status_endpoint"))
+                        .filter(String.class::isInstance)
+                        .map(String.class::cast)
+                        .map(value -> StringSupport.trimOrNull(value))
+                        .orElse(null);
+                if (uri == null) {
+                    log.debug("{} No trust mark status endpoint defined for {}, nothing to do", getLogPrefix(), issuer);
+                    continue;
+                }
+                final TrustMarkStatusCacheIdentifier cacheIdentifier =
+                        new TrustMarkStatusCacheIdentifier(uri, trustMark);
+                final CriteriaSet criteria = new CriteriaSet(new TrustMarkStatusIdentifierCriterion(cacheIdentifier));
+                final List<TrustMarkStatusContainer> cacheResult = trustMarkStatusCache.get(criteria);
+                if (cacheResult.isEmpty()) {
+                    log.warn("{} Could not fetch status via metadata cache for {}, issued by {}", getLogPrefix(),
+                            trustMarkType, issuer);
+                    subjectTrustMarks.remove(trustMark);
+                    subjectTrustMarkIds.remove(trustMarkType);
+                    continue;
+                }
+                final SignedJWT statusJwt = Optional.ofNullable(cacheResult.get(0).getStatement())
+                        .map(statement -> statement.getJwt())
+                        .orElse(null);
+                if (statusJwt == null) {
+                    log.warn("{} Could not fetch status JWT via metadata cache for {}", getLogPrefix(), issuer);
+                    subjectTrustMarks.remove(trustMark);
+                    subjectTrustMarkIds.remove(trustMarkType);
+                    continue;
+                }
+                final String status = statusJwt.getJWTClaimsSet().getStringClaim("status");
+                if (!"active".equals(status)) {
+                    log.warn("{} Status for trust mark {} for {} was not active: {}", getLogPrefix(), trustMarkType,
+                            trustMarkSubject, status);
+                    subjectTrustMarks.remove(trustMark);
+                    subjectTrustMarkIds.remove(trustMarkType);
+                    continue;
+                }
+                log.debug("{} Status for trust mark {} for {} is verified", getLogPrefix(), trustMarkType,
+                        trustMarkSubject);
+                continue;
+            } catch (final ParseException e) {
+                log.error("{} Could not parse claims set from the JWT", getLogPrefix(), e);
+            } catch (MetadataCacheException e) {
+                log.warn("{} Could not fetch status via metadata cache", getLogPrefix(), e);
+            }
+            subjectTrustMarks.remove(trustMark);
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/DefaultTrustMarkStatusCredentialResolver.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/DefaultTrustMarkStatusCredentialResolver.java
new file mode 100644
index 0000000..ce4fdd1
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/DefaultTrustMarkStatusCredentialResolver.java
@@ -0,0 +1,102 @@
+/*
+ * 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.security.credential;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+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 net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.BasePayload;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.IssuerEntityStatementCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.SubjectStatementCriterion;
+import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
+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 (issuer of a trust mark status) payload.
+ * First, a JWT is fetched via {@link SubjectStatementCriterion}. Its issuer must match with the entity
+ * statement fetched via {@link IssuerEntityStatementCriterion}.
+ */
+public class DefaultTrustMarkStatusCredentialResolver extends BasicJOSEObjectCredentialResolver {
+
+    /** Logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultTrustMarkStatusCredentialResolver.class);
+
+    /** {@inheritDoc} */
+    @Override
+    protected Iterable<Credential> resolveFromSource(@Nullable final CriteriaSet criteriaSet) throws ResolverException {
+        if (criteriaSet == null) {
+            throw new ResolverException("No criteria set supplied");
+        }
+
+        final List<Credential> result =  parseJwkSet(criteriaSet).getKeys().stream()
+                .filter(Objects::nonNull)
+                .map(jwk -> jwk != null ? buildJWKCredential(jwk, null) : null)
+                .filter(Objects::nonNull)
+                .map(Credential.class::cast)
+                .toList();
+        assert result != null;
+        return result;
+    }
+
+    /**
+     * Parses the JWKSet from the given criteria set.
+     * 
+     * @param criteriaSet criteria set containing source JWT for the JWKSet
+     * @return the JWKSet parsed from the JWT payload
+     * @throws ResolverException if the JWKSet could not be parsed or found
+     */
+    @Nonnull protected JWKSet parseJwkSet(@Nonnull final CriteriaSet criteriaSet) throws ResolverException {
+        final BasePayload subjectPayload = Optional.ofNullable(criteriaSet.get(SubjectStatementCriterion.class))
+                .map(criterion -> criterion.getValue().getParsedPayload()).orElse(null);
+        if (subjectPayload == null) {
+            log.debug("No mandatory criteria supplied for resolving subject, resolver could not process");
+            throw new ResolverException(
+                    "Credential criteria set did not contain criterion to resolve subject");
+        }
+        final IssuerEntityStatementCriterion issuerCriterion =
+                criteriaSet.get(IssuerEntityStatementCriterion.class);
+        if (issuerCriterion == null) {
+            log.debug("No mandatory IssuerEntityStatementCriterion supplied, resolver could not process");
+            throw new ResolverException(
+                    "Credential criteria set did not contain an instance of IssuerEntityStatementCriterion");
+        }
+        final EntityStatement<?> issuerStatement = issuerCriterion.getValue();
+        if (!issuerStatement.getSubject().equals(subjectPayload.getIssuer())) {
+            throw new ResolverException("Credential criteria do not match for subject and issuer");
+            }
+        if (!issuerStatement.getSubject().equals(issuerStatement.getIssuer())) {
+            throw new ResolverException("Issuer entity statement is not self signed");
+        }
+        final JWKSet  jwks = issuerStatement.getParsedPayload().getJwks();
+
+        if (jwks == null || jwks.isEmpty()) {
+            throw new ResolverException("Could not parse mandatory jwks");
+        }
+        return jwks;
+    }
+
+}
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 23aaaba..b43cf42 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
@@ -148,6 +148,9 @@
                                 <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.DefaultTrustChainHeaderValidationCondition"
                                     p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper" />
                             </property>
+                            <property name="containerIdValidationStrategy">
+                                <bean parent="shibboleth.BiConditions.Expression" c:expression="#input1?.getEntityId()?.equals(#input2?.getParsedPayload()?.getSubject())"/>
+                            </property>
                         </bean>
                     </util:list>
                 </property>
@@ -250,6 +253,9 @@
                                 <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.DefaultTrustChainHeaderValidationCondition"
                                     p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper" />
                             </property>
+                            <property name="containerIdValidationStrategy">
+                                <bean parent="shibboleth.BiConditions.Expression" c:expression="#input1?.getEntityId()?.equals(#input2?.getParsedPayload()?.getSubject())"/>
+                            </property>
                         </bean>
                     </util:list>
                 </property>
@@ -460,6 +466,9 @@
                             <property name="headerValidator">
                                 <bean parent="shibboleth.BiConditions.Expression" c:expression="true"/>
                             </property>
+                            <property name="containerIdValidationStrategy">
+                                <bean parent="shibboleth.BiConditions.Expression" c:expression="#input1?.getEntityId()?.equals(#input2?.getParsedPayload()?.getSubject())"/>
+                            </property>
                         </bean>
                     </util:list>
                 </property>
@@ -572,13 +581,20 @@
         </property>
     </bean>
 
+    <bean id="shibboleth.oidfed.TrustMarkStatusMetadataCache" parent="shibboleth.oidc.CacheBuilder">
+        <constructor-arg>
+            <bean p:cacheId="DefaultTrustMarkStatusMetadataCache" parent="shibboleth.oidfed.TrustMarkStatusMetadataCacheBuilderSpec"
+                p:cleanupTaskInterval="PT30S"/>
+        </constructor-arg>
+    </bean>
+
     <bean id="shibboleth.oidfed.TrustMarkStatusMetadataCacheBuilderSpec"
         class="net.shibboleth.oidc.metadata.cache.impl.DynamicMetadataCacheBuilderSpec"
         p:minCacheDuration="%{idp.oidfed.cache.trustMarkStatus.maxRefreshDelay:PT60S}"
         p:maxCacheDuration="%{idp.oidfed.cache.trustMarkStatus.maxRefreshDelay:PT30M}"
         p:metadataExpirationTimeStrategy-ref="DefaultResponseContainerExpirationTimeStrategy">
         <property name="identifierExtractionStrategy">
-            <bean parent="shibboleth.Functions.Expression" c:expression="#input?.getRequest().getTrustMark().serialize()"/>
+            <bean parent="shibboleth.Functions.Expression" c:expression="#input?.getIdentifier()"/>
         </property>
         <property name="criteriaToIdentifierStrategy">
             <bean parent="shibboleth.Functions.Expression" c:expression="#input?.get(T(net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.trustmark.TrustMarkStatusIdentifierCriterion))?.getIdentifier()"/>
@@ -588,8 +604,17 @@
                 p:customFilterStrategies="#{getObject('%{idp.oidfed.cache.trustMarkStatus.customFilterStrategies:}'.trim())}">
                 <property name="validationConditions">
                     <util:list value-type="java.util.function.BiPredicate">
-                        <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.DefaultEntityStatementSignatureValidationCondition"
-                            p:trustEngine-ref="shibboleth.oidfed.DefaultSubordinateStatementTrustEngine">
+                        <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.DefaultEntityStatementSignatureValidationCondition">
+                            <property name="trustEngine">
+                                <bean class="net.shibboleth.oidc.security.impl.ExplicitKeySignedJWTTrustEngine">
+                                    <constructor-arg index="0">
+                                        <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential.DefaultTrustMarkStatusCredentialResolver" />
+                                     </constructor-arg>
+                                     <constructor-arg index="1">
+                                         <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential.DefaultPayloadJOSEObjectCredentialResolver" />
+                                     </constructor-arg>
+                                 </bean>
+                            </property>
                             <property name="criteriaSetLookupStrategy">
                                 <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.trustmark.DefaultTrustMarkValidationCriteriaSetLookupFunction"
                                     p:trustChainCache-ref="shibboleth.oidfed.TrustChainMetadataCache"/>
@@ -601,6 +626,9 @@
                             <property name="headerValidator">
                                 <bean parent="shibboleth.BiConditions.Expression" c:expression="true"/>
                             </property>
+                            <property name="containerIdValidationStrategy">
+                                <bean parent="shibboleth.BiConditions.Expression" c:expression="#input1?.getEntityId()?.equals(#input2?.getParsedPayload()?.getIssuer())"/>
+                            </property>
                         </bean>
                     </util:list>
                 </property>
@@ -636,7 +664,7 @@
                     <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.DefaultClientAuthenticationLookupFunction"
                         p:clientAuthenticationDecoratorsLookupStrategy-ref="shibboleth.oidfed.cache.DefaultEndpointAuthenticationFunctions">
                         <property name="supportedAuthenticationMethodsLookupStrategy">
-                            <bean parent="shibboleth.Functions.Expression" c:expression="#input?.get(T(net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.FederationEndpointEntityStatementCriterion))?.getValue().getParsedPayload().getMetadata()?.getFederationEntityMetadata()?.get('federation_trust_mark_status_endpoint_auth_methods')" />
+                            <bean parent="shibboleth.Functions.Expression" c:expression="#input?.get(T(net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.FederationEndpointEntityStatementCriterion))?.getValue()?.getParsedPayload()?.getMetadata()?.getFederationEntityMetadata()?.get('federation_trust_mark_status_endpoint_auth_methods')" />
                         </property>
                     </bean>
                 </property>
@@ -700,6 +728,9 @@
                             <property name="headerValidator">
                                 <bean parent="shibboleth.BiConditions.Expression" c:expression="true"/>
                             </property>
+                            <property name="containerIdValidationStrategy">
+                                <bean parent="shibboleth.BiConditions.Expression" c:expression="#input1?.getEntityId()?.equals(#input2?.getParsedPayload()?.getSubject())"/>
+                            </property>
                         </bean>
                     </util:list>
                 </property>
@@ -830,6 +861,9 @@
                                 <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.DefaultTrustChainHeaderValidationCondition"
                                     p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper" />
                             </property>
+                            <property name="containerIdValidationStrategy">
+                                <bean parent="shibboleth.BiConditions.Expression" c:expression="#input1?.getEntityId()?.equals(#input2?.getParsedPayload()?.getSubject())"/>
+                            </property>
                         </bean>
                     </util:list>
                 </property>
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-beans.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-beans.xml
index 1419a94..0181ddc 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-beans.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-beans.xml
@@ -189,6 +189,11 @@
         </property>
     </bean>
 
+    <bean id="ValidateTrustMarks" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ValidateTrustMarks"
+        scope="prototype"
+        p:trustMarkStatusCache-ref="#{'%{idp.oidfed.authorize.TrustMarkStatusMetadataCache:shibboleth.oidfed.TrustMarkStatusMetadataCache}'.trim()}">
+    </bean>
+
     <bean id="ValidateAutomaticRegistrationProfileConfiguration"
         class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ValidateAutomaticRegistrationProfileConfiguration"
         scope="prototype">
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-flow.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-flow.xml
index 60ecb5d..ac07f2d 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-flow.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-flow.xml
@@ -56,6 +56,7 @@
         <evaluate expression="ResolveTrustMarks" />
         <evaluate expression="SelectAutomaticRegistrationRelyingPartyConfiguration" />
         <evaluate expression="SelectAutomaticRegistrationProfileConfiguration" />
+        <evaluate expression="ValidateTrustMarks" />
         <evaluate expression="ValidateAutomaticRegistrationProfileConfiguration" />
         <evaluate expression="InitializeRelyingPartyContext" />
         <evaluate expression="'proceed'" />
@@ -66,6 +67,7 @@
     </action-state>
 
     <end-state id="proceed"/>
+    <end-state id="InvalidMessageContext"/>
     <end-state id="InvalidMetadataPolicy"/>
     <end-state id="InvalidMetadataAgainstPolicy"/>
     <end-state id="NoTrustChainsResolved" />
@@ -74,6 +76,7 @@
     <end-state id="HandleError"/>
 
     <global-transitions>
+        <transition on="InvalidMessageContext" to="InvalidMessageContext" />
         <transition on="InvalidMetadataPolicy" to="InvalidMetadataPolicy" />
         <transition on="InvalidMetadataAgainstPolicy" to="InvalidMetadataAgainstPolicy" />
         <transition on="NoTrustChainsResolved" to="NoTrustChainsResolved" />
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 79cdd84..aa2c5e6 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
@@ -152,6 +152,9 @@
                                 <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.DefaultTrustChainHeaderValidationCondition"
                                     p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper" />
                             </property>
+                            <property name="containerIdValidationStrategy">
+                                <bean parent="shibboleth.BiConditions.Expression" c:expression="#input1?.getEntityId()?.equals(#input2?.getParsedPayload()?.getSubject())"/>
+                            </property>
                         </bean>
                     </util:list>
                 </property>
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 b31d518..47432de 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
@@ -101,6 +101,7 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
     protected final String anchorResolveEndpoint = anchorId + "/resolve";
     protected final String trustMarkIssuerId = "https://trust-mark-issuer.federation.local";
     protected final String trustMarkEndpoint = "https://trust-mark-issuer.federation.local/issue";
+    protected final String trustMarkStatusEndpoint = "https://trust-mark-issuer.federation.local/status";
     protected final String issuer = "https://op.example.org";
 
     protected JWK rpKey;
@@ -312,7 +313,7 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
                 .claim("authority_hints", authorityHints == null || authorityHints.length == 0 ?
                         new String[] { anchorId } : authorityHints)
                 .claim("metadata", Map.of("federation_entity", Map.of("trust_mark_endpoint",
-                        trustMarkEndpoint)))
+                        trustMarkEndpoint, "federation_trust_mark_status_endpoint", trustMarkStatusEndpoint)))
                 .build();
         final EntityStatement<?> rpConfiguration =
                 TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, trustMarkIssuerKey, claimsSet);
@@ -472,9 +473,16 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
 
     protected String rpResolveEntityResponse(final String clientId, final Map<String, Object> metadata,
             final List<Map<String, String>> trustMarks) {
+        final OIDCClientMetadata rpMetadata = new OIDCClientMetadata();
+        try {
+            rpMetadata.setRedirectionURI(new URI(redirectUri));
+        } catch (URISyntaxException e) {
+            Assert.fail("Could not initialize an URI", e);
+        }
+        rpMetadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));     
         final List<String> trustChain;
         try {
-            trustChain = List.of(rpEntityConfiguration(clientId),
+            trustChain = List.of(rpEntityConfiguration(clientId, rpMetadata, trustMarks, leafKey),
                     subordinateStatement(clientId, Map.of("openid_relying_party",
                             new OIDCClientMetadata().toJSONObject())),
                     trustedAnchorConfiguration());
@@ -795,6 +803,17 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
                 .build();
     }
 
+    protected String trustMarkStatusResponse(final String issuer, final String trustMark, final String status,
+            final JWK signerKey) {
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(issuer)
+                .issueTime(Date.from(Instant.now()))
+                .claim("trust_mark", trustMark)
+                .claim("status", status)
+                .build();
+        return TrustChainTestUtil.signedJwt(JWSAlgorithm.RS256, signerKey,
+                "trust-mark-status-response+jwt", claimsSet).serialize();
+    }
+
     protected class RequestUriMatcher implements ArgumentMatcher<ClassicHttpRequest> {
 
         @Nonnull private final String uri;
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 a041a57..48a6314 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
@@ -104,6 +104,8 @@ public class UserInfoFlowAutomaticRegistrationTest extends AbstractFederationFlo
             mapResponse(subordinateStatementUrl(anchorFetchEndpoint, trustMarkIssuerId),
                     mockResponse(subordinateStatement(trustMarkIssuerId,
                             Map.of("federation_entity", CollectionSupport.emptyMap()), trustMarkIssuerKey)));
+            mapResponse(trustMarkStatusEndpoint, mockResponse(200, "application/trust-mark-status-response+jwt",
+                    trustMarkStatusResponse(trustMarkIssuerId, trustMark, "active", trustMarkIssuerKey)));
         } catch (UnsupportedOperationException | IOException e) {
             Assert.fail("Could not initialize mock HTTP client", e);
         }
@@ -148,6 +150,18 @@ public class UserInfoFlowAutomaticRegistrationTest extends AbstractFederationFlo
         rpResolveEntityConfigureMockHttpClient(clientId, metadata, List.of(Map.of(
                 "trust_mark_type", "https://example.org/email-allowing-trust-mark",
                 "trust_mark", trustMark)));
+        try {
+            mapResponse(entityConfigurationUrl(trustMarkIssuerId),
+                    mockResponse(trustMarkIssuerConfiguration(trustMarkIssuerId)));
+            mapResponse(subordinateStatementUrl(anchorFetchEndpoint, trustMarkIssuerId),
+                    mockResponse(subordinateStatement(trustMarkIssuerId,
+                            Map.of("federation_entity", CollectionSupport.emptyMap()), trustMarkIssuerKey)));
+            mapResponse(trustMarkStatusEndpoint, mockResponse(200, "application/trust-mark-status-response+jwt",
+                    trustMarkStatusResponse(trustMarkIssuerId, trustMark, "active", trustMarkIssuerKey)));
+        } catch (UnsupportedOperationException | IOException e) {
+            Assert.fail("Could not initialize mock HTTP client", e);
+        }
+
         request.addHeader(USE_CUSTOM_RESOLVER_API_CONDITION, "true");
         final BearerAccessToken token = buildToken(clientId, List.of(clientId, anchorId));
         request.addHeader("Authorization", token.toAuthorizationHeader());

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


More information about the commits mailing list