[java-idp-plugin-oidc-op-oidfed] 01/01: Refactoring for metadata caches related to remote entity statements. WIP.

Codeberg noreply at shibboleth.net
Tue Dec 16 12:25:33 UTC 2025


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/f8c29c4f9527fed590f0ac93b0c6be7becd74aa4

commit f8c29c4f9527fed590f0ac93b0c6be7becd74aa4
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Tue Dec 16 14:24:59 2025 +0200

    Refactoring for metadata caches related to remote entity statements. WIP.
    
    - Replaced Nimbus's EntityStatement object with our own wrappers
      - API: net.shibboleth.idp.plugin.oidc.op.oidfed.metadata
      - Exploits shibboleth.oidfed.JWTPayloadJSONObjectMapper
        - Specially configured for being able to deserialize metadata policies, Nimbus JWKsets and Long to Instant
    - Also non-valid statements (unexpected response, invalid claims, invalid signature, ..) may be cached by the metadata caches
    - Included shibboleth.oidfed.DefaultSubordinateStatementClaimsValidator for subordinate statement claims validation
---
 .../op/oidfed/metadata/EntityConfiguration.java    |  61 +++++
 .../metadata/EntityConfigurationPayload.java       | 162 +++++++++++++
 .../oidc/op/oidfed/metadata/EntityStatement.java   | 121 ++++++++++
 .../op/oidfed/metadata/EntityStatementPayload.java | 220 ++++++++++++++++++
 .../op/oidfed/metadata/SubordinateStatement.java   |  60 +++++
 .../metadata/SubordinateStatementPayload.java      | 138 +++++++++++
 .../context/RelyingPartyTrustChainContext.java     |   2 +-
 .../policyrule/impl/EntityTrustMarkPolicyRule.java |   2 +-
 .../ExplicitClientRegistrationRequestDecoder.java  |  31 ++-
 .../impl/ExplicitClientRegistrationRequest.java    |   2 +-
 ...ntityStatementSignatureValidationComponent.java |  47 ----
 ...mbinedMetadataFromTrustChainLookupStrategy.java |   7 +-
 ...ityConfigurationEndpointExtractionStrategy.java |  82 -------
 ...tyStatementContentValidationFilterStrategy.java |  83 -------
 ...faultEntityStatementExpirationTimeStrategy.java |   4 +-
 .../DefaultEntityStatementFetchingStrategy.java    | 171 --------------
 ...StatementSignatureValidationFilterStrategy.java |   9 +-
 ...atementSubjectIdentifierExtractionStrategy.java |  40 ----
 ...efaultProvidedTrustChainValidationStrategy.java |   3 +-
 ...yResponseSignatureValidationFilterStrategy.java |  11 +-
 ...rdinateStatementEndpointExtractionStrategy.java | 149 ------------
 ...inateStatementIdentifierExtractionStrategy.java |  43 ----
 ...StatementSignatureValidationFilterStrategy.java | 116 ----------
 .../DefaultTrustChainExpirationTimeStrategy.java   |  60 -----
 ...stChainSubjectIdentifierExtractionStrategy.java |   4 +-
 ...DefaultTrustChainTrustMarksParsingStrategy.java |  48 ++--
 ...ChainTrustedTrustMarkIssuersLookupStrategy.java |  38 +---
 ...tChainTrustedTrustMarkOwnersLookupStrategy.java |  61 +----
 ...kResponseSignatureValidationFilterStrategy.java |  13 +-
 ...sResponseSignatureValidationFilterStrategy.java |  14 +-
 .../op/oidfed/metadata/EntityStatementHelper.java  |  60 +++--
 .../oidfed/metadata/IssuerEntityIDCriterion.java   |   7 +-
 .../oidc/op/oidfed/metadata/LocalKeyContainer.java |   2 +-
 .../oidfed/metadata/SubjectEntityIDCriterion.java  |   4 +-
 ...FederationEndpointResponseFetchingStrategy.java | 252 +++++++++++++++++++++
 ...actTrustEngineSignatureValidationComponent.java | 103 +++++++++
 .../BaseExpirableEntityStatementContainer.java     |  93 ++++++++
 .../cache/BaseExpirableMetadataContainer.java      |  61 +++++
 ...faultEntityStatementMetadataFilterStrategy.java |  86 +++++++
 ...ntityStatementSignatureValidationCondition.java |  98 ++++++++
 ...ltResponseContainerExpirationTimeStrategy.java} |  23 +-
 .../IssuerEntityStatementCriterion.java            |  19 +-
 .../cache/RemoteEntityConfigurationContainer.java  |  57 +++++
 .../cache/RemoteSubordinateStatementContainer.java |  67 ++++++
 .../metadata/cache/RemoteTrustChainsContainer.java |  97 ++++++++
 .../SubjectEntityStatementCriterion.java           |  15 +-
 .../cache/SubordinateStatementCacheIdentifier.java | 103 +++++++++
 .../constraints/FederationPolicyConstraint.java    |   2 +-
 .../FederationPolicyConstraintHelper.java          |  13 +-
 .../impl/AbstractFederationPolicyConstraint.java   |   3 +-
 .../impl/DefaultAllowedEntityTypesConstraint.java  |  13 +-
 .../impl/DefaultMaxPathLengthConstraint.java       |   5 +-
 .../impl/DefaultNamingConstraintsConstraint.java   |   5 +-
 ...ityConfigurationContentValidationCondition.java |  92 ++++++++
 ...tityConfigurationCriteriaSetLookupFunction.java |  43 ++++
 ...DefaultEntityConfigurationFetchingStrategy.java | 129 +++++++++++
 .../metadata/jackson/InstantDeserializer.java      |  49 ++++
 .../metadata/jackson/JWKSetDeserializer.java       |  70 ++++++
 ...inateStatementCriteriaToIdentifierStrategy.java |  15 +-
 ...efaultSubordinateStatementFetchingStrategy.java | 183 +++++++++++++++
 ...inateStatementSignatureValidationCondition.java | 116 ++++++++++
 ...ustChainSignatureValidationFilterStrategy.java} |  27 ++-
 ...ustChainSignatureValidationFilterStrategy.java} |  39 ++--
 .../DefaultTrustChainFetchingStrategy.java         | 128 ++++++++---
 .../impl/AbstractTrustChainResolutionAction.java   |  13 +-
 .../impl/AddExtensionsToClientInformation.java     |   6 +-
 .../profile/impl/BuildResolveEntityResponse.java   |   8 +-
 .../oidfed/profile/impl/CallResolveEntityApi.java  |  46 +++-
 ...TrustMarkFromMetadataCacheFetchingFunction.java |   8 +-
 .../op/oidfed/profile/impl/ResolveTrustChains.java |  20 +-
 .../op/oidfed/profile/impl/ResolveTrustMarks.java  |  21 +-
 .../op/oidfed/profile/impl/SelectTrustChain.java   |   5 +-
 .../profile/impl/StoreAutomaticRegistration.java   |   4 +-
 ...eAutomaticRegistrationProfileConfiguration.java |  12 +-
 .../impl/ValidateProvidedEntityConfiguration.java  |   3 +-
 .../profile/impl/ValidateProvidedTrustChain.java   |   3 +-
 .../profile/impl/ValidateSelectedTrustChain.java   |  15 +-
 ...DefaultSelectedTrustChainIDsLookupStrategy.java |   3 +-
 ...dTrustChainImmediateSuperiorLookupStrategy.java |   3 +-
 ...electedTrustChainTrustAnchorLookupStrategy.java |   2 +-
 .../DefaultTrustChainIDsLookupStrategy.java        |   4 +-
 ...ultTrustChainMetadataPolicyMergingStrategy.java |  58 ++---
 .../DefaultTrustChainSelectionStrategy.java        |   3 +-
 ...tRegistrationRequestClientIDLookupFunction.java |   6 +-
 ...onRequestEntityConfigurationLookupFunction.java |   3 +-
 ...entRegistrationRequestJWKSetLookupFunction.java |   6 +-
 ...egistrationRequestTrustChainLookupFunction.java |   3 +-
 ...faultEntityConfigurationCredentialResolver.java |  11 +-
 .../DefaultLocalTrustAnchorCredentialResolver.java |   6 +-
 ...DefaultPayloadJOSEObjectCredentialResolver.java |   2 +-
 ...yloadSignatureValidationCredentialResolver.java |   3 +-
 ...aultSubordinateStatementCredentialResolver.java |  17 +-
 .../DefaultTrustMarkOwnerCredentialResolver.java   |   4 +-
 .../META-INF/net.shibboleth.idp/postconfig.xml     | 231 +++++++++++++------
 .../oidfed/metadata-lookup-ext-oidfed-beans.xml    |   7 +-
 .../idp/flows/oidfed/register/register-beans.xml   |  44 ++--
 .../oidfed/resolve-entity/resolve-entity-beans.xml |   5 +-
 .../flow/oidfed/AbstractFederationFlowTest.java    |   8 +-
 .../UserInfoFlowAutomaticRegistrationTest.java     |   3 +-
 .../EntityConfigurationMetadataCacheTest.java      |  17 +-
 ... => SubordinateStatementMetadataCacheTest.java} | 154 +++++++------
 ...eTest.java => TrustChainMetadataCacheTest.java} | 164 ++++++++------
 .../idp/module/conf/oidfed/oidfed.properties       |   5 +-
 pom.xml                                            |   2 +-
 104 files changed, 3331 insertions(+), 1488 deletions(-)

diff --git a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/EntityConfiguration.java b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/EntityConfiguration.java
new file mode 100644
index 0000000..059aa52
--- /dev/null
+++ b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/EntityConfiguration.java
@@ -0,0 +1,61 @@
+/*
+ * 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 javax.annotation.Nonnull;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.shared.logic.ConstraintViolationException;
+
+/**
+ * A wrapper class extending {@link EntityStatement} with parsed claims related to entity configuration.
+ */
+public class EntityConfiguration extends EntityStatement {
+
+    /**
+     * Constructor.
+     *
+     * @param signedJwt entity configuration
+     * @param payload entity configuration payload
+     * @throws ConstraintViolationException if the payload syntax/content is not expected
+     */
+    protected EntityConfiguration(@Nonnull final SignedJWT signedJwt,
+            @Nonnull final EntityConfigurationPayload payload) 
+        throws ConstraintViolationException {
+        super(signedJwt, payload);
+    }
+
+    /**
+     * Parse an {@link EntityConfiguration} from the given JWT by using the given object mapper.
+     * 
+     * @param jwt entity statement JWT
+     * @param objectMapper object mapper capable of parsing JWT payload
+     * @return entity configuration
+     * @throws JsonProcessingException if the payload could not be parsed
+     */
+    @Nonnull public static EntityConfiguration parse(@Nonnull final SignedJWT jwt,
+            @Nonnull final ObjectMapper objectMapper) throws JsonProcessingException {
+        final JavaType objectType = objectMapper.constructType(EntityConfigurationPayload.class);
+        final EntityConfigurationPayload result =
+                objectMapper.readValue(jwt.getPayload().toString(), objectType);
+        assert result != null;
+        return new EntityConfiguration(jwt, result);
+    }
+    
+}
diff --git a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/EntityConfigurationPayload.java b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/EntityConfigurationPayload.java
new file mode 100644
index 0000000..178df20
--- /dev/null
+++ b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/EntityConfigurationPayload.java
@@ -0,0 +1,162 @@
+/*
+ * 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.List;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.MoreObjects;
+
+/**
+ * Entity Configuration payload claims as defined by the OpenID Federation 1.0 Section 3.2. This class extends the
+ * list of claims defined by {@link EntityStatementPayload} with the claims that MUST or MAY appear in Entity
+ * Configurations but not in Subordinate Statements.
+ */
+public class EntityConfigurationPayload extends EntityStatementPayload {
+
+    /** An array of strings representing the Entity Identifiers of Immediate Superiors. */
+    @JsonProperty("authority_hints") private List<String> authorityHints;
+
+    /** An array of strings representing the Entity Identifiers of Trust Anchors trusted by the Entity. */
+    @JsonProperty("trust_anchor_hints") private List<String> trustAnchorHints;
+
+    /** An array of Trust Marks. */
+    @JsonProperty("trust_marks") private List<Map<String,String>> trustMarks;
+
+    /**
+     * A map of arrays of Entity Identifiers that are trusted to represent the accreditation authority for Trust Marks.
+     */
+    @JsonProperty("trust_mark_issuers") private Map<String, List<String>> trustMarkIssuers;
+
+    /** A map of Owners of the Trust Marks. */
+    @JsonProperty("trust_mark_owners") private Map<String, Map<String, Object>> trustMarkOwners;
+
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("iss", getIssuer())
+                .add("sub", getSubject())
+                .add("iat", getIssuedAt())
+                .add("exp", getExpiration())
+                .add("jwks", getJwks())
+                .add("metadata", getMetadata())
+                .add("crit", getCritical())
+                .add("authority_hints", authorityHints)
+                .add("trust_anchor_hints", trustAnchorHints)
+                .add("trust_marks", trustMarks)
+                .add("trust_mark_issuers", trustMarkIssuers)
+                .add("trust_mark_owners", trustMarkOwners)
+                .add("customClaims", getCustomClaims()).toString();
+    }
+
+    /**
+     * Get the array of strings representing the Entity Identifiers of Immediate Superiors.
+     * 
+     * @return authority hints
+     */
+    @Nullable public List<String> getAuthorityHints() {
+        return authorityHints;
+    }
+
+    /**
+     * Set the array of strings representing the Entity Identifiers of Immediate Superiors.
+     * 
+     * @param hints authority hints
+     */
+    public void setAuthorityHints(@Nullable List<String> hints) {
+        authorityHints = hints;
+    }
+
+    /**
+     * Get the array of strings representing the Entity Identifiers of Trust Anchors trusted by the Entity.
+     * 
+     * @return trust anchor hints
+     */
+    @Nullable public List<String> getTrustAnchorHints() {
+        return trustAnchorHints;
+    }
+
+    /**
+     * Set the array of strings representing the Entity Identifiers of Trust Anchors trusted by the Entity.
+     * 
+     * @param hints trust anchor hints
+     */
+    public void setTrustAnchorHints(@Nullable List<String> hints) {
+       trustAnchorHints = hints;
+    }
+
+    /**
+     * Get the array of Trust Marks.
+     * 
+     * @return trust marks
+     */
+    @Nullable public List<Map<String, String>> getTrustMarks() {
+        return trustMarks;
+    }
+
+    /**
+     * Set the array of Trust Marks.
+     * 
+     * @param marks trust marks
+     */
+    public void setTrustMarks(@Nullable List<Map<String, String>> marks) {
+        trustMarks = marks;
+    }
+
+    /**
+     * Get the map of arrays of Entity Identifiers that are trusted to represent the accreditation authority for Trust
+     * Marks.
+     * 
+     * @return trust mark issuers
+     */
+    @Nullable public Map<String, List<String>> getTrustMarkIssuers() {
+        return trustMarkIssuers;
+    }
+
+    /**
+     * Set the map of arrays of Entity Identifiers that are trusted to represent the accreditation authority for Trust
+     * Marks.
+     * 
+     * @param issuers trust mark issuers
+     */
+    public void setTrustMarkIssuers(@Nullable Map<String, List<String>> issuers) {
+        trustMarkIssuers = issuers;
+    }
+
+    /**
+     * Get the map of Owners of the Trust Marks.
+     * 
+     * @return trust mark owners
+     */
+    @Nullable public Map<String, Map<String, Object>> getTrustMarkOwners() {
+        return trustMarkOwners;
+    }
+
+    /**
+     * Set the map of Owners of the Trust Marks.
+     * 
+     * @param owners trust mark owners
+     */
+    public void setTrustMarkOwners(@Nullable Map<String, Map<String, Object>> owners) {
+        trustMarkOwners = owners;
+    }
+
+}
diff --git a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/EntityStatement.java b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/EntityStatement.java
new file mode 100644
index 0000000..48c2c4f
--- /dev/null
+++ b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/EntityStatement.java
@@ -0,0 +1,121 @@
+/*
+ * 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 javax.annotation.Nonnull;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.MoreObjects;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+
+/**
+ * A wrapper class containing the {@link SignedJWT} and parsed claims related to entity statement.
+ */
+public class EntityStatement {
+
+    /** Entity statement as JWT. */
+    @Nonnull private final SignedJWT jwt;
+
+    /** Entity statement payload. */
+    @Nonnull private final EntityStatementPayload parsedPayload;
+
+    /**
+     * 
+     * Constructor.
+     *
+     * @param signedJwt entity statement
+     * @param payload entity statement payload
+     * @throws ConstraintViolationException if the payload syntax/content is not expected
+     */
+    protected EntityStatement(@Nonnull final SignedJWT signedJwt, @Nonnull final EntityStatementPayload payload) 
+        throws ConstraintViolationException {
+        jwt = Constraint.isNotNull(signedJwt, "JWT cannot be null");
+        parsedPayload = Constraint.isNotNull(payload, "Entity statement payload cannot be null");
+        Constraint.isNotEmpty(parsedPayload.getIssuer(), "Issuer cannot be empty in the payload");
+        Constraint.isNotEmpty(parsedPayload.getSubject(), "Subject cannot be empty in the payload");
+    }
+
+    /**
+     * Get the entity statement as JWT.
+     * 
+     * @return JWT
+     */
+    @Nonnull public SignedJWT getJwt() {
+        return jwt;
+    }
+
+    /**
+     * Get the issuer claim of the entity statement.
+     * 
+     * @return issuer
+     */
+    @Nonnull @NotEmpty public String getIssuer() {
+        final String issuer = parsedPayload.getIssuer();
+        assert issuer != null;
+        return issuer;
+    }
+
+    /**
+     * Get the subject claim of the entity statement.
+     * 
+     * @return subject
+     */
+    @Nonnull @NotEmpty public String getSubject() {
+        final String subject = parsedPayload.getSubject();
+        assert subject != null;
+        return subject;
+    }
+
+    /**
+     * Get the payload of the entity statement.
+     * 
+     * @return payload
+     */
+    @Nonnull public EntityStatementPayload getParsedPayload() {
+        return parsedPayload;
+    }
+
+    /**
+     * Parse an {@link EntityStatement} from the given JWT by using the given object mapper.
+     * 
+     * @param jwt entity statement JWT
+     * @param objectMapper object mapper capable of parsing JWT payload
+     * @return entity statement
+     * @throws JsonProcessingException if the payload could not be parsed
+     */
+    public static EntityStatement parse(@Nonnull final SignedJWT jwt, @Nonnull final ObjectMapper objectMapper)
+            throws JsonProcessingException {
+        final JavaType objectType = objectMapper.constructType(EntityStatementPayload.class);
+        final EntityStatementPayload result =
+                objectMapper.readValue(jwt.getPayload().toString(), objectType);
+        assert result != null;
+        return new EntityStatement(jwt, result);
+    }
+    
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("jwt", jwt.serialize())
+                .add("parsedPayload", parsedPayload)
+                .toString();
+    }
+
+}
diff --git a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/EntityStatementPayload.java b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/EntityStatementPayload.java
new file mode 100644
index 0000000..bd9c9ec
--- /dev/null
+++ b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/EntityStatementPayload.java
@@ -0,0 +1,220 @@
+/*
+ * 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.time.Instant;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonAnyGetter;
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.MoreObjects;
+import com.nimbusds.jose.jwk.JWKSet;
+
+/**
+ * Entity Statement payload claims as defined by the OpenID Federation 1.0 Section 3.1. These claims MUST or MAY appear
+ * in both Entity Configurations and Subordinate Statements.
+ */
+public class EntityStatementPayload {
+
+    /** The Entity Identifier of the Entity Statement. */
+    @JsonProperty("iss") private String issuer;
+
+    /** The Entity Identifier of the subject. */
+    @JsonProperty("sub") private String subject;
+
+    /** Time when this statement was issued. */
+    @JsonProperty("iat") private Instant issuedAt;
+
+    /** Expiration time after which this statement MUST NOT be accepted for processing. */
+    @JsonProperty("exp") private Instant expiration;
+
+    /** A JSON Web Key Set representing the public part of the subject's Federation Entity signing keys. */
+    @JsonProperty("jwks") private JWKSet jwkSet;
+
+    /** A JSON object that declares roles that the Entity plays per its Entity Types. */
+    @JsonProperty("metadata") private Map<String, Map<String, Object>> metadata;
+
+    /** The critical Claims that must be understood and processed. */
+    @JsonProperty("crit") private List<String> critical;
+
+    /** The map of any other claims not directly mapped. */
+    private final Map<String, Object> customClaims = new HashMap<>();
+
+    /**
+     * Get the issuer.
+     * 
+     * @return issuer
+     */
+    public String getIssuer() {
+        return issuer;
+    }
+
+    /**
+     * Set the issuer.
+     * 
+     * @param iss issuer to set
+     */
+    public void setIssuer(final String iss) {
+        issuer = iss;
+    }
+
+    /**
+     * Get the subject.
+     * 
+     * @return subject
+     */
+    public String getSubject() {
+        return subject;
+    }
+
+    /**
+     * Set the subject.
+     * 
+     * @param sub subject to set.
+     */
+    public void setSubject(final String sub) {
+        subject = sub;
+    }
+
+    /**
+     * Get the issuance time.
+     * 
+     * @return issuance time
+     */
+    public Instant getIssuedAt() {
+        return issuedAt;
+    }
+
+    /**
+     * Set the issuance time.
+     * 
+     * @param iat issuance time
+     */
+    public void setIssuedAt(final Instant iat) {
+        issuedAt = iat;
+    }
+
+    /**
+     * Get the expiration time.
+     * 
+     * @return expiration time
+     */
+    public Instant getExpiration() {
+        return expiration;
+    }
+
+    /**
+     * Set the expiration time.
+     * 
+     * @param exp expiration time
+     */
+    public void setExpiration(final Instant exp) {
+        expiration = exp;
+    }
+
+    /**
+     * Get the JWK set.
+     * 
+     * @return JWK set
+     */
+    public JWKSet getJwks() {
+        return jwkSet;
+    }
+
+    /**
+     * Set the JWK set.
+     * 
+     * @param jwks JWK set
+     */
+    public void setJwks(final JWKSet jwks) {
+        jwkSet = jwks;
+    }
+
+    /**
+     * Get the metadata.
+     * 
+     * @return metadata
+     */
+    public Map<String, Map<String, Object>> getMetadata() {
+        return metadata;
+    }
+
+    /**
+     * Set the metadata.
+     * 
+     * @param map metadata
+     */
+    public void setMetadata(final Map<String, Map<String, Object>> map) {
+        metadata = map;
+    }
+
+    /**
+     * Get the critical claims.
+     * 
+     * @return critical claims
+     */
+    public List<String> getCritical() {
+        return critical;
+    }
+
+    /**
+     * Set the critical claims.
+     * 
+     * @param crit critical claims
+     */
+    public void setCritical(final List<String> crit) {
+        critical = crit;
+    }
+
+    /**
+     * Get the map of custom claims.
+     * 
+     * @return The map of any other claims not directly mapped.
+     */
+    @JsonAnyGetter
+    public Map<String, Object> getCustomClaims() {
+        return customClaims;
+    }
+
+    /**
+     * Add a custom claim to the map of custom claims.
+     * 
+     * @param name The name of the custom claim.
+     * @param value The value of the custom claim.
+     */
+    @JsonAnySetter
+    public void setCustomClaims(final String name, final Object value) {
+        customClaims.put(name, value);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("iss", issuer)
+                .add("sub", subject)
+                .add("iat", issuedAt)
+                .add("exp", expiration)
+                .add("jwks", jwkSet)
+                .add("metadata", metadata)
+                .add("crit", critical)
+                .add("customClaims", customClaims).toString();
+    }
+
+}
diff --git a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/SubordinateStatement.java b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/SubordinateStatement.java
new file mode 100644
index 0000000..f3f5793
--- /dev/null
+++ b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/SubordinateStatement.java
@@ -0,0 +1,60 @@
+/*
+ * 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 javax.annotation.Nonnull;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.shared.logic.ConstraintViolationException;
+
+/**
+ * A wrapper class extending {@link EntityStatement} with parsed claims related to subordinate statements.
+ */
+public class SubordinateStatement extends EntityStatement {
+
+    /**
+     * Constructor.
+     *
+     * @param signedJwt subordinate statement
+     * @param payload subordinate statement payload
+     * @throws ConstraintViolationException if the payload syntax/content is not expected
+     */
+    protected SubordinateStatement(@Nonnull final SignedJWT signedJwt,
+            @Nonnull final SubordinateStatementPayload payload) 
+        throws ConstraintViolationException {
+        super(signedJwt, payload);
+    }
+
+    /**
+     * Parse a {@link SubordinateStatement} from the given JWT by using the given object mapper.
+     * 
+     * @param jwt entity statement JWT
+     * @param objectMapper object mapper capable of parsing JWT payload
+     * @return subordinate statement
+     * @throws JsonProcessingException if the payload could not be parsed
+     */
+    @Nonnull public static SubordinateStatement parse(@Nonnull final SignedJWT jwt,
+            @Nonnull final ObjectMapper objectMapper) throws JsonProcessingException {
+        final JavaType objectType = objectMapper.constructType(SubordinateStatementPayload.class);
+        final SubordinateStatementPayload result =
+                objectMapper.readValue(jwt.getPayload().toString(), objectType);
+        assert result != null;
+        return new SubordinateStatement(jwt, result);
+    }
+}
diff --git a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/SubordinateStatementPayload.java b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/SubordinateStatementPayload.java
new file mode 100644
index 0000000..cbdae5a
--- /dev/null
+++ b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/SubordinateStatementPayload.java
@@ -0,0 +1,138 @@
+/*
+ * 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.List;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.MoreObjects;
+
+import net.shibboleth.oidc.metadata.policy.MetadataPolicy;
+
+/**
+ * Subordinate Statement payload claims as defined by the OpenID Federation 1.0 Section 3.3. This class extends the
+ * list of claims defined by {@link EntityStatementPayload} with the claims that MUST or MAY appear in Subordinate
+ * Statements but not in Entity Configurations.
+ */
+public class SubordinateStatementPayload extends EntityStatementPayload {
+
+    /** A map that defines Trust Chain constraints. */
+    @JsonProperty("constraints") private Map<String, Object> constraints;
+
+    /** A map that defines a metadata policy. */
+    @JsonProperty("metadata_policy") private Map<String,Map<String,MetadataPolicy>> metadataPolicy;
+
+    /** A list of critical metadata policy operators. */
+    @JsonProperty("metadata_policy_crit") private List<String> metadataPolicyCritical;
+
+    /** A string containing the fetch endpoint URL from which the entity statement was issued. */
+    @JsonProperty("source_endpoint") private String sourceEndpoint;
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("iss", getIssuer())
+                .add("sub", getSubject())
+                .add("iat", getIssuedAt())
+                .add("exp", getExpiration())
+                .add("jwks", getJwks())
+                .add("metadata", getMetadata())
+                .add("crit", getCritical())
+                .add("constraints", constraints)
+                .add("metadata_policy", metadataPolicy)
+                .add("metadata_policy_crit", metadataPolicyCritical)
+                .add("source_endpoint", sourceEndpoint)
+                .add("customClaims", getCustomClaims()).toString();
+    }
+
+    /**
+     * Get the map that defines Trust Chain constraints.
+     * 
+     * @return constraints
+     */
+    @Nullable public Map<String, Object> getConstraints() {
+        return constraints;
+    }
+
+    /**
+     * Set the map that defines Trust Chain constraints.
+     * 
+     * @param map constraints
+     */
+    public void setConstraints(@Nullable final Map<String, Object> map) {
+        constraints = map;
+    }
+
+    /**
+     * Get the map that defines a metadata policy.
+     * 
+     * @return metadata policy
+     */
+    @Nullable public Map<String, Map<String, MetadataPolicy>> getMetadataPolicy() {
+        return metadataPolicy;
+    }
+
+    /**
+     * Set the map that defines a metadata policy.
+     * 
+     * @param map metadata policy
+     */
+    public void setMetadataPolicy(@Nullable final Map<String, Map<String, MetadataPolicy>> map) {
+        metadataPolicy = map;
+    }
+
+    /**
+     * Get the list of critical metadata policy operators.
+     * 
+     * @return critical metadata policy operators
+     */
+    @Nullable public List<String> getMetadataPolicyCritical() {
+        return metadataPolicyCritical;
+    }
+
+    /**
+     * Set the list of critical metadata policy operators.
+     * 
+     * @param operators critical metadata policy operators
+     */
+    public void setMetadataPolicyCritical(@Nullable final List<String> operators) {
+        metadataPolicyCritical = operators;
+    }
+
+    /**
+     * Get the fetch endpoint URL from which the entity statement was issued.
+     * 
+     * @return fetch endpoint URL
+     */
+    @Nullable public String getSourceEndpoint() {
+        return sourceEndpoint;
+    }
+
+    /**
+     * Set the fetch endpoint URL from which the entity statement was issued.
+     * 
+     * @param url endpoint URL
+     */
+    public void setSourceEndpoint(@Nullable final String url) {
+        sourceEndpoint = url;
+    }
+
+    
+}
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 3b2f939..c4b1b16 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
@@ -24,8 +24,8 @@ import javax.annotation.Nullable;
 import org.opensaml.messaging.context.BaseContext;
 
 import com.nimbusds.jwt.SignedJWT;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.TrustedRemoteResolverEntity;
 import net.shibboleth.shared.collection.Pair;
 
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/attribute/filter/policyrule/impl/EntityTrustMarkPolicyRule.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/attribute/filter/policyrule/impl/EntityTrustMarkPolicyRule.java
index 98b5f86..c58384a 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/attribute/filter/policyrule/impl/EntityTrustMarkPolicyRule.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/attribute/filter/policyrule/impl/EntityTrustMarkPolicyRule.java
@@ -123,7 +123,7 @@ public class EntityTrustMarkPolicyRule extends AbstractIdentifiableInitializable
         final String entityId = Optional.ofNullable(trustChainContext.getSelectedTrustChain())
                 .map(pair -> pair.getFirst())
                 .map(chain -> chain.get(0))
-                .map(statement -> statement.getEntityID().getValue())
+                .map(statement -> statement.getSubject())
                 .orElse(null);
         if (entityId == null) {
             log.debug("{} Could not resolve entity ID from selected trust chain", getLogPrefix());
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/decoding/impl/ExplicitClientRegistrationRequestDecoder.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/decoding/impl/ExplicitClientRegistrationRequestDecoder.java
index 0005384..a6ede2d 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/decoding/impl/ExplicitClientRegistrationRequestDecoder.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/decoding/impl/ExplicitClientRegistrationRequestDecoder.java
@@ -16,6 +16,7 @@ package net.shibboleth.idp.plugin.oidc.op.oidfed.decoding.impl;
 
 import java.io.IOException;
 import java.net.URI;
+import java.text.ParseException;
 import java.util.List;
 import java.util.Objects;
 
@@ -25,18 +26,22 @@ import javax.annotation.Nullable;
 import org.opensaml.messaging.decoder.MessageDecodingException;
 import org.slf4j.Logger;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.base.MoreObjects;
+import com.nimbusds.jwt.SignedJWT;
 import com.nimbusds.oauth2.sdk.http.HTTPRequest;
 import com.nimbusds.oauth2.sdk.http.JakartaServletUtils;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 
 import jakarta.servlet.http.HttpServletRequest;
 import net.shibboleth.idp.plugin.oidc.op.decoding.impl.RequestUtil;
 import net.shibboleth.idp.plugin.oidc.op.oauth2.decoding.impl.BaseOAuth2RequestDecoder;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ExplicitClientRegistrationRequest;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityConfiguration;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatementHelper;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubordinateStatement;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
@@ -92,17 +97,17 @@ public class ExplicitClientRegistrationRequestDecoder
             final String contentType = request.getContentType();
             if ("application/entity-statement+jwt".equals(contentType)) {
                 final EntityStatement entityConfiguration =
-                        EntityStatementHelper.deserializeEntityStatement(httpRequest.getQuery());
+                        deserializeEntityStatement(httpRequest.getQuery());
                 if (entityConfiguration == null) {
                     throw new MessageDecodingException("Could not deserialize entity configuration");
                 }
-                log.trace("Entity configuration claims set: {}", entityConfiguration.getClaimsSet());
+                log.trace("Entity configuration claims set: {}", entityConfiguration.getParsedPayload());
                 return new ExplicitClientRegistrationRequest(uri, entityConfiguration);
             } else if ("application/trust-chain+json".equals(contentType)) {
                 final TypeReference<List<String>> typeReference = new TypeReference<List<String>>() {};
                 final List<String> strings = objectMapper.readValue(httpRequest.getQuery(), typeReference);
                 final List<EntityStatement> trustChain = strings.stream()
-                        .map(string -> EntityStatementHelper.deserializeEntityStatement(string))
+                        .map(string -> deserializeEntityStatement(string))
                         .filter(Objects::nonNull)
                         .toList();
                 if (trustChain == null || trustChain.isEmpty()) {
@@ -119,6 +124,24 @@ public class ExplicitClientRegistrationRequestDecoder
         }
     }
 
+    @Nullable private EntityStatement deserializeEntityStatement(@Nullable final String input) {
+        try {
+            final SignedJWT jwt = SignedJWT.parse(input);
+            assert jwt != null;
+            final String subject = jwt.getJWTClaimsSet().getSubject();
+            final String issuer = jwt.getJWTClaimsSet().getIssuer();
+            if (subject != null && subject.equals(issuer)) {
+                assert objectMapper != null;
+                return EntityConfiguration.parse(jwt, objectMapper);
+            } else {
+                assert objectMapper != null;
+                return SubordinateStatement.parse(jwt, objectMapper);
+            }
+        } catch (final JsonProcessingException | ParseException e) {
+            log.warn("Could not parse entity statement", e);
+            return null;
+        }
+    }
     /** {@inheritDoc} */
     @Override
     protected String getMessageToLog(@Nullable final ExplicitClientRegistrationRequest message) {
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/messaging/impl/ExplicitClientRegistrationRequest.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/messaging/impl/ExplicitClientRegistrationRequest.java
index 5a9d978..f1c093b 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/messaging/impl/ExplicitClientRegistrationRequest.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/messaging/impl/ExplicitClientRegistrationRequest.java
@@ -22,8 +22,8 @@ import javax.annotation.Nullable;
 
 import com.nimbusds.oauth2.sdk.Request;
 import com.nimbusds.oauth2.sdk.http.HTTPRequest;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
 import net.shibboleth.shared.logic.Constraint;
 
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/AbstractEntityStatementSignatureValidationComponent.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/AbstractEntityStatementSignatureValidationComponent.java
deleted file mode 100644
index 2c48bf2..0000000
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/AbstractEntityStatementSignatureValidationComponent.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * 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 javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.opensaml.security.trust.TrustEngine;
-
-import com.nimbusds.jwt.SignedJWT;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-
-import net.shibboleth.shared.resolver.CriteriaSet;
-
-/**
- * Abstract component performing entity statement signature validation via {@link TrustEngine}.
- */
-public class AbstractEntityStatementSignatureValidationComponent
-    extends AbstractTrustEngineSignatureValidationComponent {
-
-    /**
-     * Validates the given entity statement via trust engine and the given criteria.
-     * 
-     * @param entityStatement the entity statement to be validated
-     * @param criteria the criteria (expanded with the optional default criteria)
-     * @param entityId the entity ID used for logging
-     * @return true if validation was successful, false otherwise
-     */
-    protected boolean validateStatement(@Nonnull final EntityStatement entityStatement,
-            @Nonnull final CriteriaSet criteria, @Nullable final String entityId) {
-        final SignedJWT jwt = entityStatement.getSignedStatement();
-        assert jwt != null;
-        return validateJwt(jwt, criteria, entityId);
-    }
-}
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultCombinedMetadataFromTrustChainLookupStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultCombinedMetadataFromTrustChainLookupStrategy.java
index ec2eda5..ddf4e49 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultCombinedMetadataFromTrustChainLookupStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultCombinedMetadataFromTrustChainLookupStrategy.java
@@ -26,7 +26,6 @@ import javax.annotation.Nullable;
 import org.slf4j.Logger;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.collection.CollectionSupport;
@@ -75,12 +74,12 @@ public class DefaultCombinedMetadataFromTrustChainLookupStrategy extends Abstrac
             return null;
         }
         final Map<String,Map<String,Object>> configurationMetadata =
-                EntityStatementHelper.parseMetadata(objectMapper, chain.get(0));
+                chain.get(0).getParsedPayload().getMetadata();
         final Map<String,Map<String,Object>> subordinateMetadata =
-                EntityStatementHelper.parseMetadata(objectMapper, chain.get(1));
+                chain.get(1).getParsedPayload().getMetadata();
 
         if (configurationMetadata == null || configurationMetadata.isEmpty()) {
-            log.error("Entity configuration for {} doesn't contain metadata", chain.get(0).getEntityID().getValue());
+            log.error("Entity configuration for {} doesn't contain metadata", chain.get(0).getSubject());
             return null;
         }
 
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityConfigurationEndpointExtractionStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityConfigurationEndpointExtractionStrategy.java
deleted file mode 100644
index 0ecb3b6..0000000
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityConfigurationEndpointExtractionStrategy.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * 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.net.URI;
-import java.net.URISyntaxException;
-import java.util.function.Function;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.slf4j.Logger;
-
-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 strategy for extracting entity configuration endpoint URI from the given {@link CriteriaSet}.
- */
- at ThreadSafeAfterInit
-public class DefaultEntityConfigurationEndpointExtractionStrategy extends AbstractIdentifiableInitializableComponent
-        implements Function<CriteriaSet, URI> {
-
-    /** Class logger. */
-    @Nonnull private Logger log = LoggerFactory.getLogger(DefaultEntityConfigurationEndpointExtractionStrategy.class);
-
-    /** Strategy for fetching entity ID from the criteria set. */
-    @NonnullAfterInit private Function<CriteriaSet, String> criteriaToEntityIdStrategy;
-
-    /**
-     * Set the strategy for fetching entity ID from the criteria set.
-     * 
-     * @param strategy lookup strategy
-     */
-    public void setCriteriaToEntityIdStrategy(@Nonnull final Function<CriteriaSet, String> strategy) {
-        checkSetterPreconditions();
-        criteriaToEntityIdStrategy = Constraint.isNotNull(strategy, "Criteria to entity ID strategy cannot be null");
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    protected void doInitialize() throws ComponentInitializationException {
-        super.doInitialize();
-        if (criteriaToEntityIdStrategy == null) {
-            throw new ComponentInitializationException("Criteria to entity ID strategy cannot be null");
-        }
-    }
-
-    /** {@inheritDoc} */
-    @Override @Nullable
-    public URI apply(@Nullable final CriteriaSet criteria) {
-        checkComponentActive();
-        final String entityId = criteriaToEntityIdStrategy.apply(criteria);
-        if (entityId == null) {
-            return null;
-        }
-        try {
-            return new URI(entityId + (entityId.endsWith("/") ? "" : "/") + ".well-known/openid-federation");
-        } catch (final URISyntaxException e) {
-            log.error("Could not build URI for entity configuration endpoint", e);
-        }
-        return null;    
-    }
-
-}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementContentValidationFilterStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementContentValidationFilterStrategy.java
deleted file mode 100644
index 2b335bd..0000000
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementContentValidationFilterStrategy.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * 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.function.Function;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.slf4j.Logger;
-
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-
-import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
-import net.shibboleth.oidc.jwt.claims.JWTValidationException;
-import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
-import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
-import net.shibboleth.shared.logic.Constraint;
-import net.shibboleth.shared.primitive.LoggerFactory;
-
-/**
- * Default content validating filter for entity statement. Validates the header contents of the entity statement and
- * claims via configurable claims validator. Note that the {@link ProfileRequestContext} is not fed to the claims
- * validator.
- */
- at ThreadSafeAfterInit
-public class DefaultEntityStatementContentValidationFilterStrategy
-    extends AbstractIdentifiableInitializableComponent implements Function<EntityStatement, EntityStatement> {
-
-    /** Class logger. */
-    @Nonnull private Logger log =
-            LoggerFactory.getLogger(DefaultEntityStatementContentValidationFilterStrategy.class);
-
-    /** The claims validator to use for validating the entity statement claims. */
-    @NonnullAfterInit private ClaimsValidator claimsValidator;
-
-    /**
-     * Set the claims validator to use for validating the entity statement claims.
-     * 
-     * @param validator claims validator
-     */
-    public void setClaimsValidator(@Nonnull final ClaimsValidator validator) {
-        checkSetterPreconditions();
-        claimsValidator = Constraint.isNotNull(validator, "Claims validator cannot be null");
-    }
-
-    /** {@inheritDoc} */
-    @Override @Nullable
-    public EntityStatement apply(@Nullable final EntityStatement entityStatement) {
-        checkComponentActive();
-        if (entityStatement == null) {
-            return null;
-        }
-
-        final String entityId = entityStatement.getEntityID().getValue();
-
-        //TODO: header validations
-
-        try {
-            claimsValidator.validate(entityStatement.getSignedStatement().getJWTClaimsSet(), null);
-        } catch (final JWTValidationException | ParseException e) {
-            log.warn("Claims validation failed for entity configuration {}", entityId, e);
-            return null;
-        }
-        log.debug("Entity configuration {} claims successfully validated", entityId);
-        return entityStatement;
-    }
-}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementExpirationTimeStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementExpirationTimeStrategy.java
index 7960cfd..6c20b3e 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementExpirationTimeStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementExpirationTimeStrategy.java
@@ -20,8 +20,6 @@ import java.util.function.Function;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.ThreadSafe;
 
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-
 import net.shibboleth.oidc.metadata.cache.ExpirationTimeContext;
 
 /**
@@ -41,7 +39,7 @@ public class DefaultEntityStatementExpirationTimeStrategy
         if (statement == null) {
             return contextExpiration;
         }
-        final Instant metadataExpiration = statement.getClaimsSet().getExpirationTime().toInstant();
+        final Instant metadataExpiration = statement.getParsedPayload().getExpiration();
         return metadataExpiration.isBefore(contextExpiration) ? metadataExpiration : contextExpiration;
     }
 
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementFetchingStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementFetchingStrategy.java
deleted file mode 100644
index 6445b33..0000000
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementFetchingStrategy.java
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * 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.io.IOException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.Optional;
-import java.util.function.Function;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.apache.hc.client5.http.classic.HttpClient;
-import org.apache.hc.client5.http.classic.methods.HttpGet;
-import org.apache.hc.client5.http.protocol.HttpClientContext;
-import org.apache.hc.core5.http.ClassicHttpRequest;
-import org.apache.hc.core5.http.ClassicHttpResponse;
-import org.apache.hc.core5.http.HttpEntity;
-import org.apache.hc.core5.http.HttpStatus;
-import org.apache.hc.core5.http.ParseException;
-import org.apache.hc.core5.http.io.entity.EntityUtils;
-import org.opensaml.security.httpclient.HttpClientSecurityParameters;
-import org.opensaml.security.httpclient.HttpClientSecuritySupport;
-import org.slf4j.Logger;
-
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-
-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.primitive.StringSupport;
-import net.shibboleth.shared.resolver.CriteriaSet;
-
-/**
- * Default entity statement fetching strategy via configurable {@link HttpClient} and strategy for looking up the
- * endpoint URI.
- */
- at ThreadSafeAfterInit
-public class DefaultEntityStatementFetchingStrategy extends AbstractIdentifiableInitializableComponent
-        implements Function<CriteriaSet, EntityStatement> {
-
-    /** Class logger. */
-    @Nonnull private Logger log = LoggerFactory.getLogger(DefaultEntityStatementFetchingStrategy.class);
-
-    /** HTTP client to use. */
-    @NonnullAfterInit protected HttpClient httpClient;
-
-    /** HTTP client security parameters. */
-    @Nullable private HttpClientSecurityParameters httpClientSecurityParameters;
-
-    /** Strategy to lookup URI from the criteria set. */
-    @NonnullAfterInit private Function<CriteriaSet, URI> criteriaToEndpointStrategy;
-
-    /**
-     * Set the {@link HttpClient} to use.
-     * 
-     * @param client HTTP client to use
-     */
-    public void setHttpClient(@Nonnull final HttpClient client) {
-        checkSetterPreconditions();
-        httpClient = Constraint.isNotNull(client, "HttpClient cannot be null");
-    }
-
-    /**
-     * Set the optional client security parameters.
-     * 
-     * @param params the new client security parameters
-     */
-    public void setHttpClientSecurityParameters(@Nullable final HttpClientSecurityParameters params) {
-        checkSetterPreconditions();
-        httpClientSecurityParameters = params;
-    }
-
-    /**
-     * Set the strategy to lookup URI from the criteria set.
-     * 
-     * @param strategy lookup strategy
-     */
-    public void setCriteriaToEndpointStrategy(@Nonnull final Function<CriteriaSet, URI> strategy) {
-        checkSetterPreconditions();
-        criteriaToEndpointStrategy = Constraint.isNotNull(strategy, "Criteria to endpoint strategy cannot be null");
-    }
-
-    /**
-     * Build the {@link HttpClientContext} instance to be used by the HttpClient.
-     * 
-     * @param request the HTTP client request
-     * @return the client context instance
-     */
-    @Nonnull
-    protected HttpClientContext buildHttpContext(@Nonnull final ClassicHttpRequest request) {
-        final HttpClientContext clientContext = HttpClientContext.create();
-        assert clientContext != null;
-        HttpClientSecuritySupport.marshalSecurityParameters(clientContext, httpClientSecurityParameters, false);
-        HttpClientSecuritySupport.addDefaultTLSTrustEngineCriteria(clientContext, request);
-        return clientContext;
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    protected void doInitialize() throws ComponentInitializationException {
-        super.doInitialize();
-        if (httpClient == null) {
-            throw new ComponentInitializationException("Httpclient cannot be null");
-        }
-        if (criteriaToEndpointStrategy == null) {
-            throw new ComponentInitializationException("Criteria to endpoint strategy cannot be null");
-        }
-    }
-
-    /** {@inheritDoc} */
-    @Override @Nullable
-    public EntityStatement apply(@Nullable final CriteriaSet criteria) {
-        checkComponentActive();
-        final URI uri = criteriaToEndpointStrategy.apply(criteria);
-        if (uri == null || StringSupport.trimOrNull(uri.getScheme()) == null) {
-            log.error("No valid URI could be resolved for fetching entity statement: {}", uri);
-            return null;
-        }
-        log.debug("Using URI {} for fetching entity statement", uri);
-        final HttpGet httpRequest = new HttpGet(uri);
-        final HttpClientContext httpContext = buildHttpContext(httpRequest);
-        try (final ClassicHttpResponse response = httpClient.executeOpen(null, httpRequest, httpContext)) {
-            final String scheme = httpRequest.getUri().getScheme();
-            assert scheme != null;
-            HttpClientSecuritySupport.checkTLSCredentialEvaluated(httpContext, scheme);
-            if (response != null && response.getCode() == HttpStatus.SC_OK) {
-                final HttpEntity responseEntity = response.getEntity();
-                if (!"application/entity-statement+jwt".equals(responseEntity.getContentType())) {
-                    log.warn("Unexpected response content type {} from URI {}", responseEntity.getContentType(), uri);
-                    return null;
-                }
-                final EntityStatement entityStatement =
-                        EntityStatementHelper.deserializeEntityStatement(EntityUtils.toString(response.getEntity()));
-                final String type = Optional.ofNullable(entityStatement)
-                        .map(statement -> statement.getSignedStatement().getHeader().getType())
-                        .map(header -> header.getType()).orElse(null);
-                if (!"entity-statement+jwt".equals(type)) {
-                    log.warn("Ignoring entity statement with unexpected type {}", type);
-                    return null;
-                }
-                return entityStatement;
-            } else {
-                log.debug("Unable to fetch entity configuration from URI: {} (HTTP status {})", uri,
-                        response == null ? null : response.getCode());
-                return null;
-            }
-        } catch (final ParseException | URISyntaxException | IOException e) {
-            log.debug("Unable to fetch entity statement from URI: {}", uri, e);
-        }
-
-        return null;
-    }
-
-}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementSignatureValidationFilterStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementSignatureValidationFilterStrategy.java
index e4952b2..44bdc68 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementSignatureValidationFilterStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementSignatureValidationFilterStrategy.java
@@ -22,8 +22,7 @@ import javax.annotation.Nullable;
 import org.opensaml.security.trust.TrustEngine;
 import org.slf4j.Logger;
 
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.SubjectEntityStatementCriterion;
 import net.shibboleth.oidc.metadata.filter.MetadataFilterContext;
 import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
 import net.shibboleth.shared.primitive.LoggerFactory;
@@ -35,7 +34,7 @@ import net.shibboleth.shared.resolver.CriteriaSet;
  */
 @ThreadSafeAfterInit
 public class DefaultEntityStatementSignatureValidationFilterStrategy
-    extends AbstractEntityStatementSignatureValidationComponent
+    extends AbstractTrustEngineSignatureValidationComponent
     implements BiFunction<EntityStatement, MetadataFilterContext, EntityStatement> {
 
     /** Class logger. */
@@ -51,10 +50,10 @@ public class DefaultEntityStatementSignatureValidationFilterStrategy
             return null;
         }
 
-        final String entityId = entityStatement.getEntityID().getValue();
+        final String entityId = entityStatement.getSubject();
         log.trace("Starting signature validation of entity statement for {}", entityId);
         final CriteriaSet criteria = new CriteriaSet(new SubjectEntityStatementCriterion(entityStatement));
-        if (validateStatement(entityStatement, criteria, entityId)) {
+        if (validateJwt(entityStatement.getJwt(), criteria, entityId)) {
             return entityStatement;
         }
         return null;
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementSubjectIdentifierExtractionStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementSubjectIdentifierExtractionStrategy.java
deleted file mode 100644
index 75e6fec..0000000
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementSubjectIdentifierExtractionStrategy.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * 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.function.Function;
-
-import javax.annotation.Nullable;
-import javax.annotation.concurrent.ThreadSafe;
-
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-
-/**
- * Default strategy to extract entity ID from an entity statement.
- */
- at ThreadSafe
-public class DefaultEntityStatementSubjectIdentifierExtractionStrategy
-        implements Function<EntityStatement, String> {
-
-    /** {@inheritDoc} */
-    @Override @Nullable
-    public String apply(@Nullable final EntityStatement entityStatement) {
-        if (entityStatement == null) {
-            return null;
-        }
-        return entityStatement.getEntityID().getValue();
-    }
-
-}
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 0bd362a..e917c57 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
@@ -26,7 +26,6 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.constraints.FederationPolicyConstraint;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.constraints.FederationPolicyConstraintHelper;
@@ -125,7 +124,7 @@ public class DefaultProvidedTrustChainValidationStrategy extends AbstractIdentif
             if (!FederationPolicyConstraintHelper.verifyPolicyConstraints(
                     objectMapper, trustChain.get(i), trustChain.subList(0, i), federationPolicyConstraints)) {
                 log.debug("Subordinate statement {} policy constraints validation failed",
-                        trustChain.get(i).getEntityID());
+                        trustChain.get(i).getSubject());
                 return false;
             }
         }
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityResponseSignatureValidationFilterStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityResponseSignatureValidationFilterStrategy.java
index 7284e9c..c7b1d33 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityResponseSignatureValidationFilterStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultResolveEntityResponseSignatureValidationFilterStrategy.java
@@ -25,9 +25,10 @@ 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.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityResponse;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.RemoteEntityConfigurationContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.SubjectEntityStatementCriterion;
 import net.shibboleth.oidc.metadata.cache.MetadataCache;
 import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
 import net.shibboleth.oidc.metadata.filter.MetadataFilterContext;
@@ -52,14 +53,14 @@ public class DefaultResolveEntityResponseSignatureValidationFilterStrategy
             LoggerFactory.getLogger(DefaultResolveEntityResponseSignatureValidationFilterStrategy.class);
 
     /** Cache used to fetch the issuer entity configuration from. */
-    @NonnullAfterInit private MetadataCache<EntityStatement> entityConfigurationCache;
+    @NonnullAfterInit private MetadataCache<RemoteEntityConfigurationContainer> entityConfigurationCache;
 
     /**
      * Set the cache used to fetch the issuer entity configuration from.
      * 
      * @param cache cache used to fetch the issuer entity configuration from
      */
-    public void setEntityConfigurationCache(@Nonnull final MetadataCache<EntityStatement> cache) {
+    public void setEntityConfigurationCache(@Nonnull final MetadataCache<RemoteEntityConfigurationContainer> cache) {
         checkSetterPreconditions();
         entityConfigurationCache = Constraint.isNotNull(cache, "Entity Configuration cache cannot be null");
     }
@@ -115,9 +116,9 @@ public class DefaultResolveEntityResponseSignatureValidationFilterStrategy
     @Nullable protected EntityStatement fetchIssuerStatement(@Nonnull final String issuer) {
         final CriteriaSet criteria = new CriteriaSet(new SubjectEntityIDCriterion(issuer));
         try {
-            final List<EntityStatement> result = entityConfigurationCache.get(criteria);
+            final List<RemoteEntityConfigurationContainer> result = entityConfigurationCache.get(criteria);
             if (!result.isEmpty()) {
-                return result.get(0);
+                return result.get(0).getEntityStatement();
             }
         } catch (final MetadataCacheException e) {
             log.debug("Error while fetching issuer entity configuration for {}", issuer, e);
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultSubordinateStatementEndpointExtractionStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultSubordinateStatementEndpointExtractionStrategy.java
deleted file mode 100644
index b4e19e1..0000000
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultSubordinateStatementEndpointExtractionStrategy.java
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * 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.net.URI;
-import java.net.URISyntaxException;
-import java.util.List;
-import java.util.function.Function;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.slf4j.Logger;
-
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-import com.nimbusds.openid.connect.sdk.federation.entities.FederationEntityMetadata;
-
-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.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 strategy for fetching endpoint URI used for fetching subordinate statement. The issuer's entity configuration
- * is fetched from a configurable {@link MetadataCache}.
- */
- at ThreadSafeAfterInit
-public class DefaultSubordinateStatementEndpointExtractionStrategy extends AbstractIdentifiableInitializableComponent
-        implements Function<CriteriaSet, URI> {
-
-    /** Class logger. */
-    @Nonnull private final Logger log =
-            LoggerFactory.getLogger(DefaultSubordinateStatementEndpointExtractionStrategy.class);
-
-    /** Cache containing entity configurations. */
-    @NonnullAfterInit private MetadataCache<EntityStatement> entityConfigurationCache;
-
-    /** Strategy to fetch subject entity ID from criteria set. */
-    @NonnullAfterInit private Function<CriteriaSet, String> criteriaToSubjectEntityIdStrategy;
-
-    /** Strategy to fetch issuer entity ID from criteria set. */
-    @NonnullAfterInit private Function<CriteriaSet, String> criteriaToIssuerEntityIdStrategy;
-
-    /**
-     * Set the strategy to fetch subject entity ID from criteria set.
-     * 
-     * @param strategy lookup strategy
-     */
-    public void setCriteriaToSubjectEntityIdStrategy(@Nonnull final Function<CriteriaSet, String> strategy) {
-        checkSetterPreconditions();
-        criteriaToSubjectEntityIdStrategy = Constraint.isNotNull(strategy,
-                "Criteria to subject entity ID strategy cannot be null");
-    }
-
-    /**
-     * Set the strategy to fetch issuer entity ID from criteria set.
-     * 
-     * @param strategy lookup strategy
-     */
-    public void setCriteriaToIssuerEntityIdStrategy(@Nonnull final Function<CriteriaSet, String> strategy) {
-        checkSetterPreconditions();
-        criteriaToIssuerEntityIdStrategy = Constraint.isNotNull(strategy,
-                "Criteria to issuer entity ID strategy cannot be null");
-    }
-
-    /**
-     * Set the cache containing entity configurations.
-     * 
-     * @param cache cache containing entity configurations
-     */
-    public void setEntityConfigurationCache(@Nonnull final MetadataCache<EntityStatement> cache) {
-        checkSetterPreconditions();
-        entityConfigurationCache = Constraint.isNotNull(cache, "Entity Configuration cache cannot be null");
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    protected void doInitialize() throws ComponentInitializationException {
-        super.doInitialize();
-        if (criteriaToSubjectEntityIdStrategy == null) {
-            throw new ComponentInitializationException("Criteria to subject entity ID strategy cannot be null");
-        }
-        if (criteriaToIssuerEntityIdStrategy == null) {
-            throw new ComponentInitializationException("Criteria to issuer entity ID strategy cannot be null");
-        }
-        if (entityConfigurationCache == null) {
-            throw new ComponentInitializationException("Entity Configuration cache cannot be null");
-        }
-    }
-
-    /** {@inheritDoc} */
-    @Override @Nullable
-    public URI apply(@Nullable final CriteriaSet criteria) {
-        checkComponentActive();
-        final String subjectEntityId = criteriaToSubjectEntityIdStrategy.apply(criteria);
-        if (subjectEntityId == null) {
-            return null;
-        }
-        final String issuerEntityId = criteriaToIssuerEntityIdStrategy.apply(criteria);
-        if (issuerEntityId == null) {
-            return null;
-        }
-        final List<EntityStatement> issuerStatements;
-        try {
-            issuerStatements =
-                    entityConfigurationCache.get(new CriteriaSet(new SubjectEntityIDCriterion(issuerEntityId)));
-        } catch (final MetadataCacheException e) {
-            log.error("Could not resolve entity configuration for issuer {}", issuerEntityId, e);
-            return null;
-        }
-        if (issuerStatements.isEmpty()) {
-            log.error("Could not resolve entity configuration for issuer {}", issuerEntityId);
-            return null;
-        }
-        final FederationEntityMetadata issuerMetadata =
-                issuerStatements.get(0).getClaimsSet().getFederationEntityMetadata();
-        final URI federationFetchEndpoint =
-                issuerMetadata != null ? issuerMetadata.getFederationFetchEndpointURI() : null;
-        if (federationFetchEndpoint == null) {
-            log.warn("No federation fetch endpoint found from the entity configuration for {}", issuerEntityId);
-            return null;
-        }
-
-        try {
-            return new URI(federationFetchEndpoint + "?sub=" + subjectEntityId);
-        } catch (final URISyntaxException e) {
-            log.error("Could not build URI for fetching subordinate entity statement", e);
-        }
-        return null;    
-    }
-
-}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultSubordinateStatementIdentifierExtractionStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultSubordinateStatementIdentifierExtractionStrategy.java
deleted file mode 100644
index 7f2dc61..0000000
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultSubordinateStatementIdentifierExtractionStrategy.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * 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.function.Function;
-
-import javax.annotation.Nullable;
-import javax.annotation.concurrent.ThreadSafe;
-
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-
-import net.shibboleth.shared.collection.Pair;
-
-/**
- * Default strategy for transforming subordinate statement into a {@link Pair} consisting of issuer and subject entity
- * IDs.
- */
- at ThreadSafe
-public class DefaultSubordinateStatementIdentifierExtractionStrategy
-        implements Function<EntityStatement, Pair<String, String>> {
-
-    /** {@inheritDoc} */
-    @Override @Nullable
-    public Pair<String, String> apply(@Nullable final EntityStatement entityStatement) {
-        if (entityStatement == null) {
-            return null;
-        }
-        return new Pair<>(entityStatement.getClaimsSet().getIssuerEntityID().getValue(),
-                entityStatement.getEntityID().getValue());
-    }
-}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultSubordinateStatementSignatureValidationFilterStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultSubordinateStatementSignatureValidationFilterStrategy.java
deleted file mode 100644
index 9e33ec8..0000000
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultSubordinateStatementSignatureValidationFilterStrategy.java
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * 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.List;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.slf4j.Logger;
-
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-
-import net.shibboleth.oidc.metadata.cache.MetadataCache;
-import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
-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.ComponentInitializationException;
-import net.shibboleth.shared.logic.Constraint;
-import net.shibboleth.shared.primitive.LoggerFactory;
-import net.shibboleth.shared.resolver.CriteriaSet;
-
-/**
- * Default signature validating filter for subordinate statement. This implementation extends
- * {@link DefaultEntityStatementSignatureValidationFilterStrategy} by fetching the issuer's entity configuration via
- * configurable {@link MetadataCache} and passing it via {@link IssuerEntityStatementCriterion} in the criteria set.
- */
- at ThreadSafeAfterInit
-public class DefaultSubordinateStatementSignatureValidationFilterStrategy
-        extends DefaultEntityStatementSignatureValidationFilterStrategy {
-
-    /** Class logger. */
-    @Nonnull private Logger log =
-            LoggerFactory.getLogger(DefaultSubordinateStatementSignatureValidationFilterStrategy.class);
-
-    /** Cache used to fetch the issuer entity configuration from. */
-    @NonnullAfterInit private MetadataCache<EntityStatement> entityConfigurationCache;
-
-    /**
-     * Set the cache used to fetch the issuer entity configuration from.
-     * 
-     * @param cache cache used to fetch the issuer entity configuration from
-     */
-    public void setEntityConfigurationCache(@Nonnull final MetadataCache<EntityStatement> cache) {
-        checkSetterPreconditions();
-        entityConfigurationCache = Constraint.isNotNull(cache, "Entity Configuration cache cannot be null");
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    protected void doInitialize() throws ComponentInitializationException {
-        super.doInitialize();
-        if (entityConfigurationCache == null) {
-            throw new ComponentInitializationException("Entity Configuration cache cannot be null");
-        }
-    }
-
-    /** {@inheritDoc} */
-    @Override @Nullable
-    public EntityStatement apply(@Nullable final EntityStatement entityStatement,
-            @Nullable final MetadataFilterContext filterContext) {
-        checkComponentActive();
-        if (entityStatement == null) {
-            return null;
-        }
-
-        final String entityId = entityStatement.getEntityID().getValue();
-        final String issuer = entityStatement.getClaimsSet().getIssuerEntityID().getValue();
-        assert issuer != null;
-        log.trace("Validating subordinate statement for {}, issued by {}", entityId, issuer);
-        final EntityStatement issuerStatement = fetchIssuerStatement(issuer);
-        if (issuerStatement == null) {
-            return null;
-        }
-        final CriteriaSet criteria = new CriteriaSet();
-        criteria.add(new IssuerEntityStatementCriterion(issuerStatement));
-        criteria.add(new SubjectEntityStatementCriterion(entityStatement));
-        if (validateStatement(entityStatement, criteria, entityId)) {
-            return entityStatement;
-        }
-        return null;
-    }
-
-    /**
-     * Fetch the issuer entity configuration from the metadata cache.
-     * 
-     * @param issuer the issuer entity ID
-     * @return the issuer entity configuration, or null if could not be fetched
-     */
-    @Nullable protected EntityStatement fetchIssuerStatement(@Nonnull final String issuer) {
-        final CriteriaSet criteria = new CriteriaSet(new SubjectEntityIDCriterion(issuer));
-        try {
-            final List<EntityStatement> result = entityConfigurationCache.get(criteria);
-            if (!result.isEmpty()) {
-                return result.get(0);
-            }
-        } catch (final MetadataCacheException e) {
-            log.debug("Error while fetching issuer entity configuration for {}", issuer, e);
-        }
-        log.warn("Could not fetch entity configuration for {}", issuer);
-        return null;
-    }
-}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainExpirationTimeStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainExpirationTimeStrategy.java
deleted file mode 100644
index ddcc640..0000000
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainExpirationTimeStrategy.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * 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.time.Instant;
-import java.util.List;
-import java.util.function.Function;
-
-import javax.annotation.Nullable;
-import javax.annotation.concurrent.ThreadSafe;
-
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-
-import net.shibboleth.oidc.metadata.cache.ExpirationTimeContext;
-
-/**
- * Default strategy for fetching expiration time for the list of trust chains. The shortest expiration of all the chains
- * is taken and compared to the maximum cache duration defined in the {@link ExpirationTimeContext}.
- */
- at ThreadSafe
-public class DefaultTrustChainExpirationTimeStrategy
-        implements Function<ExpirationTimeContext<List<List<EntityStatement>>>, Instant> {
-
-    /** {@inheritDoc} */
-    @Nullable public Instant apply(@Nullable final ExpirationTimeContext<List<List<EntityStatement>>> context) {
-        if (context == null) {
-            return null;
-        }
-        final Instant contextExpiration = context.getNow().plus(context.getMaxCacheDuration());
-        final List<List<EntityStatement>> chains = context.getMetadata();
-        if (chains == null || chains.isEmpty()) {
-            return contextExpiration;
-        }
-        Instant metadataExpiration = null;
-        for (final List<EntityStatement> chain : chains) {
-            for (final EntityStatement statement : chain) {
-                final Instant statementExpiration = statement.getClaimsSet().getExpirationTime().toInstant();
-                metadataExpiration = metadataExpiration == null ? statementExpiration : 
-                    statementExpiration.isBefore(metadataExpiration) ? statementExpiration : metadataExpiration;
-            }
-        }
-        if (metadataExpiration == null) {
-            return contextExpiration;
-        }
-        return metadataExpiration.isBefore(contextExpiration) ? metadataExpiration : contextExpiration;
-    }
-
-}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainSubjectIdentifierExtractionStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainSubjectIdentifierExtractionStrategy.java
index 8367543..030890c 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainSubjectIdentifierExtractionStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainSubjectIdentifierExtractionStrategy.java
@@ -20,8 +20,6 @@ import java.util.function.Function;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.ThreadSafe;
 
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-
 /**
  * Default identifier strategy for the list of trust chains: the entity ID of the leaf entity is always used.
  */
@@ -39,7 +37,7 @@ public class DefaultTrustChainSubjectIdentifierExtractionStrategy
         if (firstChain == null || firstChain.isEmpty() || firstChain.get(0) == null) {
             return null;
         }
-        return firstChain.get(0).getEntityID().getValue();
+        return firstChain.get(0).getSubject();
     }
 
 }
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainTrustMarksParsingStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainTrustMarksParsingStrategy.java
index 623ef0b..fb1c5d7 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainTrustMarksParsingStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainTrustMarksParsingStrategy.java
@@ -26,13 +26,9 @@ import javax.annotation.Nullable;
 
 import org.slf4j.Logger;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.JavaType;
 import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.type.MapType;
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.SignedJWT;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
@@ -88,20 +84,22 @@ public class DefaultTrustChainTrustMarksParsingStrategy extends AbstractIdentifi
         final Map<String, List<SignedJWT>> result = new HashMap<>();
         for (final EntityStatement statement : trustChain) {
             assert statement != null;
-            final SignedJWT statementJwt = statement.getSignedStatement();
-            final List<Object> rawTrustMarks = statement.getClaimsSet().getJSONArrayClaim("trust_marks");
-            log.trace("Inspecting entity statement {} with trust marks {}", statementJwt.serialize(), rawTrustMarks);
+            if (statement.getParsedPayload() instanceof EntityConfigurationPayload ecp) {
+                final SignedJWT statementJwt = statement.getJwt();
+                final List<Map<String, String>> rawTrustMarks = ecp.getTrustMarks();
+                log.trace("Inspecting entity statement {} with trust marks {}", statementJwt.serialize(), rawTrustMarks);
 
-            if (rawTrustMarks != null && !rawTrustMarks.isEmpty()) {
-                log.trace("Transforming the trust mark into a JWT");
-                final List<SignedJWT> trustMarks = rawTrustMarks
-                    .stream()
-                    .map(entry -> parseTrustMark(entry))
-                    .filter(Objects::nonNull)
-                    .toList();
-                log.debug("Returning {} trust marks for entity {}", trustMarks.size(),
-                        statement.getEntityID().getValue());
-                result.put(statement.getEntityID().getValue(), trustMarks);
+                if (rawTrustMarks != null && !rawTrustMarks.isEmpty()) {
+                    log.trace("Transforming the trust mark into a JWT");
+                    final List<SignedJWT> trustMarks = rawTrustMarks
+                            .stream()
+                            .map(entry -> parseTrustMark(entry))
+                            .filter(Objects::nonNull)
+                            .toList();
+                    log.debug("Returning {} trust marks for entity {}", trustMarks.size(),
+                            statement.getSubject());
+                    result.put(statement.getSubject(), trustMarks);
+                }
             }
         }
         
@@ -114,23 +112,11 @@ public class DefaultTrustChainTrustMarksParsingStrategy extends AbstractIdentifi
      * @param trustMarkEntry trust mark entry as Strign to be parsed into a JWT
      * @return trust mark JWT if valid, null otherwise
      */
-    @Nullable private SignedJWT parseTrustMark(@Nullable final Object trustMarkEntry) {
+    @Nullable private SignedJWT parseTrustMark(@Nullable final Map<String, String> trustMarkEntry) {
         if (trustMarkEntry == null) {
             return null;
         }
-        final JavaType stringType = objectMapper.constructType(String.class);
-        final MapType stringMapType =
-                objectMapper.getTypeFactory().constructMapType(Map.class, stringType, stringType);
-        try {
-            final Map<String,String> map = objectMapper.readValue(trustMarkEntry.toString(), stringMapType);
-            if (map != null) {
-                log.debug("Parsed trust_mark map {}", map);
-                return verifyTrustMark(map.get("trust_mark"), map.get("trust_mark_type"));
-            }
-        } catch (final JsonProcessingException e) {
-            log.warn("Could not parse trust mark issuers from the trust chain", e);
-        }
-        return null;
+        return verifyTrustMark(trustMarkEntry.get("trust_mark"), trustMarkEntry.get("trust_mark_type"));
     }
 
     /**
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainTrustedTrustMarkIssuersLookupStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainTrustedTrustMarkIssuersLookupStrategy.java
index 64d15be..70dff7e 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainTrustedTrustMarkIssuersLookupStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainTrustedTrustMarkIssuersLookupStrategy.java
@@ -14,7 +14,6 @@
 
 package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
 
-import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.function.Function;
@@ -23,21 +22,13 @@ import java.util.stream.Collectors;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
-import org.slf4j.Logger;
-
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.JavaType;
 import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.type.ArrayType;
-import com.fasterxml.jackson.databind.type.MapType;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
-import net.shibboleth.shared.primitive.LoggerFactory;
 
 /**
  * Default function for fetching trusted trust mark issuers from a trust chain: they are read from the trust anchor's
@@ -46,10 +37,6 @@ import net.shibboleth.shared.primitive.LoggerFactory;
 public class DefaultTrustChainTrustedTrustMarkIssuersLookupStrategy extends AbstractIdentifiableInitializableComponent
     implements Function<List<EntityStatement>, Map<String, List<String>>> {
 
-    /** Class logger. */
-    @Nonnull private final Logger log =
-            LoggerFactory.getLogger(DefaultTrustChainTrustedTrustMarkIssuersLookupStrategy.class);
-
     /** JSON object mapper used for decoding JSON into Map. */
     @NonnullAfterInit private ObjectMapper objectMapper;
 
@@ -82,7 +69,7 @@ public class DefaultTrustChainTrustedTrustMarkIssuersLookupStrategy extends Abst
         return parseTrustedIssuers(trustChain)
                 .entrySet().stream()
                 .filter(entry -> entry.getKey() != null && entry.getValue() != null)
-                .map(entry -> Map.entry(entry.getKey(), Arrays.stream(entry.getValue()).toList()))
+                .map(entry -> Map.entry(entry.getKey(), entry.getValue()))
                 .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue));
     }
 
@@ -92,23 +79,10 @@ public class DefaultTrustChainTrustedTrustMarkIssuersLookupStrategy extends Abst
      * @param trustChain trust chain to be parsed
      * @return map of trusted issuers
      */
-    @Nonnull protected Map<String, String[]> parseTrustedIssuers(@Nonnull final List<EntityStatement> trustChain) {
-        final Object issuersClaim =
-                trustChain.get(trustChain.size() - 1).getClaimsSet().getClaim("trust_mark_issuers");
-        if (issuersClaim != null) {
-            final ArrayType arrayType = objectMapper.getTypeFactory().constructArrayType(String.class);
-            final JavaType stringType = objectMapper.constructType(String.class);
-            final MapType mapType =
-                    objectMapper.getTypeFactory().constructMapType(Map.class, stringType, arrayType);
-            try {
-                final Map<String, String[]> result = objectMapper.readValue(issuersClaim.toString(), mapType);
-                if (result != null) {
-                    return result;
-                }
-            } catch (final JsonProcessingException e) {
-                log.warn("Could not parse trust mark issuers from the trust chain", e);
-            }
-        }
-        return CollectionSupport.emptyMap();
+    @Nonnull protected Map<String, List<String>> parseTrustedIssuers(@Nonnull final List<EntityStatement> trustChain) {
+        final Map<String, List<String>> map =
+                trustChain.get(trustChain.size() - 1).getParsedPayload() instanceof EntityConfigurationPayload ecp ?
+                        ecp.getTrustMarkIssuers() : null;
+        return map == null ? CollectionSupport.emptyMap() : map;
     }
 }
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainTrustedTrustMarkOwnersLookupStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainTrustedTrustMarkOwnersLookupStrategy.java
index d1c7e03..e9e49d2 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainTrustedTrustMarkOwnersLookupStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainTrustedTrustMarkOwnersLookupStrategy.java
@@ -20,82 +20,37 @@ import java.util.function.Function;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
 
 import org.slf4j.Logger;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.JavaType;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.type.MapType;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-
-import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.collection.CollectionSupport;
-import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
-import net.shibboleth.shared.component.ComponentInitializationException;
-import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
 /**
  * Default function for fetching trusted trust mark owners from a trust chain: they are read from the trust anchor's
  * entity configuration.
  */
-public class DefaultTrustChainTrustedTrustMarkOwnersLookupStrategy extends AbstractIdentifiableInitializableComponent
+ at ThreadSafe
+public class DefaultTrustChainTrustedTrustMarkOwnersLookupStrategy
     implements Function<List<EntityStatement>, Map<String, Map<String, Object>>> {
 
     /** Class logger. */
     @Nonnull private final Logger log =
             LoggerFactory.getLogger(DefaultTrustChainTrustedTrustMarkOwnersLookupStrategy.class);
 
-    /** JSON object mapper used for decoding JSON into Map. */
-    @NonnullAfterInit private ObjectMapper objectMapper;
-
-    /**
-     * Set the JSON {@link ObjectMapper} used for decoding JSON into Map.
-     * 
-     * @param mapper object mapper
-     */
-    public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
-        checkSetterPreconditions();
-
-        objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
-    }
-
-    /** {@inheritDoc} */
-    protected void doInitialize() throws ComponentInitializationException {
-        super.doInitialize();
-        if (objectMapper == null) {
-            throw new ComponentInitializationException("Object mapper cannot be null");
-        }
-    }
-
     /** {@inheritDoc} */
     @Nullable @Override
     public Map<String, Map<String, Object>> apply(@Nullable final List<EntityStatement> trustChain) {
-        checkComponentActive();
         if (trustChain == null || trustChain.size() < 3) {
             return null;
         }
-        final Object ownersClaim =
-                trustChain.get(trustChain.size() - 1).getClaimsSet().getClaim("trust_mark_owners");
-        log.debug("Raw trust_mark_owners claim {}", ownersClaim);
+        final Map<String, Map<String, Object>> ownersClaim =
+                trustChain.get(trustChain.size() - 1).getParsedPayload() instanceof EntityConfigurationPayload ecp ?
+                        ecp.getTrustMarkOwners() : null;
+        log.debug("Parsed trust_mark_owners claim {}", ownersClaim);
         if (ownersClaim != null) {
-            final JavaType objectType = objectMapper.constructType(Object.class);
-            final JavaType stringType = objectMapper.constructType(String.class);
-            final MapType objectMapType =
-                    objectMapper.getTypeFactory().constructMapType(Map.class, stringType, objectType);
-            final MapType rootMapType =
-                    objectMapper.getTypeFactory().constructMapType(Map.class, stringType, objectMapType);
-            try {
-                final Map<String, Map<String, Object>> result =
-                        objectMapper.readValue(ownersClaim.toString(), rootMapType);
-                if (result != null) {
-                    log.debug("Parsed trust_mark_owners map {}", result);
-                    return result;
-                }
-            } catch (final JsonProcessingException e) {
-                log.warn("Could not parse trust mark issuers from the trust chain", e);
-            }
+            return ownersClaim;
         }
         log.debug("Returning empty map");
         return CollectionSupport.emptyMap();
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustMarkResponseSignatureValidationFilterStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustMarkResponseSignatureValidationFilterStrategy.java
index 4cebae4..3520d8b 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustMarkResponseSignatureValidationFilterStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustMarkResponseSignatureValidationFilterStrategy.java
@@ -25,9 +25,10 @@ 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.idp.plugin.oidc.op.oidfed.messaging.impl.TrustMarkResponse;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.RemoteTrustChainsContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.SubjectEntityStatementCriterion;
 import net.shibboleth.oidc.metadata.cache.MetadataCache;
 import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
 import net.shibboleth.oidc.metadata.filter.MetadataFilterContext;
@@ -52,14 +53,14 @@ public class DefaultTrustMarkResponseSignatureValidationFilterStrategy
             LoggerFactory.getLogger(DefaultTrustMarkResponseSignatureValidationFilterStrategy.class);
 
     /** Cache used to fetch the issuer entity configuration from. */
-    @NonnullAfterInit private MetadataCache<List<List<EntityStatement>>> trustChainCache;
+    @NonnullAfterInit private MetadataCache<RemoteTrustChainsContainer> trustChainCache;
 
     /**
      * Set the cache used to fetch the trust chain for the trust mark issuer from.
      * 
      * @param cache cache used to fetch the trust chain for the trust mark issuer from
      */
-    public void setTrustChainCache(@Nonnull final MetadataCache<List<List<EntityStatement>>> cache) {
+    public void setTrustChainCache(@Nonnull final MetadataCache<RemoteTrustChainsContainer> cache) {
         checkSetterPreconditions();
         trustChainCache = Constraint.isNotNull(cache, "Trust Chain cache cannot be null");
     }
@@ -115,9 +116,9 @@ public class DefaultTrustMarkResponseSignatureValidationFilterStrategy
     @Nullable protected EntityStatement fetchIssuerStatement(@Nonnull final String issuer) {
         final CriteriaSet criteria = new CriteriaSet(new SubjectEntityIDCriterion(issuer));
         try {
-            final List<List<List<EntityStatement>>> result = trustChainCache.get(criteria);
-            if (!result.isEmpty() && !result.get(0).isEmpty() && !result.get(0).get(0).isEmpty()) {
-                return result.get(0).get(0).get(0);
+            final List<RemoteTrustChainsContainer> result = trustChainCache.get(criteria);
+            if (!result.isEmpty() && !result.get(0).getTrustChains().isEmpty()) {
+                return result.get(0).getTrustChains().get(0).get(0);
             }
         } catch (final MetadataCacheException e) {
             log.debug("Error while fetching issuer trust chain for {}", issuer, e);
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustMarkStatusResponseSignatureValidationFilterStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustMarkStatusResponseSignatureValidationFilterStrategy.java
index bb63f0d..6dddce0 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustMarkStatusResponseSignatureValidationFilterStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustMarkStatusResponseSignatureValidationFilterStrategy.java
@@ -25,9 +25,10 @@ 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.idp.plugin.oidc.op.oidfed.messaging.impl.TrustMarkStatusResponse;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.RemoteTrustChainsContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.SubjectEntityStatementCriterion;
 import net.shibboleth.oidc.metadata.cache.MetadataCache;
 import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
 import net.shibboleth.oidc.metadata.filter.MetadataFilterContext;
@@ -52,14 +53,14 @@ public class DefaultTrustMarkStatusResponseSignatureValidationFilterStrategy
             LoggerFactory.getLogger(DefaultTrustMarkStatusResponseSignatureValidationFilterStrategy.class);
 
     /** Cache used to fetch the issuer entity configuration from. */
-    @NonnullAfterInit private MetadataCache<List<EntityStatement>> trustChainCache;
+    @NonnullAfterInit private MetadataCache<RemoteTrustChainsContainer> trustChainCache;
 
     /**
      * Set the cache used to fetch the trust chain for the trust mark issuer from.
      * 
      * @param cache cache used to fetch the trust chain for the trust mark issuer from
      */
-    public void setTrustChainCache(@Nonnull final MetadataCache<List<EntityStatement>> cache) {
+    public void setTrustChainCache(@Nonnull final MetadataCache<RemoteTrustChainsContainer> cache) {
         checkSetterPreconditions();
         trustChainCache = Constraint.isNotNull(cache, "Trust Chain cache cannot be null");
     }
@@ -115,9 +116,10 @@ public class DefaultTrustMarkStatusResponseSignatureValidationFilterStrategy
     @Nullable protected EntityStatement fetchIssuerStatement(@Nonnull final String issuer) {
         final CriteriaSet criteria = new CriteriaSet(new SubjectEntityIDCriterion(issuer));
         try {
-            final List<List<EntityStatement>> result = trustChainCache.get(criteria);
-            if (!result.isEmpty() && !result.get(0).isEmpty()) {
-                return result.get(0).get(0);
+            final List<RemoteTrustChainsContainer> result = trustChainCache.get(criteria);
+            if (!result.isEmpty()) {
+                final List<List<EntityStatement>> chains = result.get(0).getTrustChains();
+                return chains.isEmpty() ? null : chains.get(0).get(0);
             }
         } catch (final MetadataCacheException e) {
             log.debug("Error while fetching issuer trust chain for {}", issuer, e);
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/EntityStatementHelper.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/EntityStatementHelper.java
index 6b3444f..46d2aa4 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/EntityStatementHelper.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/EntityStatementHelper.java
@@ -15,6 +15,7 @@
 package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
 
 import java.net.URI;
+import java.time.Instant;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -28,9 +29,8 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.JavaType;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.type.MapType;
-import com.nimbusds.oauth2.sdk.ParseException;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.RemoteEntityConfigurationContainer;
 import net.shibboleth.oidc.metadata.cache.MetadataCache;
 import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
 import net.shibboleth.shared.collection.CollectionSupport;
@@ -55,7 +55,13 @@ public class EntityStatementHelper {
      */
     @Nonnull public static Map<String, Object> parseClaimAsMap(@Nonnull final ObjectMapper objectMapper,
             @Nonnull final EntityStatement entityStatement, @Nonnull final String claim ) {
-        final Object rawClaim = entityStatement.getClaimsSet().getClaim(claim);
+        final Object rawClaim;
+        try {
+            rawClaim = entityStatement.getJwt().getJWTClaimsSet().getClaim(claim);
+        } catch (final java.text.ParseException e) {
+            log.warn("Could not parse claim {}", claim, e);
+            return CollectionSupport.emptyMap();
+        }
         log.trace("Raw {} claim value: {}", claim, rawClaim);
         if (rawClaim != null) {
             final JavaType objectType = objectMapper.constructType(Object.class);
@@ -88,7 +94,13 @@ public class EntityStatementHelper {
     @Nonnull public static Map<String, Map<String, Object>> parseClaimAsMapOfMaps(
             @Nonnull final ObjectMapper objectMapper, @Nonnull final EntityStatement entityStatement,
             @Nonnull final String claim ) {
-        final Object rawClaim = entityStatement.getClaimsSet().getClaim(claim);
+        final Object rawClaim;
+        try {
+            rawClaim = entityStatement.getJwt().getJWTClaimsSet().getClaim(claim);
+        } catch (final java.text.ParseException e) {
+            log.warn("Could not parse claim {}", claim, e);
+            return CollectionSupport.emptyMap();
+        }
         log.trace("Raw {} claim value: {}", claim, rawClaim);
         if (rawClaim != null) {
             final JavaType objectType = objectMapper.constructType(Object.class);
@@ -124,24 +136,6 @@ public class EntityStatementHelper {
         return parseClaimAsMapOfMaps(objectMapper, entityStatement, "metadata");
     }
 
-    /**
-     * Deseriealize the given string into an entity statement.
-     * 
-     * @param serialized input string
-     * @return entity statement
-     */
-    @Nullable public static EntityStatement deserializeEntityStatement(@Nullable final String serialized) {
-        try {
-            if (serialized != null) {
-                return EntityStatement.parse(serialized);
-            }
-        } catch (final ParseException e) {
-            log.trace("Could not construct entity statement from {}", serialized, e);
-        }
-        log.warn("Could not deserialize entity statement {}", serialized);
-        return null;
-    }
-
     /**
      * Serialize the given trust chain into a comma-separated list of strings.
      * 
@@ -151,7 +145,7 @@ public class EntityStatementHelper {
     @Nullable
     public static String getSerializedTrustChain(@Nullable final List<EntityStatement> trustChain) {
         return trustChain == null ? null :
-            String.join(",", trustChain.stream().map(es -> es.getSignedStatement().serialize()).toList());
+            String.join(",", trustChain.stream().map(es -> es.getJwt().serialize()).toList());
     }
 
     /**
@@ -162,7 +156,7 @@ public class EntityStatementHelper {
      */
     @Nullable
     public static String getSerializedEntityStatement(@Nullable final EntityStatement entityStatement) {
-        return entityStatement == null ? null : entityStatement.getSignedStatement().serialize();
+        return entityStatement == null ? null : entityStatement.getJwt().serialize();
     }
 
     /**
@@ -172,19 +166,21 @@ public class EntityStatementHelper {
      * @param cache metadata cache used for fetching the entity configuration
      * @param entityId entity ID of the entity
      * @param endpointId the endpoint ID
-     * @param objectMapper object mapper used for parsing
+     * @param expiration expiration passed to the entity configuration cache
      * @return endpoint URI
      */
-    @Nullable public static URI fetchEndpointUriFromFederationEntity(@Nonnull MetadataCache<EntityStatement> cache,
+    @Nullable public static URI fetchEndpointUriFromFederationEntity(
+            @Nonnull MetadataCache<RemoteEntityConfigurationContainer> cache,
             @Nonnull final String entityId, @Nonnull final String endpointId,
-            @Nonnull final ObjectMapper objectMapper) {
-        final CriteriaSet criteria = new CriteriaSet(new SubjectEntityIDCriterion(entityId));
+            @Nonnull final Instant expiration) {
+        final CriteriaSet criteria = new CriteriaSet(new SubjectEntityIDCriterion(entityId),
+                new ResponseContainerExpirationCriterion(expiration));
         try {
-            final List<EntityStatement> result = cache.get(criteria);
+            final List<RemoteEntityConfigurationContainer> result = cache.get(criteria);
             if (!result.isEmpty()) {
-                return Optional.ofNullable(parseMetadata(objectMapper, result.get(0)).get("federation_entity"))
-                        .filter(Map.class::isInstance)
-                        .map(map -> (Map<String,Object>) map)
+                return Optional.ofNullable(result.get(0).getEntityStatement())
+                        .map(statement -> statement.getParsedPayload().getMetadata())
+                        .map(metadata -> metadata.get("federation_entity"))
                         .map(map -> map.get(endpointId))
                         .filter(String.class::isInstance)
                         .map(String.class::cast)
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/IssuerEntityIDCriterion.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/IssuerEntityIDCriterion.java
index a47d7eb..b00b56f 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/IssuerEntityIDCriterion.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/IssuerEntityIDCriterion.java
@@ -18,6 +18,7 @@ import java.util.Objects;
 
 import javax.annotation.Nonnull;
 
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.resolver.Criterion;
 
@@ -27,14 +28,14 @@ import net.shibboleth.shared.resolver.Criterion;
 public class IssuerEntityIDCriterion implements Criterion {
 
     /** The entity ID value. */
-    @Nonnull final String value;
+    @Nonnull @NotEmpty final String value;
 
     /**
      * Constructor.
      *
      * @param entityId the entity ID value, must not be null
      */
-    public IssuerEntityIDCriterion(@Nonnull final String entityId) {
+    public IssuerEntityIDCriterion(@Nonnull @NotEmpty final String entityId) {
         value = Constraint.isNotEmpty(entityId, "Entity ID cannot be null");
     }
 
@@ -43,7 +44,7 @@ public class IssuerEntityIDCriterion implements Criterion {
      * 
      * @return the entity ID value
      */
-    public String getValue() {
+    @Nonnull @NotEmpty public String getValue() {
         return value;
     }
 
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/LocalKeyContainer.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/LocalKeyContainer.java
index a070c9e..858cf39 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/LocalKeyContainer.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/LocalKeyContainer.java
@@ -39,7 +39,7 @@ public class LocalKeyContainer {
      * 
      * @return the key set
      */
-    @Nullable JWKSet getJWKSet() {
+    @Nullable public JWKSet getJWKSet() {
         return jwkSet;
     }
 
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/SubjectEntityIDCriterion.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/SubjectEntityIDCriterion.java
index 7352e0e..4ca9c45 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/SubjectEntityIDCriterion.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/SubjectEntityIDCriterion.java
@@ -35,7 +35,7 @@ public class SubjectEntityIDCriterion implements Criterion {
      *
      * @param entityId the entity ID value, must not be null
      */
-    public SubjectEntityIDCriterion(@Nonnull final String entityId) {
+    public SubjectEntityIDCriterion(@Nonnull @NotEmpty final String entityId) {
         value = Constraint.isNotEmpty(entityId, "Entity ID cannot be null");
     }
 
@@ -44,7 +44,7 @@ public class SubjectEntityIDCriterion implements Criterion {
      * 
      * @return the entity ID value
      */
-    public String getValue() {
+    @Nonnull @NotEmpty public String getValue() {
         return value;
     }
 
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/AbstractFederationEndpointResponseFetchingStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/AbstractFederationEndpointResponseFetchingStrategy.java
new file mode 100644
index 0000000..4438bbe
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/AbstractFederationEndpointResponseFetchingStrategy.java
@@ -0,0 +1,252 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.ProtocolException;
+import org.opensaml.security.httpclient.HttpClientSecurityParameters;
+import org.opensaml.security.httpclient.HttpClientSecuritySupport;
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+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;
+
+/**
+ * An abstract base class for fetching strategies related to OpenID Federation endpoints.
+ *
+ * @param <R> data needed for request
+ * @param <C> metadata container class stored to the metadata cache
+ */
+public abstract class
+    AbstractFederationEndpointResponseFetchingStrategy<R extends Object, C extends BaseExpirableMetadataContainer>
+    extends AbstractIdentifiableInitializableComponent implements Function<CriteriaSet, C> {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(AbstractFederationEndpointResponseFetchingStrategy.class);
+
+    /** HTTP client to use. */
+    @NonnullAfterInit protected HttpClient httpClient;
+
+    /** HTTP client security parameters. */
+    @Nullable private HttpClientSecurityParameters httpClientSecurityParameters;
+
+    /** JSON object mapper used for decoding JSON into Map. */
+    @NonnullAfterInit protected ObjectMapper objectMapper;
+
+    /** Strategy to fetch request data from the criteria set. */
+    @NonnullAfterInit private Function<CriteriaSet, R> criteriaToRequestDataStrategy;
+
+    /** Strategy to fetch lifetime for container with valid contents. */
+    @NonnullAfterInit private Function<CriteriaSet, Duration> criteriaToValidContainerLifetimeStrategy;
+
+    /** Strategy to fetch lifetime for container with invalid contents. */
+    @NonnullAfterInit private Function<CriteriaSet, Duration> criteriaToInvalidContainerLifetimeStrategy;
+
+    /**
+     * Set the {@link HttpClient} to use.
+     * 
+     * @param client HTTP client to use
+     */
+    public void setHttpClient(@Nonnull final HttpClient client) {
+        checkSetterPreconditions();
+        httpClient = Constraint.isNotNull(client, "HttpClient cannot be null");
+    }
+
+    /**
+     * Set the optional client security parameters.
+     * 
+     * @param params the new client security parameters
+     */
+    public void setHttpClientSecurityParameters(@Nullable final HttpClientSecurityParameters params) {
+        checkSetterPreconditions();
+        httpClientSecurityParameters = params;
+    }
+
+    /**
+     * Set the strategy to fetch request data from the criteria set.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setCriteriaToRequestDataStrategy(@Nonnull final Function<CriteriaSet, R> strategy) {
+        checkSetterPreconditions();
+        criteriaToRequestDataStrategy =
+                Constraint.isNotNull(strategy, "Criteria to request data strategy cannot be null");
+    }
+
+    /**
+
+     * Set the JSON {@link ObjectMapper} used for decoding JSON into Map.
+     * 
+     * @param mapper object mapper
+     */
+    public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+        checkSetterPreconditions();
+
+        objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
+    }
+
+    /**
+     * Set the strategy to fetch lifetime for container with valid contents.
+     * 
+     * @param strategy lifetime strategy
+     */
+    public void setCriteriaToValidContainerLifetimeStrategy(@Nonnull final Function<CriteriaSet, Duration> strategy) {
+        checkSetterPreconditions();
+
+        criteriaToValidContainerLifetimeStrategy =
+                Constraint.isNotNull(strategy, "Criteria to valid container lifetime strategy cannot be null");
+    }
+
+    /**
+     * Set the strategy to fetch lifetime for container with invalid contents.
+     * 
+     * @param strategy lifetime strategy
+     */
+    public void setCriteriaToInvalidContainerLifetimeStrategy(
+            @Nonnull final Function<CriteriaSet, Duration> strategy) {
+        checkSetterPreconditions();
+
+        criteriaToInvalidContainerLifetimeStrategy =
+                Constraint.isNotNull(strategy, "Criteria to invalid container lifetime strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        if (httpClient == null) {
+            throw new ComponentInitializationException("Httpclient cannot be null");
+        }
+        if (objectMapper == null) {
+            throw new ComponentInitializationException("Object mapper cannot be null");
+        }
+        if (criteriaToRequestDataStrategy == null) {
+            throw new ComponentInitializationException("Criteria to request data strategy cannot be null");
+        }
+        if (criteriaToValidContainerLifetimeStrategy == null) {
+            throw new ComponentInitializationException("Criteria to valid container lifetime strategy cannot be null");
+        }
+        if (criteriaToInvalidContainerLifetimeStrategy == null) {
+            throw new ComponentInitializationException(
+                    "Criteria to invalid container lifetime strategy cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public C apply(@Nullable final CriteriaSet criteria) {
+        checkComponentActive();
+        if (criteria == null) {
+            log.error("No criteria set provided");
+            return null;
+        }
+        final R requestData = criteriaToRequestDataStrategy.apply(criteria);
+        if (requestData == null) {
+            log.debug("Could not resolve request data from criteria {}", criteria);
+            return null;
+        }
+        final Duration validLifetime = criteriaToValidContainerLifetimeStrategy.apply(criteria);
+        if (validLifetime == null) {
+            log.warn("Could not fetch expiration time for valid container");
+            return null;
+        }
+        final Instant validExpiration = Instant.now().plus(validLifetime);
+        final Duration invalidLifetime = criteriaToInvalidContainerLifetimeStrategy.apply(criteria);
+        if (invalidLifetime == null) {
+            log.warn("Could not fetch expiration time for invalid container");
+            return null;
+        }
+        final Instant invalidExpiration = Instant.now().plus(invalidLifetime);
+        final ClassicHttpRequest httpRequest = initializeHttpRequest(criteria, requestData);
+        if (httpRequest == null) {
+            log.debug("Could not initialize HTTP request for {}", requestData);
+            return null;
+        }
+        final HttpClientContext httpContext = buildHttpContext(httpRequest);
+        try (final ClassicHttpResponse response = httpClient.executeOpen(null, httpRequest, httpContext)) {
+            final String scheme = httpRequest.getUri().getScheme();
+            if (scheme == null) {
+                throw new URISyntaxException(httpRequest.getUri().toString(), "Could not parse scheme");
+            }
+            HttpClientSecuritySupport.checkTLSCredentialEvaluated(httpContext, scheme);
+            assert validExpiration != null; assert invalidExpiration != null;
+            return parseHttpResponse(criteria, requestData, response, validExpiration, invalidExpiration);
+        } catch (final ProtocolException | URISyntaxException | IOException e) {
+            log.debug("Unable to fetch resolve entity response via request data: {}", requestData, e);
+        }
+
+        return null;
+    }
+
+    /**
+     * Build the {@link HttpClientContext} instance to be used with {@link #httpClient}.
+     * 
+     * @param request the HTTP client request
+     * @return the client context instance
+     */
+    @Nonnull
+    protected HttpClientContext buildHttpContext(@Nonnull final ClassicHttpRequest request) {
+        final HttpClientContext clientContext = HttpClientContext.create();
+        assert clientContext != null;
+        HttpClientSecuritySupport.marshalSecurityParameters(clientContext, httpClientSecurityParameters, false);
+        HttpClientSecuritySupport.addDefaultTLSTrustEngineCriteria(clientContext, request);
+        return clientContext;
+    }
+
+    /**
+     * Initializes the {@link ClassicHttpRequest} to be used with {@link #httpClient}.
+     * 
+     * @param criteria criteria set
+     * @param requestData request data
+     * @return the request to be used with {@link #httpClient}
+     */
+    @Nullable protected abstract ClassicHttpRequest initializeHttpRequest(@Nonnull final CriteriaSet criteria,
+            @Nonnull final R requestData);
+
+    /**
+     * Parses the response message container to be stored in the metadata cache.
+     * 
+     * @param criteria criteria set
+     * @param requestData the request data
+     * @param response the HTTP response obtained from {@link #httpClient}
+     * @param validExpiration expiration instant for containers with valid content
+     * @param invalidExpiration expiration instant for container with invalid content
+     * @return the response message container
+     * @throws ProtocolException if HTTP protocol violation occurs
+     * @throws IOException if generic I/O exception occurs
+     */
+    @Nullable protected abstract C parseHttpResponse(@Nonnull final CriteriaSet criteria,
+            @Nonnull final R requestData, @Nullable final ClassicHttpResponse response,
+            @Nonnull final Instant validExpiration, @Nonnull final Instant invalidExpiration)
+                    throws ProtocolException, IOException;
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/AbstractTrustEngineSignatureValidationComponent.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/AbstractTrustEngineSignatureValidationComponent.java
new file mode 100644
index 0000000..0d68cbc
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/AbstractTrustEngineSignatureValidationComponent.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache;
+
+import java.text.ParseException;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.SecurityException;
+import org.opensaml.security.trust.TrustEngine;
+import org.slf4j.Logger;
+
+import com.nimbusds.jwt.SignedJWT;
+
+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;
+
+/**
+ * Abstract component performing signature validation via {@link TrustEngine}.
+ */
+public class AbstractTrustEngineSignatureValidationComponent extends AbstractIdentifiableInitializableComponent {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(AbstractTrustEngineSignatureValidationComponent.class);
+
+    /** Trust engine used to validate a signature. */
+    @NonnullAfterInit private TrustEngine<SignedJWT> trustEngine;
+
+    /** Set of externally specified default criteria for input to the trust engine. */
+    @Nullable private CriteriaSet defaultCriteria;
+
+    /**
+     * Set trust engine used to validate a signature.
+     * 
+     * @param engine trust engine
+     */
+    public void setTrustEngine(@Nonnull final TrustEngine<SignedJWT> engine) {
+        checkSetterPreconditions();
+        trustEngine = Constraint.isNotNull(engine, "Trust Engine cannot be null");
+    }
+
+    /**
+     * Set the optional set of default criteria used as input to the trust engine.
+     * 
+     * @param criteria criteria set to use
+     */
+    public void setDefaultCriteria(@Nullable final CriteriaSet criteria) {
+        checkSetterPreconditions();
+        defaultCriteria = criteria;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        if (trustEngine == null) {
+            throw new ComponentInitializationException("Trust Engine cannot be null");
+        }
+    }
+
+    /**
+     * Validates the given JWT via trust engine and the given criteria.
+     * 
+     * @param jwt the JWT to be validated
+     * @param criteria the criteria (expanded with the optional default criteria)
+     * @param entityId the entity ID used for logging
+     * @return true if validation was successful, false otherwise
+     */
+    protected boolean validateJwt(@Nonnull final SignedJWT jwt, @Nonnull final CriteriaSet criteria,
+            @Nullable final String entityId) {
+        if (defaultCriteria != null && !defaultCriteria.isEmpty()) {
+            criteria.addAll(defaultCriteria);
+        }
+        try {
+            if (trustEngine.validate(jwt, criteria)) {
+                log.debug("Successfully validated entity statement for {}", entityId);
+                return true;
+            }
+            log.warn("Trust Engine validation failed for {}, issued by {}", entityId,
+                    jwt.getJWTClaimsSet().getIssuer());
+        } catch (final SecurityException | ParseException e) {
+            log.warn("Could not validate entity statement for {}", entityId, e);
+        }
+        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/cache/BaseExpirableEntityStatementContainer.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/BaseExpirableEntityStatementContainer.java
new file mode 100644
index 0000000..025fb94
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/BaseExpirableEntityStatementContainer.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache;
+
+import java.io.Serializable;
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A base container class for metadata caches carrying expiration instant.
+ */
+public abstract class BaseExpirableEntityStatementContainer extends BaseExpirableMetadataContainer
+    implements Serializable {
+
+    /** Serial version UID. */
+    private static final long serialVersionUID = 656269369356432370L;
+
+    /** Entity statement for this container. */
+    @Nullable private final EntityStatement statement;
+
+    /** Flag to indicate if {@link #statement} is valid. */
+    private boolean validStatement;
+
+    /** Expiration instant for this container when invalid statement is contained. */
+    @Nonnull private final Instant invalidStatementExpiration;
+
+    /**
+     * Constructor.
+     *
+     * @param validExpirationInstant expiration instant for valid statement, must not be null
+     * @param entityStatement entity statement
+     * @param invalidExpirationInstant expiration instant for invalid statement, must not be null
+     */
+    public BaseExpirableEntityStatementContainer(@Nonnull final Instant validExpirationInstant,
+            @Nullable final EntityStatement entityStatement, @Nonnull final Instant invalidExpirationInstant) {
+        super(validExpirationInstant);
+        statement = entityStatement;
+        validStatement = entityStatement != null;
+        invalidStatementExpiration = Constraint.isNotNull(invalidExpirationInstant,
+                "Expiration instant for invalid statements cannot be null");
+    }
+
+    /**
+     * Get the entity statement. If the statement has been invalidated, a null is returned.
+     * 
+     * @return entity statement
+     */
+    @Nullable public EntityStatement getEntityStatement() {
+        return getEntityStatement(false);
+    }
+
+    /**
+     * Get the entity statement.
+     * 
+     * @param includeInvalidStatement a flag to return invalidated entity statement
+     * @return entity statement
+     */
+    @Nullable public EntityStatement getEntityStatement(final boolean includeInvalidStatement) {
+        return includeInvalidStatement ? statement : validStatement ? statement : null;
+    }
+
+    /** {@inheritDoc} */
+    @Nonnull public Instant getExpirationInstant() {
+        return getEntityStatement() != null ? super.getExpirationInstant() : invalidStatementExpiration;
+    }
+
+    /**
+     * Invalidates the entity statement.
+     * 
+     * @return this container
+     */
+    public BaseExpirableEntityStatementContainer invalidateStatement() {
+        validStatement = false;
+        return this;
+    }
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/BaseExpirableMetadataContainer.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/BaseExpirableMetadataContainer.java
new file mode 100644
index 0000000..cf156a2
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/BaseExpirableMetadataContainer.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache;
+
+import java.io.Serializable;
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A base container class for metadata caches carrying expiration instant.
+ */
+public abstract class BaseExpirableMetadataContainer implements Serializable {
+
+    /** Serial version UID. */
+    private static final long serialVersionUID = 656269369356865370L;
+
+    /** Expiration instant for this container. */
+    @Nonnull private final Instant expiration;
+
+    /**
+     * Constructor.
+     *
+     * @param expirationInstant expiration instant, must not be null
+     */
+    public BaseExpirableMetadataContainer(@Nonnull final Instant expirationInstant) {
+        expiration = Constraint.isNotNull(expirationInstant, "Expiration instant cannot be null");
+    }
+
+    /**
+     * Get expiration instant.
+     * 
+     * @return expiration instant
+     */
+    @Nonnull public Instant getExpirationInstant() {
+        return expiration;
+    }
+
+    /**
+     * Get the entity ID of the requested entity.
+     * 
+     * @return requested entity ID
+     */
+    @Nonnull @NotEmpty public abstract String getEntityId();
+
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/DefaultEntityStatementMetadataFilterStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/DefaultEntityStatementMetadataFilterStrategy.java
new file mode 100644
index 0000000..c7c5ac3
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/DefaultEntityStatementMetadataFilterStrategy.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache;
+
+import java.util.List;
+import java.util.function.BiFunction;
+import java.util.function.BiPredicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+
+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;
+
+/**
+ * Default metadata validation filter for entity statement containers.
+ */
+public class DefaultEntityStatementMetadataFilterStrategy
+    extends AbstractIdentifiableInitializableComponent implements
+    BiFunction<BaseExpirableEntityStatementContainer, MetadataFilterContext, BaseExpirableEntityStatementContainer> {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(DefaultEntityStatementMetadataFilterStrategy.class);
+
+    /** List of validation conditions. */
+    @NonnullAfterInit
+    private List<BiPredicate<BaseExpirableEntityStatementContainer, MetadataFilterContext>> validationConditions;
+
+    /**
+     * Set the list of validation conditions.
+     * 
+     * @param predicates validation conditions
+     */
+    public void setValidationConditions(
+            @Nonnull final List<BiPredicate<BaseExpirableEntityStatementContainer, MetadataFilterContext>> predicates) {
+        checkSetterPreconditions();
+        validationConditions = Constraint.isNotNull(predicates, "List of validation conditions cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        if (validationConditions == null) {
+            throw new ComponentInitializationException("List of validation conditions cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public BaseExpirableEntityStatementContainer apply(@Nullable final BaseExpirableEntityStatementContainer container,
+            @Nullable final MetadataFilterContext filterContext) {
+        checkComponentActive();
+        if (container == null) {
+            return null;
+        }
+        for (final BiPredicate<BaseExpirableEntityStatementContainer, MetadataFilterContext> condition :
+            validationConditions) {
+            if (!condition.test(container, filterContext)) {
+                log.debug("Condition {} failed, invalidating container for {}", condition, container.getEntityId());
+                return container.invalidateStatement();
+            }
+        }
+        log.debug("Metadata container successfully validated by all predicates for {}", container.getEntityId());
+        return container;
+    }
+
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/DefaultEntityStatementSignatureValidationCondition.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/DefaultEntityStatementSignatureValidationCondition.java
new file mode 100644
index 0000000..a995083
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/DefaultEntityStatementSignatureValidationCondition.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache;
+
+import java.util.function.BiPredicate;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.trust.TrustEngine;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.AbstractTrustEngineSignatureValidationComponent;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.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.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Default signature validating filter for entity statement. The signature validation is performed via configurable
+ * {@link TrustEngine}. The {@link CriteriaSet} passed to trust engine is fetched via configurable strategy.
+ */
+ at ThreadSafeAfterInit
+public class DefaultEntityStatementSignatureValidationCondition
+    extends AbstractTrustEngineSignatureValidationComponent
+    implements BiPredicate<BaseExpirableEntityStatementContainer, MetadataFilterContext> {
+
+    /** Class logger. */
+    @Nonnull private Logger log =
+            LoggerFactory.getLogger(DefaultEntityStatementSignatureValidationCondition.class);
+
+    /** Lookup strategy for criteria set passed to the trust engine. */
+    @NonnullAfterInit private Function<BaseExpirableEntityStatementContainer, CriteriaSet> criteriaSetLookupStrategy;
+
+    /**
+     * Set the lookup strategy for criteria set passed to the trust engine.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setCriteriaSetLookupStrategy(
+            @Nonnull final Function<BaseExpirableEntityStatementContainer, CriteriaSet> strategy) {
+        checkSetterPreconditions();
+        criteriaSetLookupStrategy = Constraint.isNotNull(strategy, "CriteriaSet lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        if (criteriaSetLookupStrategy == null) {
+            throw new ComponentInitializationException("CriteriaSet lookup strategy cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean test(@Nullable final BaseExpirableEntityStatementContainer responseContainer,
+            @Nullable final MetadataFilterContext filterContext) {
+        checkComponentActive();
+        if (responseContainer == null) {
+            return false;
+        }
+
+        final EntityStatement entityConfiguration = responseContainer.getEntityStatement();
+        final String entityId = responseContainer.getEntityId();
+        if (entityConfiguration != null ) {
+            log.trace("Starting signature validation of entity configuration for {}", entityId);
+            final CriteriaSet criteria = criteriaSetLookupStrategy.apply(responseContainer);
+            if (criteria == null) {
+                log.error("Could not resolve criteria set for {}", entityId);
+                return false;
+            }
+            if (!validateJwt(entityConfiguration.getJwt(), criteria, entityId)) {
+                return false;
+            }
+        } else {
+            log.debug("Ignoring signature check for the error response for {}", entityId);
+        }
+        return true;
+    }
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementExpirationTimeStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/DefaultResponseContainerExpirationTimeStrategy.java
similarity index 55%
copy from idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementExpirationTimeStrategy.java
copy to idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/DefaultResponseContainerExpirationTimeStrategy.java
index 7960cfd..8451dcf 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityStatementExpirationTimeStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/DefaultResponseContainerExpirationTimeStrategy.java
@@ -12,7 +12,7 @@
  * limitations under the License.
  */
 
-package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache;
 
 import java.time.Instant;
 import java.util.function.Function;
@@ -20,29 +20,28 @@ import java.util.function.Function;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.ThreadSafe;
 
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-
 import net.shibboleth.oidc.metadata.cache.ExpirationTimeContext;
 
 /**
- * Default strategy for fetching expiration time for the entity statement.
+ * Default strategy for fetching expiration time for the response containers extending {@link
+ * BaseExpirableMetadataContainer}. The expiration instant is fetched from which is before: the instant returned by
+ * {@link BaseExpirableMetadataContainer#getExpirationInstant()} or {@link ExpirationTimeContext#getMaxCacheDuration()}.
  */
 @ThreadSafe
-public class DefaultEntityStatementExpirationTimeStrategy
-        implements Function<ExpirationTimeContext<EntityStatement>, Instant> {
+public class DefaultResponseContainerExpirationTimeStrategy
+        implements Function<ExpirationTimeContext<BaseExpirableMetadataContainer>, Instant> {
 
     /** {@inheritDoc} */
-    @Nullable public Instant apply(@Nullable final ExpirationTimeContext<EntityStatement> context) {
+    @Nullable public Instant apply(@Nullable final ExpirationTimeContext<BaseExpirableMetadataContainer> context) {
         if (context == null) {
             return null;
         }
         final Instant contextExpiration = context.getNow().plus(context.getMaxCacheDuration());
-        final EntityStatement statement = context.getMetadata();
-        if (statement == null) {
+        final BaseExpirableMetadataContainer container = context.getMetadata();
+        if (container == null) {
             return contextExpiration;
         }
-        final Instant metadataExpiration = statement.getClaimsSet().getExpirationTime().toInstant();
-        return metadataExpiration.isBefore(contextExpiration) ? metadataExpiration : contextExpiration;
+        final Instant containerExpiration = container.getExpirationInstant();
+        return containerExpiration.isBefore(contextExpiration) ? containerExpiration : contextExpiration;
     }
-
 }
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/IssuerEntityStatementCriterion.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/IssuerEntityStatementCriterion.java
similarity index 75%
rename from idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/IssuerEntityStatementCriterion.java
rename to idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/IssuerEntityStatementCriterion.java
index 1e3a88e..b46046b 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/IssuerEntityStatementCriterion.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/IssuerEntityStatementCriterion.java
@@ -12,19 +12,18 @@
  * limitations under the License.
  */
 
-package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache;
 
 import java.util.Objects;
 
 import javax.annotation.Nonnull;
 
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.resolver.Criterion;
 
 /**
- * A {@link Criterion} representing an issuer entity statement.
+ * A {@link Criterion} representing a subject entity statement.
  */
 public class IssuerEntityStatementCriterion implements Criterion {
 
@@ -34,10 +33,10 @@ public class IssuerEntityStatementCriterion implements Criterion {
     /**
      * Constructor.
      *
-     * @param entityStatement the entity statement value, must not be null
+     * @param statement the entity statement value, must not be null
      */
-    public IssuerEntityStatementCriterion(@Nonnull final EntityStatement entityStatement) {
-        value = Constraint.isNotNull(entityStatement, "Entity statement cannot be null");
+    public IssuerEntityStatementCriterion(@Nonnull final EntityStatement statement) {
+        value = Constraint.isNotNull(statement, "Entity statement cannot be null");
     }
 
     /**
@@ -45,14 +44,14 @@ public class IssuerEntityStatementCriterion implements Criterion {
      * 
      * @return the entity statement value
      */
-    public EntityStatement getValue() {
+    @Nonnull public EntityStatement getValue() {
         return value;
     }
 
     /** {@inheritDoc} */
     @Override
     public String toString() {
-        return "IssuerEntityStatementCriterion [value=" + value.getSignedStatement().serialize() + "]";
+        return "IssuerEntityStatementCriterion [value=" + value.getJwt().serialize() + "]";
     }
 
     /** {@inheritDoc} */
@@ -74,6 +73,6 @@ public class IssuerEntityStatementCriterion implements Criterion {
             return false;
         }
         final IssuerEntityStatementCriterion other = (IssuerEntityStatementCriterion) obj;
-        return value.getSignedStatement().serialize().equals(other.value.getSignedStatement().serialize());
+        return value.getJwt().serialize().equals(other.value.getJwt().serialize());
     }
 }
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/RemoteEntityConfigurationContainer.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/RemoteEntityConfigurationContainer.java
new file mode 100644
index 0000000..9ed1b2a
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/RemoteEntityConfigurationContainer.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache;
+
+import java.io.Serializable;
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityConfiguration;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A container class for metadata caches carrying entity ID and statement details related to Entity Configuration.
+ */
+public class RemoteEntityConfigurationContainer extends BaseExpirableEntityStatementContainer implements Serializable {
+
+    /** Serial version UID. */
+    private static final long serialVersionUID = 756269369356865370L;
+
+    /** Requested entity ID. */
+    @Nonnull @NotEmpty private final String entityId;
+
+    /**
+     * Constructor.
+     *
+     * @param entity entity ID, must not be null
+     * @param configuration entity configuration parsed from the response
+     * @param validExpirationInstant expiration instant for valid statement, must not be null
+     * @param invalidExpirationInstant expiration instant for invalid statement, must not be null
+     */
+    public RemoteEntityConfigurationContainer(@Nonnull @NotEmpty final String entity,
+            @Nullable final EntityConfiguration configuration, @Nonnull final Instant validExpirationInstant,
+            @Nonnull final Instant invalidExpirationInstant) {
+        super(validExpirationInstant, configuration, invalidExpirationInstant);
+        entityId = Constraint.isNotNull(entity, "Entity ID cannot be empty");
+    }
+
+    /** {@inheritDoc} */
+    @Nonnull @NotEmpty public String getEntityId() {
+        return entityId;
+    }
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/RemoteSubordinateStatementContainer.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/RemoteSubordinateStatementContainer.java
new file mode 100644
index 0000000..9689b77
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/RemoteSubordinateStatementContainer.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache;
+
+import java.io.Serializable;
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubordinateStatement;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A container class for metadata caches carrying ID and statement details related to Subordinate Statement.
+ */
+public class RemoteSubordinateStatementContainer extends BaseExpirableEntityStatementContainer implements Serializable {
+
+    /** Serial version UID. */
+    private static final long serialVersionUID = 756269369356865370L;
+
+    /** Requested entity ID. */
+    @Nonnull private final SubordinateStatementCacheIdentifier identifier;
+
+    /**
+     * Constructor.
+     *
+     * @param id identifier, must not be null
+     * @param statement subordinate statement parsed from the response
+     * @param validExpirationInstant expiration instant for valid statement, must not be null
+     * @param invalidExpirationInstant expiration instant for invalid statement, must not be null
+     */
+    public RemoteSubordinateStatementContainer(@Nonnull final SubordinateStatementCacheIdentifier id,
+            @Nullable final SubordinateStatement statement, @Nonnull final Instant validExpirationInstant,
+            @Nonnull final Instant invalidExpirationInstant) {
+        super(validExpirationInstant, statement, invalidExpirationInstant);
+        identifier = Constraint.isNotNull(id, "Identifier cannot be null");
+    }
+
+    /**
+     * Get entity ID.
+     * 
+     * @return entity ID
+     */
+    @Nonnull @NotEmpty public SubordinateStatementCacheIdentifier getIdentifier() {
+        return identifier;
+    }
+
+    /** {@inheritDoc} */
+    @Nonnull @NotEmpty public String getEntityId() {
+        return identifier.getSubject();
+    }
+
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/RemoteTrustChainsContainer.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/RemoteTrustChainsContainer.java
new file mode 100644
index 0000000..3bac744
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/RemoteTrustChainsContainer.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache;
+
+import java.io.Serializable;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
+import net.shibboleth.shared.annotation.constraint.Live;
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A container class for metadata caches carrying ID and trust chains details related to Trust Chains of the entity.
+ */
+public class RemoteTrustChainsContainer extends BaseExpirableMetadataContainer implements Serializable {
+
+    /** Serial version UID. */
+    private static final long serialVersionUID = 656434369356432370L;
+
+    /** Requested entity ID. */
+    @Nonnull @NotEmpty private final String entityId;
+
+    /** Trust chains for this container. */
+    @Nonnull @NonnullElements @Live private final List<List<EntityStatement>> trustChains;
+
+    /**
+     * Constructor.
+     *
+     * @param expirationInstant expiration instant, must not be null
+     * @param entity entity ID, must not be null
+     */
+    public RemoteTrustChainsContainer(@Nonnull final Instant expirationInstant,
+            @Nonnull @NotEmpty final String entity) {
+        this(expirationInstant, entity, CollectionSupport.emptyList());
+    }
+
+    /**
+     * 
+     * Constructor.
+     *
+     * @param expirationInstant expiration instant, must not be null
+     * @param entity entity ID, must not be null
+     * @param chains initial trust chains value
+     */
+    public RemoteTrustChainsContainer(@Nonnull final Instant expirationInstant, @Nonnull @NotEmpty final String entity,
+            @Nonnull @NonnullElements final List<List<EntityStatement>> chains) {
+        super(expirationInstant);
+        entityId = Constraint.isNotNull(entity, "Entity ID cannot be empty");
+        trustChains = new ArrayList<>(Constraint.isNotNull(chains, "Trust chains cannot be null"));
+    }
+
+    /** {@inheritDoc} */
+    @Override @Nonnull @NotEmpty
+    public String getEntityId() {
+        return entityId;
+    }
+
+    /**
+     * Get the list of trust chains.
+     * 
+     * @return list of trust chains
+     */
+    @Nonnull @NonnullElements public List<List<EntityStatement>> getTrustChains() {
+        return CollectionSupport.copyToList(trustChains);
+    }
+
+    /**
+     * Remove the given trust chain from the list of trust chains.
+     * 
+     * @param trustChain trust chain to be removed
+     * @return this object
+     */
+    @Nonnull public RemoteTrustChainsContainer removeTrustChain(
+            @Nonnull @NonnullElements final List<EntityStatement> trustChain) {
+        trustChains.remove(trustChain);
+        return this;
+    }
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/SubjectEntityStatementCriterion.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/SubjectEntityStatementCriterion.java
similarity index 79%
rename from idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/SubjectEntityStatementCriterion.java
rename to idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/SubjectEntityStatementCriterion.java
index 32228c3..70fd128 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/SubjectEntityStatementCriterion.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/SubjectEntityStatementCriterion.java
@@ -12,14 +12,13 @@
  * limitations under the License.
  */
 
-package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache;
 
 import java.util.Objects;
 
 import javax.annotation.Nonnull;
 
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.resolver.Criterion;
 
@@ -34,10 +33,10 @@ public class SubjectEntityStatementCriterion implements Criterion {
     /**
      * Constructor.
      *
-     * @param entityStatement the entity statement value, must not be null
+     * @param statement the entity statement value, must not be null
      */
-    public SubjectEntityStatementCriterion(@Nonnull final EntityStatement entityStatement) {
-        value = Constraint.isNotNull(entityStatement, "Entity statement cannot be null");
+    public SubjectEntityStatementCriterion(@Nonnull final EntityStatement statement) {
+        value = Constraint.isNotNull(statement, "Entity statement cannot be null");
     }
 
     /**
@@ -52,7 +51,7 @@ public class SubjectEntityStatementCriterion implements Criterion {
     /** {@inheritDoc} */
     @Override
     public String toString() {
-        return "SubjectEntityStatementCriterion [value=" + value.getSignedStatement().serialize() + "]";
+        return "SubjectEntityStatementCriterion [value=" + value.getJwt().serialize() + "]";
     }
 
     /** {@inheritDoc} */
@@ -74,6 +73,6 @@ public class SubjectEntityStatementCriterion implements Criterion {
             return false;
         }
         final SubjectEntityStatementCriterion other = (SubjectEntityStatementCriterion) obj;
-        return value.getSignedStatement().serialize().equals(other.value.getSignedStatement().serialize());
+        return value.getJwt().serialize().equals(other.value.getJwt().serialize());
     }
 }
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/SubordinateStatementCacheIdentifier.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/SubordinateStatementCacheIdentifier.java
new file mode 100644
index 0000000..6d21536
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/SubordinateStatementCacheIdentifier.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+
+import com.google.common.base.MoreObjects;
+
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Default identifier for subordinate statement within {@link RemoteSubordinateStatementContainer}.
+ */
+public class SubordinateStatementCacheIdentifier implements Serializable {
+
+    /** Serial version UID. */
+    private static final long serialVersionUID = -4169816153785776493L;
+
+    /** Issuer of the subordinate statement. */
+    @Nonnull @NotEmpty private final String issuer;
+
+    /** Subject of the subordinate statement. */
+    @Nonnull @NotEmpty private final String subject;
+
+    /**
+     * Constructor.
+     *
+     * @param iss issuer
+     * @param sub subject
+     */
+    public SubordinateStatementCacheIdentifier(@Nonnull @NotEmpty final String iss,
+            @Nonnull @NotEmpty final String sub) {
+        issuer = Constraint.isNotEmpty(iss, "Issuer cannot be empty");
+        subject = Constraint.isNotEmpty(sub, "Subject cannot be empty");
+    }
+
+    /**
+     * Get the issuer of the subordinate statement.
+     * 
+     * @return issuer
+     */
+    @Nonnull @NotEmpty public String getIssuer() {
+        return issuer;
+    }
+
+    /**
+     * Get the subject of the subordinate statement.
+     * 
+     * @return subject
+     */
+    @Nonnull @NotEmpty public String getSubject() {
+        return subject;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("issuer", getIssuer())
+                .add("subject", getSubject())
+                .toString();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int hashCode() {
+        return Objects.hash(issuer, subject);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final SubordinateStatementCacheIdentifier other = (SubordinateStatementCacheIdentifier) obj;
+        return issuer.equals(other.issuer) && subject.equals(other.subject);
+
+    }
+
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/FederationPolicyConstraint.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/FederationPolicyConstraint.java
index b7ea206..a7c3bba 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/FederationPolicyConstraint.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/FederationPolicyConstraint.java
@@ -19,7 +19,7 @@ import java.util.List;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 
 /**
  * Interface to be implemented by the federation policy constraints.
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/FederationPolicyConstraintHelper.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/FederationPolicyConstraintHelper.java
index 01c82bb..0dfe5f3 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/FederationPolicyConstraintHelper.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/FederationPolicyConstraintHelper.java
@@ -23,9 +23,9 @@ import javax.annotation.Nullable;
 import org.slf4j.Logger;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 
-import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatementHelper;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubordinateStatementPayload;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
 /**
@@ -53,14 +53,19 @@ public class FederationPolicyConstraintHelper {
             return true;
         }
         final Map<String, Object> constraints =
-                EntityStatementHelper.parseClaimAsMap(objectMapper, subordinateStatement, "constraints");
+                subordinateStatement.getParsedPayload() instanceof SubordinateStatementPayload ssp ?
+                        ssp.getConstraints() : null;
+        if (constraints == null || constraints.isEmpty()) {
+            log.trace("No constraints found for {}: {}", subordinateStatement.getSubject(), constraints);
+            return true;
+        }
         for (final String constraint : constraints.keySet()) {
             final FederationPolicyConstraint validator = federationPolicyConstraints.get(constraint);
             if (validator != null) {
                 log.trace("Validating federation policy constraint {} with {}", constraint, validator);
                 if (!validator.validate(constraints.get(constraint), trustChain)) {
                     log.warn("Subordinate statement issued by {} contained constraint {} that failed",
-                            subordinateStatement.getClaimsSet().getIssuer(), constraint);
+                            subordinateStatement.getIssuer(), constraint);
                     return false;
                 }
             } else {
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/impl/AbstractFederationPolicyConstraint.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/impl/AbstractFederationPolicyConstraint.java
index e0563b9..a495486 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/impl/AbstractFederationPolicyConstraint.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/impl/AbstractFederationPolicyConstraint.java
@@ -21,8 +21,7 @@ import javax.annotation.Nullable;
 
 import org.slf4j.Logger;
 
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.constraints.FederationPolicyConstraint;
 import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
 import net.shibboleth.shared.logic.ConstraintViolationException;
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/impl/DefaultAllowedEntityTypesConstraint.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/impl/DefaultAllowedEntityTypesConstraint.java
index f18fe3e..1215d54 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/impl/DefaultAllowedEntityTypesConstraint.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/impl/DefaultAllowedEntityTypesConstraint.java
@@ -17,6 +17,7 @@ package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.constraints.impl;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -24,10 +25,10 @@ import javax.annotation.Nullable;
 import org.slf4j.Logger;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 
-import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatementHelper;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.logic.ConstraintViolationException;
@@ -89,16 +90,16 @@ public class DefaultAllowedEntityTypesConstraint extends AbstractFederationPolic
         log.trace("Allowed entity types: {}", allowedTypes);
         for (final EntityStatement entityStatement : trustChain) {
             assert entityStatement != null;
-            assert objectMapper != null;
             final Map<String, Map<String,Object>> metadata =
-                    EntityStatementHelper.parseMetadata(objectMapper, entityStatement);
+                    Optional.ofNullable(entityStatement.getParsedPayload().getMetadata())
+                    .orElse(CollectionSupport.emptyMap());
             for (final String entityType : metadata.keySet()) {
                 if (!allowedTypes.contains(entityType)) {
                     log.warn("The entity type {} is not allowed in entity statement {}", entityType,
-                            entityStatement.getEntityID());
+                            entityStatement.getSubject());
                     return false;
                 }
-                log.trace("The entity type {} is allowed for {}", entityType, entityStatement.getEntityID());
+                log.trace("The entity type {} is allowed for {}", entityType, entityStatement.getSubject());
             }
         }
         return true;
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/impl/DefaultMaxPathLengthConstraint.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/impl/DefaultMaxPathLengthConstraint.java
index c3dca69..6284b24 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/impl/DefaultMaxPathLengthConstraint.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/impl/DefaultMaxPathLengthConstraint.java
@@ -21,8 +21,7 @@ import javax.annotation.Nullable;
 
 import org.slf4j.Logger;
 
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 import net.shibboleth.shared.logic.ConstraintViolationException;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
@@ -51,7 +50,7 @@ public class DefaultMaxPathLengthConstraint extends AbstractFederationPolicyCons
     protected boolean doValidate(@Nonnull final Integer constraintData,
             @Nonnull final List<EntityStatement> trustChain) {
         final List<EntityStatement> intermediates = trustChain.stream()
-                .filter(es -> !es.getEntityID().getValue().equals(es.getClaimsSet().getIssuer().getValue()))
+                .filter(es -> !es.getSubject().equals(es.getIssuer()))
                 .toList();
         final int length = intermediates.size();
         log.trace("Maximum path length: {}, amount of intermediates is {}", constraintData, length);
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/impl/DefaultNamingConstraintsConstraint.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/impl/DefaultNamingConstraintsConstraint.java
index c3ff2e2..6110572 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/impl/DefaultNamingConstraintsConstraint.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/constraints/impl/DefaultNamingConstraintsConstraint.java
@@ -30,8 +30,7 @@ import org.bouncycastle.asn1.x509.NameConstraintValidatorException;
 import org.bouncycastle.asn1.x509.PKIXNameConstraintValidator;
 import org.slf4j.Logger;
 
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.logic.ConstraintViolationException;
 import net.shibboleth.shared.primitive.LoggerFactory;
@@ -96,7 +95,7 @@ public class DefaultNamingConstraintsConstraint extends AbstractFederationPolicy
                     new GeneralSubtree(new GeneralName(GeneralName.dNSName, item))));
 
         for (final EntityStatement entityStatement : trustChain) {
-            final String host = URI.create(entityStatement.getEntityID().getValue()).getHost();
+            final String host = URI.create(entityStatement.getSubject()).getHost();
             try {
                 bcValidator.checkExcluded(new GeneralName(GeneralName.dNSName, host));
                 log.trace("Validation for 'excluded' was successful for {}", host);
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ec/DefaultEntityConfigurationContentValidationCondition.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ec/DefaultEntityConfigurationContentValidationCondition.java
new file mode 100644
index 0000000..21f03d5
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ec/DefaultEntityConfigurationContentValidationCondition.java
@@ -0,0 +1,92 @@
+/*
+ * 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.ec;
+
+import java.text.ParseException;
+import java.util.function.BiPredicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.BaseExpirableEntityStatementContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.RemoteEntityConfigurationContainer;
+import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
+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.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Default content validating filter for entity configuration. Verifies that the 
+ * {@link RemoteEntityConfigurationContainer#getEntityId()} matches with the subject of the entity configuration and
+ * validates the claims via configurable claims validator. Note that the {@link ProfileRequestContext} is not fed to
+ * the claims validator.
+ */
+ at ThreadSafeAfterInit
+public class DefaultEntityConfigurationContentValidationCondition
+    extends AbstractIdentifiableInitializableComponent
+    implements BiPredicate<BaseExpirableEntityStatementContainer, MetadataFilterContext>  {
+
+    /** Class logger. */
+    @Nonnull private Logger log =
+            LoggerFactory.getLogger(DefaultEntityConfigurationContentValidationCondition.class);
+
+    /** The claims validator to use for validating the entity statement claims. */
+    @NonnullAfterInit private ClaimsValidator claimsValidator;
+
+    /**
+     * Set the claims validator to use for validating the entity statement claims.
+     * 
+     * @param validator claims validator
+     */
+    public void setClaimsValidator(@Nonnull final ClaimsValidator validator) {
+        checkSetterPreconditions();
+        claimsValidator = Constraint.isNotNull(validator, "Claims validator cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean test(@Nullable final BaseExpirableEntityStatementContainer responseContainer,
+            @Nullable final MetadataFilterContext filterContext) {
+        checkComponentActive();
+
+        if (responseContainer == null || responseContainer.getEntityStatement() == null) {
+            return false;
+        }
+
+        final EntityStatement statement = responseContainer.getEntityStatement();
+        if (!responseContainer.getEntityId().equals(statement.getParsedPayload().getSubject())) {
+            log.warn("Entity statement subject {} does not match with the requested entity ID {}",
+                    statement.getParsedPayload().getSubject(), responseContainer.getEntityId());
+            return false;
+        }
+
+        try {
+            claimsValidator.validate(statement.getJwt().getJWTClaimsSet(), null);
+        } catch (final JWTValidationException | ParseException e) {
+            log.warn("Claims validation failed for entity statement {}", responseContainer.getEntityId(), e);
+            return false;
+        }
+        log.trace("Entity statement claims successfully validated for {}", responseContainer.getEntityId());
+        return true;
+    }
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ec/DefaultEntityConfigurationCriteriaSetLookupFunction.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ec/DefaultEntityConfigurationCriteriaSetLookupFunction.java
new file mode 100644
index 0000000..1ace272
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ec/DefaultEntityConfigurationCriteriaSetLookupFunction.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.metadata.ec;
+
+import java.util.Optional;
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.BaseExpirableEntityStatementContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.SubjectEntityStatementCriterion;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Default criteria set lookup strategy for entity configurations.
+ */
+ at ThreadSafe
+public class DefaultEntityConfigurationCriteriaSetLookupFunction
+    implements Function<BaseExpirableEntityStatementContainer, CriteriaSet> {
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public CriteriaSet apply(@Nullable final BaseExpirableEntityStatementContainer metadataContainer) {
+        return Optional.ofNullable(metadataContainer)
+                .map(container -> container.getEntityStatement())
+                .map(statement -> new CriteriaSet(new SubjectEntityStatementCriterion(statement)))
+                .orElse(null);
+    }
+
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ec/DefaultEntityConfigurationFetchingStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ec/DefaultEntityConfigurationFetchingStrategy.java
new file mode 100644
index 0000000..1e52a13
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ec/DefaultEntityConfigurationFetchingStrategy.java
@@ -0,0 +1,129 @@
+/*
+ * 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.ec;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.ProtocolException;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.net.URIBuilder;
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityConfiguration;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityIDCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.AbstractFederationEndpointResponseFetchingStrategy;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.RemoteEntityConfigurationContainer;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Default strategy for fetching entity configuration for the request specified in the criteria set. The parsed
+ * response details are stored inside a {@link RemoteEntityConfigurationContainer}.
+ */
+ at ThreadSafeAfterInit
+public class DefaultEntityConfigurationFetchingStrategy
+    extends AbstractFederationEndpointResponseFetchingStrategy<String, RemoteEntityConfigurationContainer> {
+
+    /** The JWT type header. */
+    @Nonnull
+    public static final JOSEObjectType JWT_TYPE_HEADER = new JOSEObjectType("entity-statement+jwt");
+
+    /** The HTTP response content type. */
+    @Nonnull public static final String HTTP_RESPONSE_CONTENT_TYPE = "application/" + JWT_TYPE_HEADER.toString();
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(DefaultEntityConfigurationFetchingStrategy.class);
+
+    /**
+     * Constructor.
+     */
+    public DefaultEntityConfigurationFetchingStrategy() {
+        setCriteriaToRequestDataStrategy(criteria -> {
+            final SubjectEntityIDCriterion criterion = criteria.get(SubjectEntityIDCriterion.class);
+            if (criterion == null) {
+                log.debug("No SubjectEntityIDCriterion, returning null");
+                return null;
+            }
+            return criterion.getValue();
+        });
+    }
+
+    /** {@inheritDoc} */
+    @Nullable protected ClassicHttpRequest initializeHttpRequest(@Nonnull final CriteriaSet criteria,
+            @Nonnull final String entityId) {
+        final HttpGet httpRequest = new HttpGet(entityId);
+        try {
+            final String path = httpRequest.getPath();
+            final URI uri =
+                    new URIBuilder(httpRequest.getUri()).setPath(
+                            (path.endsWith("/") ? path : path.concat("/"))
+                            .concat(".well-known/openid-federation")).build();
+            httpRequest.setUri(uri);
+        } catch (final URISyntaxException e) {
+            log.error("Could not create URI with the given parameters {}", entityId, e);
+        }
+        return httpRequest;
+    }
+
+    /** {@inheritDoc} */
+    @Nullable protected RemoteEntityConfigurationContainer parseHttpResponse(@Nonnull final CriteriaSet criteria,
+            @Nonnull final String entityId, @Nullable final ClassicHttpResponse response,
+            @Nonnull final Instant validExpiration, @Nonnull final Instant invalidExpiration)
+                    throws ProtocolException, IOException {
+        if (response != null) {
+            if (!HTTP_RESPONSE_CONTENT_TYPE.equals(response.getEntity().getContentType())) {
+                log.warn("Unexpected content type: {}", response.getEntity().getContentType());
+                return new RemoteEntityConfigurationContainer(entityId, null, validExpiration, invalidExpiration);
+            }
+
+            try {
+                final SignedJWT jwt = SignedJWT.parse(EntityUtils.toString(response.getEntity()));
+                if (!JWT_TYPE_HEADER.equals(jwt.getHeader().getType())) {
+                    log.warn("Unexpected JWT type header {}", jwt.getHeader().getType());
+                    return new RemoteEntityConfigurationContainer(entityId, null, validExpiration, invalidExpiration);
+                }
+                assert objectMapper != null;
+                final EntityConfiguration entityConfiguration = EntityConfiguration.parse(jwt, objectMapper);
+                return new RemoteEntityConfigurationContainer(
+                        entityId, entityConfiguration, validExpiration, invalidExpiration);
+            } catch (final JsonProcessingException e) {
+                log.warn("Could not deserialize entity statement payload from the response", e);
+            } catch (final ConstraintViolationException e) {
+                log.warn("Parsed entity configuration claims did not pass syntax validation", e);
+            } catch (java.text.ParseException e) {
+                log.warn("Could not parse JWT from the response", e);
+            }
+        } else {
+            log.debug("Unable to fetch entity configuration for: {} (null response)", entityId);
+        }
+
+        return new RemoteEntityConfigurationContainer(entityId, null, validExpiration, invalidExpiration);
+    }
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/jackson/InstantDeserializer.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/jackson/InstantDeserializer.java
new file mode 100644
index 0000000..b16bce8
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/jackson/InstantDeserializer.java
@@ -0,0 +1,49 @@
+/*
+ * 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.jackson;
+
+import java.io.IOException;
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.type.TypeFactory;
+
+/**
+ * Jackson deserializer for {@link Instant} that exploits {@link Instant#ofEpochSecond(long)}.
+ */
+public class InstantDeserializer extends JsonDeserializer<Instant> {
+
+    /**
+     * Constructor.
+     */
+    public InstantDeserializer() {
+        // no op
+    }
+
+    /** {@inheritDoc} */
+    @Override @Nonnull
+    public Instant deserialize(final JsonParser jsonParser, final DeserializationContext deserializationContext)
+            throws IOException, JsonProcessingException {
+
+        final JavaType longType = TypeFactory.defaultInstance().constructType(Long.class);
+        return Instant.ofEpochSecond(deserializationContext.readValue(jsonParser, longType));
+    }
+}
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/jackson/JWKSetDeserializer.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/jackson/JWKSetDeserializer.java
new file mode 100644
index 0000000..b458b03
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/jackson/JWKSetDeserializer.java
@@ -0,0 +1,70 @@
+/*
+ * 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.jackson;
+
+import java.io.IOException;
+import java.text.ParseException;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.type.MapType;
+import com.fasterxml.jackson.databind.type.TypeFactory;
+import com.nimbusds.jose.jwk.JWKSet;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Jackson deserializer for {@link JWKSet} that exploits {@link JWKSet#parse(Map)}.
+ */
+public class JWKSetDeserializer extends JsonDeserializer<JWKSet> {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(JWKSetDeserializer.class);
+
+    /**
+     * Constructor.
+     */
+    public JWKSetDeserializer() {
+        // no op
+    }
+
+    /** {@inheritDoc} */
+    @Override @Nonnull
+    public JWKSet deserialize(final JsonParser jsonParser, final DeserializationContext deserializationContext)
+            throws IOException, JsonProcessingException {
+
+        log.debug("Starting deseralize");
+        final JavaType objectType = TypeFactory.defaultInstance().constructType(Object.class);
+        final JavaType stringType = TypeFactory.defaultInstance().constructType(String.class);
+        final MapType objectMapType =
+                TypeFactory.defaultInstance().constructMapType(Map.class, stringType, objectType);
+
+        final Map<String,Object> map = deserializationContext.readValue(jsonParser, objectMapType);
+        try {
+            return JWKSet.parse(map);
+        } catch (final ParseException e) {
+            throw new IOException("Could not parse JWKSet from the given input", e);
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultSubordinateStatementCriteriaToIdentifierStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ss/DefaultSubordinateStatementCriteriaToIdentifierStrategy.java
similarity index 84%
rename from idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultSubordinateStatementCriteriaToIdentifierStrategy.java
rename to idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ss/DefaultSubordinateStatementCriteriaToIdentifierStrategy.java
index 6e589b2..a942b37 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultSubordinateStatementCriteriaToIdentifierStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ss/DefaultSubordinateStatementCriteriaToIdentifierStrategy.java
@@ -12,28 +12,29 @@
  * limitations under the License.
  */
 
-package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ss;
 
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.SubordinateStatementCacheIdentifier;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
-import net.shibboleth.shared.collection.Pair;
 import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.resolver.CriteriaSet;
 
 /**
- * Default identifier strategy for subordinate statement. The criteria set is transformed into a {@link Pair} that
- * consists of subject and issuer entity IDs.
+ * Default identifier strategy for subordinate statement. The criteria set is transformed into a
+ * {@link SubordinateStatementCacheIdentifier} that consists of subject and issuer entity IDs.
  */
 @ThreadSafeAfterInit
 public class DefaultSubordinateStatementCriteriaToIdentifierStrategy
-        extends AbstractIdentifiableInitializableComponent implements Function<CriteriaSet, Pair<String, String>> {
+        extends AbstractIdentifiableInitializableComponent
+        implements Function<CriteriaSet, SubordinateStatementCacheIdentifier> {
 
     /** Strategy to fetch subject entity ID from criteria set. */
     @NonnullAfterInit private Function<CriteriaSet, String> criteriaToSubjectEntityIdStrategy;
@@ -77,7 +78,7 @@ public class DefaultSubordinateStatementCriteriaToIdentifierStrategy
 
     /** {@inheritDoc} */
     @Override @Nullable
-    public Pair<String, String> apply(@Nullable final CriteriaSet criteria) {
+    public SubordinateStatementCacheIdentifier apply(@Nullable final CriteriaSet criteria) {
         checkComponentActive();
         final String subjectEntityId = criteriaToSubjectEntityIdStrategy.apply(criteria);
         if (subjectEntityId == null) {
@@ -87,7 +88,7 @@ public class DefaultSubordinateStatementCriteriaToIdentifierStrategy
         if (issuerEntityId == null) {
             return null;
         }
-        return new Pair<>(issuerEntityId, subjectEntityId);
+        return new SubordinateStatementCacheIdentifier(issuerEntityId, subjectEntityId);
     }
 
 }
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ss/DefaultSubordinateStatementFetchingStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ss/DefaultSubordinateStatementFetchingStrategy.java
new file mode 100644
index 0000000..76af4b5
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ss/DefaultSubordinateStatementFetchingStrategy.java
@@ -0,0 +1,183 @@
+/*
+ * 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.ss;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.ProtocolException;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.net.URIBuilder;
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.IssuerEntityIDCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityIDCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubordinateStatement;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.AbstractFederationEndpointResponseFetchingStrategy;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.RemoteEntityConfigurationContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.RemoteSubordinateStatementContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.SubordinateStatementCacheIdentifier;
+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.ThreadSafeAfterInit;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Default strategy for fetching entity configuration for the request specified in the criteria set. The parsed
+ * response details are stored inside a {@link RemoteEntityConfigurationContainer}.
+ */
+ at ThreadSafeAfterInit
+public class DefaultSubordinateStatementFetchingStrategy
+    extends AbstractFederationEndpointResponseFetchingStrategy
+      <SubordinateStatementCacheIdentifier, RemoteSubordinateStatementContainer> {
+
+    /** The JWT type header. */
+    @Nonnull
+    public static final JOSEObjectType JWT_TYPE_HEADER = new JOSEObjectType("entity-statement+jwt");
+
+    /** The HTTP response content type. */
+    @Nonnull public static final String HTTP_RESPONSE_CONTENT_TYPE = "application/" + JWT_TYPE_HEADER.toString();
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(DefaultSubordinateStatementFetchingStrategy.class);
+
+    @NonnullAfterInit private MetadataCache<RemoteEntityConfigurationContainer> entityConfigurationCache;
+
+    /**
+     * Constructor.
+     */
+    public DefaultSubordinateStatementFetchingStrategy() {
+        setCriteriaToRequestDataStrategy(criteria -> {
+            final SubjectEntityIDCriterion subjectCriterion = criteria.get(SubjectEntityIDCriterion.class);
+            if (subjectCriterion == null) {
+                log.debug("No SubjectEntityIDCriterion, returning null");
+                return null;
+            }
+            final IssuerEntityIDCriterion issuerCriterion = criteria.get(IssuerEntityIDCriterion.class);
+            if (issuerCriterion == null) {
+                log.debug("No IssuerEntityIDCriterion, returning null");
+                return null;
+            }
+            return new SubordinateStatementCacheIdentifier(issuerCriterion.getValue(), subjectCriterion.getValue());
+        });
+    }
+
+    public void setEntityConfigurationCache(@Nonnull final MetadataCache<RemoteEntityConfigurationContainer> cache) {
+        checkSetterPreconditions();
+        entityConfigurationCache = Constraint.isNotNull(cache, "Entity configuration cache cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Nullable protected ClassicHttpRequest initializeHttpRequest(@Nonnull final CriteriaSet criteria,
+            @Nonnull final SubordinateStatementCacheIdentifier id) {
+        final String issuerEntityId = id.getIssuer();
+        final List<RemoteEntityConfigurationContainer> issuerStatements;
+        try {
+            issuerStatements =
+                    entityConfigurationCache.get(new CriteriaSet(
+                            new SubjectEntityIDCriterion(issuerEntityId)));
+        } catch (final MetadataCacheException e) {
+            log.error("Could not resolve entity configuration for issuer {}", issuerEntityId, e);
+            return null;
+        }
+        if (issuerStatements.isEmpty()) {
+            log.error("Could not resolve entity configuration for issuer {}", issuerEntityId);
+            return null;
+        }
+        final EntityStatement entityConfiguration = issuerStatements.get(0).getEntityStatement();
+        if (entityConfiguration == null) {
+            log.error("Could not resolve entity configuration for issuer {}", issuerEntityId);
+            return null;
+        }
+        final Map<String, Object> federationEntity =
+                Optional.ofNullable(entityConfiguration.getParsedPayload().getMetadata())
+                .map(metadata -> metadata.get("federation_entity"))
+                .orElse(null);
+        if (federationEntity == null) {
+            log.error("TODO");
+            return null;
+        }
+        if (federationEntity.get("federation_fetch_endpoint") instanceof String endpoint) {
+            final HttpGet httpRequest = new HttpGet(endpoint);
+            try {
+                final URI uri =
+                        new URIBuilder(httpRequest.getUri()).addParameter("sub", id.getSubject()).build();
+                httpRequest.setUri(uri);
+                log.debug("URI set to {}", uri);
+            } catch (final URISyntaxException e) {
+                log.error("Could not create URI with the given parameters {}", issuerEntityId, e);
+            }
+            return httpRequest;
+        } else {
+            log.warn("Could not parse federation_fetch_endpoint for {}", issuerEntityId);
+        }
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    @Nullable protected RemoteSubordinateStatementContainer parseHttpResponse(@Nonnull final CriteriaSet criteria,
+            @Nonnull final SubordinateStatementCacheIdentifier id, @Nullable final ClassicHttpResponse response,
+            @Nonnull final Instant validExpiration, @Nonnull final Instant invalidExpiration)
+                    throws ProtocolException, IOException {
+        if (response != null) {
+            if (!HTTP_RESPONSE_CONTENT_TYPE.equals(response.getEntity().getContentType())) {
+                log.warn("Unexpected content type: {}", response.getEntity().getContentType());
+                return new RemoteSubordinateStatementContainer(id, null, validExpiration, invalidExpiration);
+            }
+
+            try {
+                final SignedJWT jwt = SignedJWT.parse(EntityUtils.toString(response.getEntity()));
+                if (!JWT_TYPE_HEADER.equals(jwt.getHeader().getType())) {
+                    log.warn("Unexpected JWT type header {}", jwt.getHeader().getType());
+                    return new RemoteSubordinateStatementContainer(id, null, validExpiration, invalidExpiration);
+                }
+                assert objectMapper != null;
+                final SubordinateStatement statement = SubordinateStatement.parse(jwt, objectMapper);
+                log.trace("Subordinate statement successfully parsed and stored into the container");
+                return new RemoteSubordinateStatementContainer(id, statement, validExpiration, invalidExpiration);
+            } catch (final JsonProcessingException e) {
+                log.warn("Could not deserialize entity statement payload from the response", e);
+            } catch (final ConstraintViolationException e) {
+                log.warn("Parsed entity configuration claims did not pass syntax validation", e);
+            } catch (java.text.ParseException e) {
+                log.warn("Could not parse JWT from the response", e);
+            }
+        } else {
+            log.debug("Unable to fetch entity configuration for: {} (null response)", id);
+        }
+
+        return new RemoteSubordinateStatementContainer(id, null, validExpiration, invalidExpiration);
+    }
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ss/DefaultSubordinateStatementSignatureValidationCondition.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ss/DefaultSubordinateStatementSignatureValidationCondition.java
new file mode 100644
index 0000000..7ba3b68
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/ss/DefaultSubordinateStatementSignatureValidationCondition.java
@@ -0,0 +1,116 @@
+/*
+ * 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.ss;
+
+import java.util.List;
+import java.util.function.BiPredicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.trust.TrustEngine;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.AbstractTrustEngineSignatureValidationComponent;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityIDCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.IssuerEntityStatementCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.RemoteEntityConfigurationContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.RemoteSubordinateStatementContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.SubjectEntityStatementCriterion;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+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.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * Default signature validating filter for entity statement. The signature validation is performed via configurable
+ * {@link TrustEngine}.
+ */
+ at ThreadSafeAfterInit
+public class DefaultSubordinateStatementSignatureValidationCondition
+    extends AbstractTrustEngineSignatureValidationComponent
+    implements BiPredicate<RemoteSubordinateStatementContainer, MetadataFilterContext> {
+
+    /** Class logger. */
+    @Nonnull private Logger log =
+            LoggerFactory.getLogger(DefaultSubordinateStatementSignatureValidationCondition.class);
+
+    @NonnullAfterInit private MetadataCache<RemoteEntityConfigurationContainer> entityConfigurationCache;
+
+    public void setEntityConfigurationCache(@Nonnull final MetadataCache<RemoteEntityConfigurationContainer> cache) {
+        checkSetterPreconditions();
+        entityConfigurationCache = Constraint.isNotNull(cache, "Entity configuration cache cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean test(@Nullable final RemoteSubordinateStatementContainer responseContainer,
+            @Nullable final MetadataFilterContext filterContext) {
+        checkComponentActive();
+        if (responseContainer == null) {
+            return false;
+        }
+
+        final EntityStatement statement = responseContainer.getEntityStatement();
+        if (statement != null ) {
+            final String entityId = responseContainer.getIdentifier().getSubject();
+            final String issuer = responseContainer.getIdentifier().getIssuer();
+            assert issuer != null;
+            log.trace("Validating subordinate statement for {}, issued by {}", entityId, issuer);
+            final EntityStatement issuerStatement = fetchIssuerStatement(issuer);
+            if (issuerStatement == null) {
+                log.debug("Could not fetch issuer statement");
+                return false;
+            }
+            final CriteriaSet criteria = new CriteriaSet();
+            criteria.add(new IssuerEntityStatementCriterion(issuerStatement));
+            criteria.add(new SubjectEntityStatementCriterion(statement));
+
+            log.trace("Starting signature validation of entity configuration for {}", entityId);
+            if (validateJwt(statement.getJwt(), criteria, entityId)) {
+                return true;
+            }
+            return false;
+        } else {
+            log.debug("Ignoring signature check for the error response");
+        }
+        return true;
+    }
+
+    /**
+     * Fetch the issuer entity configuration from the metadata cache.
+     * 
+     * @param issuer the issuer entity ID
+     * @return the issuer entity configuration, or null if could not be fetched
+     */
+    @Nullable protected EntityStatement fetchIssuerStatement(@Nonnull final String issuer) {
+        final CriteriaSet criteria = new CriteriaSet(new SubjectEntityIDCriterion(issuer));
+        try {
+            final List<RemoteEntityConfigurationContainer> result = entityConfigurationCache.get(criteria);
+            if (!result.isEmpty()) {
+                return result.get(0).getEntityStatement();
+            }
+        } catch (final MetadataCacheException e) {
+            log.debug("Error while fetching issuer entity configuration for {}", issuer, e);
+        }
+        log.warn("Could not fetch entity configuration for {}", issuer);
+        return null;
+    }
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainSignatureValidationFilterStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/tc/DefaultProvidedTrustChainSignatureValidationFilterStrategy.java
similarity index 83%
copy from idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainSignatureValidationFilterStrategy.java
copy to idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/tc/DefaultProvidedTrustChainSignatureValidationFilterStrategy.java
index 71ee250..0066b6d 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainSignatureValidationFilterStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/tc/DefaultProvidedTrustChainSignatureValidationFilterStrategy.java
@@ -12,7 +12,7 @@
  * limitations under the License.
  */
 
-package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.tc;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -25,8 +25,10 @@ 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.idp.plugin.oidc.op.oidfed.metadata.AbstractTrustEngineSignatureValidationComponent;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SignatureValidationKeyContainerJwtCriterion;
 import net.shibboleth.oidc.metadata.filter.MetadataFilterContext;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
@@ -48,12 +50,13 @@ import net.shibboleth.shared.resolver.CriteriaSet;
  * All the chains that don't pass the full validation are filtered out from the result.
  */
 @ThreadSafeAfterInit
-public class DefaultTrustChainSignatureValidationFilterStrategy extends AbstractTrustEngineSignatureValidationComponent
-        implements BiFunction<List<List<EntityStatement>>, MetadataFilterContext, List<List<EntityStatement>>> {
+public class DefaultProvidedTrustChainSignatureValidationFilterStrategy
+    extends AbstractTrustEngineSignatureValidationComponent
+    implements BiFunction<List<List<EntityStatement>>, MetadataFilterContext, List<List<EntityStatement>>> {
 
     /** Class logger. */
     @Nonnull private Logger log =
-            LoggerFactory.getLogger(DefaultTrustChainSignatureValidationFilterStrategy.class);
+            LoggerFactory.getLogger(DefaultProvidedTrustChainSignatureValidationFilterStrategy.class);
 
     /** Signature validation filter strategy for trust anchor entity statements. */
     @NonnullAfterInit private BiFunction<EntityStatement, MetadataFilterContext, EntityStatement>
@@ -89,26 +92,26 @@ public class DefaultTrustChainSignatureValidationFilterStrategy extends Abstract
         if (trustChains == null) {
             return null;
         }
-
         final List<List<EntityStatement>> result = new ArrayList<>();
+        
         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 SignedJWT keyContainer = trustChain.get(i + 1).getJwt();
                 final CriteriaSet criteria =
                         new CriteriaSet(new SignatureValidationKeyContainerJwtCriterion(keyContainer));
-                if (!validateJwt(trustChainEntry.getSignedStatement(), criteria,
-                        trustChain.get(i).getEntityID().getValue())) {
+                if (!validateJwt(trustChainEntry.getJwt(), criteria,
+                        trustChain.get(i).getSubject())) {
                     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());
+                                    trustChain.get(trustChain.size() - 1).getSubject());
                     log.trace("Key container {} could not validate the trust chain entry {}", keyContainer.serialize(),
-                            trustChainEntry.getSignedStatement().serialize());
+                            trustChainEntry.getJwt().serialize());
                     continue trustChainLoop;
                 }
             }
             final EntityStatement trustAnchorStatement = trustChain.get(trustChain.size() - 1);
-            final String entityId = trustAnchorStatement.getEntityID().getValue();
+            final String entityId = trustAnchorStatement.getSubject();
             final EntityStatement filteredStatement =
                     entityStatementSignatureValidationFilterStrategy.apply(trustAnchorStatement, filterContext);
             if (filteredStatement != null) {
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainSignatureValidationFilterStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/tc/DefaultRemoteTrustChainSignatureValidationFilterStrategy.java
similarity index 77%
rename from idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainSignatureValidationFilterStrategy.java
rename to idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/tc/DefaultRemoteTrustChainSignatureValidationFilterStrategy.java
index 71ee250..cd1d8f0 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainSignatureValidationFilterStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/tc/DefaultRemoteTrustChainSignatureValidationFilterStrategy.java
@@ -12,9 +12,8 @@
  * limitations under the License.
  */
 
-package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.tc;
 
-import java.util.ArrayList;
 import java.util.List;
 import java.util.function.BiFunction;
 
@@ -25,8 +24,11 @@ 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.idp.plugin.oidc.op.oidfed.metadata.AbstractTrustEngineSignatureValidationComponent;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SignatureValidationKeyContainerJwtCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.RemoteTrustChainsContainer;
 import net.shibboleth.oidc.metadata.filter.MetadataFilterContext;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
@@ -48,12 +50,13 @@ import net.shibboleth.shared.resolver.CriteriaSet;
  * All the chains that don't pass the full validation are filtered out from the result.
  */
 @ThreadSafeAfterInit
-public class DefaultTrustChainSignatureValidationFilterStrategy extends AbstractTrustEngineSignatureValidationComponent
-        implements BiFunction<List<List<EntityStatement>>, MetadataFilterContext, List<List<EntityStatement>>> {
+public class DefaultRemoteTrustChainSignatureValidationFilterStrategy
+    extends AbstractTrustEngineSignatureValidationComponent
+    implements BiFunction<RemoteTrustChainsContainer, MetadataFilterContext, RemoteTrustChainsContainer> {
 
     /** Class logger. */
     @Nonnull private Logger log =
-            LoggerFactory.getLogger(DefaultTrustChainSignatureValidationFilterStrategy.class);
+            LoggerFactory.getLogger(DefaultRemoteTrustChainSignatureValidationFilterStrategy.class);
 
     /** Signature validation filter strategy for trust anchor entity statements. */
     @NonnullAfterInit private BiFunction<EntityStatement, MetadataFilterContext, EntityStatement>
@@ -83,42 +86,42 @@ public class DefaultTrustChainSignatureValidationFilterStrategy extends Abstract
 
     /** {@inheritDoc} */
     @Override @Nullable
-    public List<List<EntityStatement>> apply(@Nullable final List<List<EntityStatement>> trustChains,
+    public RemoteTrustChainsContainer apply(@Nullable final RemoteTrustChainsContainer container,
             @Nullable final MetadataFilterContext filterContext) {
         checkComponentActive();
-        if (trustChains == null) {
+        if (container == null) {
             return null;
         }
 
-        final List<List<EntityStatement>> result = new ArrayList<>();
-        trustChainLoop: for (final List<EntityStatement> trustChain : trustChains) {
+        trustChainLoop: for (final List<EntityStatement> trustChain : container.getTrustChains()) {
             for (int i = 0; i < trustChain.size() - 1; i++) {
                 final EntityStatement trustChainEntry = trustChain.get(i);
-                final SignedJWT keyContainer = trustChain.get(i + 1).getSignedStatement();
+                final SignedJWT keyContainer = trustChain.get(i + 1).getJwt();
                 final CriteriaSet criteria =
                         new CriteriaSet(new SignatureValidationKeyContainerJwtCriterion(keyContainer));
-                if (!validateJwt(trustChainEntry.getSignedStatement(), criteria,
-                        trustChain.get(i).getEntityID().getValue())) {
+                if (!validateJwt(trustChainEntry.getJwt(), criteria,
+                        trustChain.get(i).getSubject())) {
                     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());
+                                    trustChain.get(trustChain.size() - 1).getSubject());
                     log.trace("Key container {} could not validate the trust chain entry {}", keyContainer.serialize(),
-                            trustChainEntry.getSignedStatement().serialize());
+                            trustChainEntry.getJwt().serialize());
+                    container.removeTrustChain(trustChain);
                     continue trustChainLoop;
                 }
             }
             final EntityStatement trustAnchorStatement = trustChain.get(trustChain.size() - 1);
-            final String entityId = trustAnchorStatement.getEntityID().getValue();
+            final String entityId = trustAnchorStatement.getSubject();
             final EntityStatement filteredStatement =
                     entityStatementSignatureValidationFilterStrategy.apply(trustAnchorStatement, filterContext);
             if (filteredStatement != null) {
                 log.debug("Successfully validated entity statement for trust anchor {}", entityId);
-                result.add(trustChain);
             } else {
                 log.debug("Signature validation failed for trust anchor {}", entityId);
+                container.removeTrustChain(trustChain);
             }
         }
-        return result;
+        return container;
     }
 
 }
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainFetchingStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/tc/DefaultTrustChainFetchingStrategy.java
similarity index 72%
rename from idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainFetchingStrategy.java
rename to idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/tc/DefaultTrustChainFetchingStrategy.java
index 8ed6760..a0eca13 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainFetchingStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/tc/DefaultTrustChainFetchingStrategy.java
@@ -12,11 +12,14 @@
  * limitations under the License.
  */
 
-package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.tc;
 
+import java.time.Duration;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.function.Function;
 import java.util.stream.Stream;
 
@@ -26,9 +29,19 @@ import javax.annotation.Nullable;
 import org.slf4j.Logger;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityID;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityConfiguration;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityConfigurationPayload;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.IssuerEntityIDCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.LocalKeyContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.PreSelectedTrustChainCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityIDCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.BaseExpirableEntityStatementContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.RemoteEntityConfigurationContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.RemoteSubordinateStatementContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.RemoteTrustChainsContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.SubjectEntityStatementCriterion;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.constraints.FederationPolicyConstraint;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.constraints.FederationPolicyConstraintHelper;
 import net.shibboleth.oidc.metadata.cache.MetadataCache;
@@ -51,7 +64,7 @@ import net.shibboleth.shared.resolver.CriteriaSet;
  */
 @ThreadSafeAfterInit
 public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableInitializableComponent
-        implements Function<CriteriaSet, List<List<EntityStatement>>> {
+        implements Function<CriteriaSet, RemoteTrustChainsContainer> {
 
     /** Class logger. */
     @Nonnull private Logger log = LoggerFactory.getLogger(DefaultTrustChainFetchingStrategy.class);
@@ -60,10 +73,10 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
     @NonnullAfterInit private Function<CriteriaSet, String> criteriaToSubjectEntityIdStrategy;
 
     /** Cache for entity configurations. */
-    @NonnullAfterInit private MetadataCache<EntityStatement> entityConfigurationCache;
+    @NonnullAfterInit private MetadataCache<RemoteEntityConfigurationContainer> entityConfigurationCache;
 
     /** Cache for subordinate entity statements. */
-    @NonnullAfterInit private MetadataCache<EntityStatement> subordinateStatementCache;
+    @NonnullAfterInit private MetadataCache<RemoteSubordinateStatementContainer> subordinateStatementCache;
 
     /** Cache containing local copies of trusted trust anchor keys. */
     @NonnullAfterInit private MetadataCache<Map<String, LocalKeyContainer>> localTrustAnchorsCache;
@@ -74,6 +87,12 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
     /** JSON object mapper used for decoding JSON into Map. */
     @NonnullAfterInit private ObjectMapper objectMapper;
 
+    /** Strategy to fetch lifetime for container with valid contents. */
+    @NonnullAfterInit private Function<CriteriaSet, Duration> criteriaToValidContainerLifetimeStrategy;
+
+    /** Strategy to fetch lifetime for container with invalid contents. */
+    @NonnullAfterInit private Function<CriteriaSet, Duration> criteriaToInvalidContainerLifetimeStrategy;
+
     /**
      * Set the strategy for fetching entity ID from the criteria set.
      * 
@@ -90,7 +109,7 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
      * 
      * @param cache entity configurations cache
      */
-    public void setEntityConfigurationCache(@Nonnull final MetadataCache<EntityStatement> cache) {
+    public void setEntityConfigurationCache(@Nonnull final MetadataCache<RemoteEntityConfigurationContainer> cache) {
         checkSetterPreconditions();
         entityConfigurationCache = Constraint.isNotNull(cache, "Entity Configuration cache cannot be null");
     }
@@ -100,7 +119,7 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
      * 
      * @param cache subordinate statements cache.
      */
-    public void setSubordinateStatementCache(@Nonnull final MetadataCache<EntityStatement> cache) {
+    public void setSubordinateStatementCache(@Nonnull final MetadataCache<RemoteSubordinateStatementContainer> cache) {
         checkSetterPreconditions();
         subordinateStatementCache = Constraint.isNotNull(cache, "Subordinate statement cache cannot be null");
     }
@@ -136,6 +155,31 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
         objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
     }
 
+    /**
+     * Set the strategy to fetch lifetime for container with valid contents.
+     * 
+     * @param strategy lifetime strategy
+     */
+    public void setCriteriaToValidContainerLifetimeStrategy(@Nonnull final Function<CriteriaSet, Duration> strategy) {
+        checkSetterPreconditions();
+
+        criteriaToValidContainerLifetimeStrategy =
+                Constraint.isNotNull(strategy, "Criteria to valid container lifetime strategy cannot be null");
+    }
+
+    /**
+     * Set the strategy to fetch lifetime for container with invalid contents.
+     * 
+     * @param strategy lifetime strategy
+     */
+    public void setCriteriaToInvalidContainerLifetimeStrategy(
+            @Nonnull final Function<CriteriaSet, Duration> strategy) {
+        checkSetterPreconditions();
+
+        criteriaToInvalidContainerLifetimeStrategy =
+                Constraint.isNotNull(strategy, "Criteria to invalid container lifetime strategy cannot be null");
+    }
+
     /** {@inheritDoc} */
     @Override
     protected void doInitialize() throws ComponentInitializationException {
@@ -158,36 +202,59 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
         if (objectMapper == null) {
             throw new ComponentInitializationException("Object mapper cannot be null");
         }
+        if (criteriaToValidContainerLifetimeStrategy == null) {
+            throw new ComponentInitializationException("Criteria to valid container lifetime strategy cannot be null");
+        }
+        if (criteriaToInvalidContainerLifetimeStrategy == null) {
+            throw new ComponentInitializationException(
+                    "Criteria to invalid container lifetime strategy cannot be null");
+        }
     }
 
     /** {@inheritDoc} */
     @Override @Nullable
-    public List<List<EntityStatement>> apply(@Nullable final CriteriaSet criteria) {
+    public RemoteTrustChainsContainer apply(@Nullable final CriteriaSet criteria) {
         checkComponentActive();
         if (criteria == null) {
             return null;
         }
+        final Duration validLifetime = criteriaToValidContainerLifetimeStrategy.apply(criteria);
+        if (validLifetime == null) {
+            log.warn("Could not fetch expiration time for valid container");
+            return null;
+        }
+        final Instant validExpiration = Instant.now().plus(validLifetime);
+        final Duration invalidLifetime = criteriaToInvalidContainerLifetimeStrategy.apply(criteria);
+        if (invalidLifetime == null) {
+            log.warn("Could not fetch expiration time for invalid container");
+            return null;
+        }
+        final Instant invalidExpiration = Instant.now().plus(invalidLifetime);
         final PreSelectedTrustChainCriterion preSelectedCriterion = criteria.get(PreSelectedTrustChainCriterion.class);
         final List<String> preSelectedChain =
                 preSelectedCriterion == null ? CollectionSupport.emptyList() : preSelectedCriterion.getValue();
-        final EntityStatement entityConfiguration;
+        final EntityConfiguration entityConfiguration;
         final SubjectEntityStatementCriterion subjectStatementCriterion =
                 criteria.get(SubjectEntityStatementCriterion.class);
         if (subjectStatementCriterion == null) {
             try {
-                final List<EntityStatement> entityConfigurations = entityConfigurationCache.get(criteria);
-                if (entityConfigurations.isEmpty()) {
+                final List<RemoteEntityConfigurationContainer> containers = entityConfigurationCache.get(criteria);
+                if (containers.isEmpty()) {
                     return null;
                 }
-                entityConfiguration = entityConfigurations.get(0);
+                entityConfiguration = Optional.ofNullable(containers.get(0).getEntityStatement())
+                        .filter(EntityConfiguration.class::isInstance)
+                        .map(EntityConfiguration.class::cast)
+                        .orElse(null);
             } catch (final MetadataCacheException e) {
                 log.error("Could not fetch entity configuration for the trust chain", e);
                 return null;
             }
         } else {
-            entityConfiguration = subjectStatementCriterion.getValue();
+            entityConfiguration = subjectStatementCriterion.getValue() instanceof EntityConfiguration ec ? ec : null;
         }
 
+        log.trace("Entity configuration found to build the trust chains on: {}", entityConfiguration != null);
         if (entityConfiguration == null) {
             return null;
         }
@@ -196,7 +263,11 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
                 CollectionSupport.listOf(CollectionSupport.listOf(entityConfiguration)), preSelectedChain);
         final List<List<EntityStatement>> trustChains =
                 stripIntermediateConfigurations(entityConfiguration, rawChains);
-        return Stream.concat(trustChains.stream(), resolveLocallyTrustedTrustChains(trustChains).stream()).toList();
+        final List<List<EntityStatement>> result = Stream.concat(trustChains.stream(),
+                resolveLocallyTrustedTrustChains(trustChains).stream()).toList();
+        assert result != null; assert invalidExpiration != null; assert validExpiration != null;
+        return new RemoteTrustChainsContainer(result.isEmpty() ? invalidExpiration : validExpiration,
+                entityConfiguration.getSubject(), result);
     }
 
     /**
@@ -212,9 +283,10 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
         final List<List<EntityStatement>> result = new ArrayList<>();
         for (final List<EntityStatement> chain : chains) {
             result.add(chain.stream()
-                    .filter(es -> (es.equals(leaf) || es.getClaimsSet().getAuthorityHints() == null
-                        || es.getClaimsSet().getAuthorityHints().isEmpty() 
-                        || !es.getEntityID().equals(es.getClaimsSet().getIssuerEntityID())))
+                    .filter(es -> (es.equals(leaf)
+                        || !es.getSubject().equals(es.getIssuer())
+                        || (es.getParsedPayload() instanceof EntityConfigurationPayload ecp
+                                && ecp.getAuthorityHints() == null)))
                     .toList());
         }
         return result;
@@ -235,7 +307,7 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
             for (int i = 0; i < chain.size() - 1; i++) {
                 final EntityStatement statement = chain.get(i);
                 assert statement != null;
-                final String entityId = chain.get(i).getEntityID().getValue();
+                final String entityId = chain.get(i).getSubject();
                 assert entityId != null;
                 if (isLocallyTrusted(statement) && !isTrustAnchor(entityId, result)) {
                     try {
@@ -266,7 +338,7 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
      */
     private boolean isLocallyTrusted(@Nonnull final EntityStatement statement) {
         final CriteriaSet criteria = new CriteriaSet(new SubjectEntityStatementCriterion(statement));
-        final String entityId = statement.getEntityID().getValue();
+        final String entityId = statement.getSubject();
         assert entityId != null;
         final List<Map<String, LocalKeyContainer>> keyContainers;
         try {
@@ -292,7 +364,7 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
     private boolean isTrustAnchor(@Nonnull final String entityId,
             @Nonnull final List<List<EntityStatement>> trustChains) {
         return trustChains.stream()
-                .filter(chain -> entityId.equals(chain.get(chain.size() - 1).getEntityID().getValue()))
+                .filter(chain -> entityId.equals(chain.get(chain.size() - 1).getSubject()))
                 .findAny()
                 .isPresent();
     }
@@ -312,12 +384,13 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
         boolean hints = false;
         for (final List<EntityStatement> chain : entities) {
             final EntityStatement entityStatement = chain.get(chain.size() - 1);
-            final List<EntityID> authorityHints = entityStatement.getClaimsSet().getAuthorityHints();
+            final List<String> authorityHints =
+                    entityStatement.getParsedPayload() instanceof EntityConfigurationPayload ecp ?
+                            ecp.getAuthorityHints() : null;
             if (authorityHints == null || authorityHints.isEmpty()) {
                 result.add(chain);
             } else {
                 final List<Pair<EntityStatement, EntityStatement>> authorities = authorityHints.stream()
-                        .map(EntityID::getValue)
                         .filter(id -> preSelectedChain.isEmpty() || preSelectedChain.contains(id))
                         .map(id -> fetchAuthority(entityStatement, id))
                         .filter(pair -> pair != null && pair.getFirst() != null && pair.getSecond() != null)
@@ -350,8 +423,8 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
      */
     @Nullable protected Pair<EntityStatement, EntityStatement> fetchAuthority(@Nonnull final EntityStatement entity,
             @Nullable final String authorityHint) {
-        final String entityId = entity.getEntityID().getValue();
-        if (entityId == null || authorityHint == null) {
+        final String entityId = entity.getSubject();
+        if (authorityHint == null) {
             log.error("Entity ID ({}) or authority hint ({}) may not be null", entityId, authorityHint);
             return null;
         }
@@ -375,8 +448,9 @@ public class DefaultTrustChainFetchingStrategy extends AbstractIdentifiableIniti
      * @param statements list of entity statements
      * @return first entity statemenet, or null the list was empty.
      */
-    @Nullable private EntityStatement getFirstIfFound(@Nonnull final List<EntityStatement> statements) {
-        return statements.size() > 0 ? statements.get(0) : null;
+    @Nullable private EntityStatement getFirstIfFound(
+            @Nonnull final List<? extends BaseExpirableEntityStatementContainer> statements) {
+        return statements.size() > 0 ? statements.get(0).getEntityStatement() : null;
     }
 
 }
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AbstractTrustChainResolutionAction.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AbstractTrustChainResolutionAction.java
index 028a362..18ce7a0 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AbstractTrustChainResolutionAction.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AbstractTrustChainResolutionAction.java
@@ -30,9 +30,8 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
 import org.slf4j.Logger;
 
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-
 import net.minidev.json.JSONObject;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.policy.FederationMetadataPolicyHelper;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.context.RelyingPartyTrustChainContext;
 import net.shibboleth.idp.profile.AbstractProfileAction;
@@ -63,7 +62,7 @@ public class AbstractTrustChainResolutionAction extends AbstractProfileAction {
     @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextCreationStrategy;
 
     /** Strategy used to get combined entity metadata from trust chain. */
-    @Nonnull private Function<List<EntityStatement>,Map<String,Map<String,Object>>> metadataLookupStrategy;
+    @NonnullAfterInit private Function<List<EntityStatement>,Map<String,Map<String,Object>>> metadataLookupStrategy;
 
     /** Strategy used to merge metadata policies in trust chain for specific entity type. */
     @NonnullAfterInit private BiFunction<List<EntityStatement>,String,Map<String, MetadataPolicy>>
@@ -130,7 +129,8 @@ public class AbstractTrustChainResolutionAction extends AbstractProfileAction {
      * 
      * @return lookup strategy
      */
-    @Nonnull public Function<List<EntityStatement>,Map<String,Map<String,Object>>> getMetadataLookupStrategy() {
+    @NonnullAfterInit
+    public Function<List<EntityStatement>,Map<String,Map<String,Object>>> getMetadataLookupStrategy() {
         checkComponentActive();
         return metadataLookupStrategy;
     }
@@ -216,6 +216,9 @@ public class AbstractTrustChainResolutionAction extends AbstractProfileAction {
     protected void doInitialize() throws ComponentInitializationException {
         super.doInitialize();
 
+        if (metadataLookupStrategy == null) {
+            throw new ComponentInitializationException("MetadataLookupStrategy cannot be null");
+        }
         if (metadataPolicyMergingStrategy == null) {
             throw new ComponentInitializationException("MetadataPolicyMergingStrategy cannot be null");
         }
@@ -269,7 +272,7 @@ public class AbstractTrustChainResolutionAction extends AbstractProfileAction {
                     log.debug("{} Policy-enforced metadata {}", getLogPrefix(), requestMetadata.toJSONString());
                 } else {
                     log.warn("{} Metadata validation failed for {} for entity type {}", getLogPrefix(),
-                            chain.get(0).getEntityID(), entityType);
+                            chain.get(0).getSubject(), entityType);
                 }
             }
             policyCompliantChains.add(new Pair<>(chain, verifiedMetadata));
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AddExtensionsToClientInformation.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AddExtensionsToClientInformation.java
index 485b0e5..f65565d 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AddExtensionsToClientInformation.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/AddExtensionsToClientInformation.java
@@ -29,10 +29,10 @@ import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
 import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
 import org.slf4j.Logger;
 
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
 
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCClientRegistrationResponseContext;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.context.RelyingPartyTrustChainContext;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.support.ClientInformationExtensionSupport;
 import net.shibboleth.idp.profile.AbstractProfileAction;
@@ -151,10 +151,10 @@ public class AddExtensionsToClientInformation extends AbstractProfileAction {
 
         clientInformation.getOIDCMetadata().setCustomField(
                 ClientInformationExtensionSupport.KEY_VALIDATED_TRUST_ANCHOR,
-                trustChain.get(trustChain.size() - 1).getEntityID().getValue());
+                trustChain.get(trustChain.size() - 1).getSubject());
         clientInformation.getOIDCMetadata().setCustomField(
                 ClientInformationExtensionSupport.KEY_VALIDATED_TRUST_CHAIN,
-                trustChain.stream().map(es -> es.getSignedStatement().serialize()).toList());
+                trustChain.stream().map(es -> es.getJwt().serialize()).toList());
         final Map<String,List<String>> verifiedTrustMarks = trustChainContext.getVerifiedTrustMarkIds();
         if (verifiedTrustMarks != null) {
             clientInformation.getOIDCMetadata().setCustomField(
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildResolveEntityResponse.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildResolveEntityResponse.java
index 5471c4e..73f365a 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildResolveEntityResponse.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/BuildResolveEntityResponse.java
@@ -36,8 +36,8 @@ import org.slf4j.Logger;
 
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.SignedJWT;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.context.RelyingPartyTrustChainContext;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultEntityTypesLookupFunction;
 import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
@@ -154,7 +154,7 @@ public class BuildResolveEntityResponse extends AbstractBuildEntityStatementActi
             return false;
         }
         builder.claim("trust_chain", trustChain.stream()
-                .map(statement -> statement.getSignedStatement().serialize())
+                .map(statement -> statement.getJwt().serialize())
                 .toList());
 
         final Instant expirationTime = resolveTrustChainExpiration(trustChain);
@@ -165,7 +165,7 @@ public class BuildResolveEntityResponse extends AbstractBuildEntityStatementActi
         }
         builder.expirationTime(Date.from(expirationTime));
 
-        final String entityId = trustChain.get(0).getEntityID().getValue();
+        final String entityId = trustChain.get(0).getSubject();
         assert entityId != null;
         final Map<String, String> trustMarks = buildTrustMarks(entityId, trustChainContext.getVerifiedTrustMarks());
         if (trustMarks != null && !trustMarks.isEmpty()) {
@@ -217,7 +217,7 @@ public class BuildResolveEntityResponse extends AbstractBuildEntityStatementActi
     @Nullable private Instant resolveTrustChainExpiration(@Nonnull final List<EntityStatement> trustChain) {
         Instant metadataExpiration = null;
         for (final EntityStatement statement : trustChain) {
-            final Instant statementExpiration = statement.getClaimsSet().getExpirationTime().toInstant();
+            final Instant statementExpiration = statement.getParsedPayload().getExpiration();
             metadataExpiration = metadataExpiration == null ? statementExpiration : 
                 statementExpiration.isBefore(metadataExpiration) ? statementExpiration : metadataExpiration;
         }
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/CallResolveEntityApi.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/CallResolveEntityApi.java
index c4f123b..a4539c8 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/CallResolveEntityApi.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/CallResolveEntityApi.java
@@ -39,20 +39,22 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
 import org.slf4j.Logger;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.nimbusds.jwt.SignedJWT;
 import com.nimbusds.oauth2.sdk.id.ClientID;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 
 import net.shibboleth.idp.authn.AuthnEventIds;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityRequest;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityResponse;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatementHelper;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ResolveEntityRequestCriterion;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ResolveEntityResponseContainer;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ResponseContainerExpirationCriterion;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityIDCriterion;
-import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityStatementCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.RemoteEntityConfigurationContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.SubjectEntityStatementCriterion;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.TrustedRemoteResolverEntity;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.context.RelyingPartyTrustChainContext;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultPreSelectedTrustChainIDsLookupStrategy;
@@ -92,7 +94,7 @@ public class CallResolveEntityApi extends AbstractProfileAction {
     @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextCreationStrategy;
 
     /** Cache used to fetch the issuer entity configuration from. */
-    @NonnullAfterInit private MetadataCache<EntityStatement> entityConfigurationCache;
+    @NonnullAfterInit private MetadataCache<RemoteEntityConfigurationContainer> entityConfigurationCache;
 
     /** Cache containing responses from resolve entity APIs. */
     @NonnullAfterInit private MetadataCache<ResolveEntityResponseContainer> resolveEntityTrustChainMetadataCache;
@@ -165,7 +167,7 @@ public class CallResolveEntityApi extends AbstractProfileAction {
      * 
      * @param cache cache used to fetch the issuer entity configuration from
      */
-    public void setEntityConfigurationCache(@Nonnull final MetadataCache<EntityStatement> cache) {
+    public void setEntityConfigurationCache(@Nonnull final MetadataCache<RemoteEntityConfigurationContainer> cache) {
         checkSetterPreconditions();
         entityConfigurationCache = Constraint.isNotNull(cache, "Entity Configuration cache cannot be null");
     }
@@ -373,8 +375,25 @@ public class CallResolveEntityApi extends AbstractProfileAction {
                 trustChainContext.setAttemptedTrustedRemoteResolverEntities(
                         CollectionSupport.copyToList(alreadyAttemptedEntities));
             }
-            final URI uri = EntityStatementHelper.fetchEndpointUriFromFederationEntity(entityConfigurationCache,
-                    trustedEntity.getEntityId(), "federation_resolve_endpoint", objectMapper);
+            final CriteriaSet criteria = new CriteriaSet(new SubjectEntityIDCriterion(trustedEntity.getEntityId()),
+                    new ResponseContainerExpirationCriterion(Instant.now().plus(cachedLifetime)));
+
+            final List<RemoteEntityConfigurationContainer> configurationCacheResult;
+            try {
+                configurationCacheResult = entityConfigurationCache.get(criteria);
+            } catch (final MetadataCacheException e) {
+                continue;
+            }
+            if (configurationCacheResult.isEmpty()) {
+                log.warn("{} Could not resolve entity configuration for {}", getLogPrefix(), trustedEntity);
+                continue;
+            }
+            final URI uri = Optional.ofNullable(configurationCacheResult.get(0).getEntityStatement())
+                        .map(statement -> statement.getParsedPayload().getMetadata())
+                        .map(metadata -> metadata.get("federation_entity"))
+                        .map(entityMetadata -> entityMetadata.get("federation_resolve_endpoint"))
+                        .map(endpoint -> URI.create((String) endpoint))
+                        .orElse(null);
             if (uri == null) {
                 log.warn("{} Could not fetch federation resolve endpoint for {}", getLogPrefix(), trustedEntity);
                 continue;
@@ -415,7 +434,18 @@ public class CallResolveEntityApi extends AbstractProfileAction {
                 }
                 final List<EntityStatement> chain = rawTrustChain.stream()
                         .filter(Objects::nonNull)
-                        .map(entry -> EntityStatementHelper.deserializeEntityStatement(entry))
+                        .map(entry -> {
+                            try {
+                                final SignedJWT jwt = SignedJWT.parse(entry);
+                                assert jwt != null;
+                                assert objectMapper != null;
+                                return EntityStatement.parse(jwt, objectMapper);
+                            } catch (final JsonProcessingException | ParseException e) {
+                                log.warn("Could not parse the trust chain into list of entity statements", e);
+                                return null;
+                            }
+                        })
+                        .filter(Objects::nonNull)
                         .toList();
                 if (!preSelectedChain.isEmpty() && !preSelectedChain.equals(trustChainIDsLookupStrategy.apply(chain))) {
                     log.debug("{} Ignored resolved trust chain that doesn't match with preselected chain",
@@ -444,7 +474,7 @@ public class CallResolveEntityApi extends AbstractProfileAction {
                         .toList();
                 if (trustMarks != null) {
                     final Map<String, List<SignedJWT>> trustMarksByEntity = new HashMap<>();
-                    for (final String entity : chain.stream().map(entity -> entity.getEntityID().getValue()).toList()) {
+                    for (final String entity : chain.stream().map(entity -> entity.getSubject()).toList()) {
                         final List<SignedJWT> trustMarksForEntity = trustMarks.stream()
                                 .filter(jwt -> {
                                     try {
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/DefaultTrustMarkFromMetadataCacheFetchingFunction.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/DefaultTrustMarkFromMetadataCacheFetchingFunction.java
index 90bd729..47ca881 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/DefaultTrustMarkFromMetadataCacheFetchingFunction.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/DefaultTrustMarkFromMetadataCacheFetchingFunction.java
@@ -28,7 +28,6 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 
 import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.TrustMarkRequest;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.TrustMarkResponse;
@@ -36,6 +35,7 @@ import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatementHelper;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ResponseContainerExpirationCriterion;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.TrustMarkRequestCriterion;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.TrustMarkResponseContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.RemoteEntityConfigurationContainer;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.TrustedRemoteEntity;
 import net.shibboleth.oidc.metadata.cache.MetadataCache;
 import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
@@ -58,7 +58,7 @@ public class DefaultTrustMarkFromMetadataCacheFetchingFunction extends AbstractI
     @Nonnull private Logger log = LoggerFactory.getLogger(DefaultTrustMarkFromMetadataCacheFetchingFunction.class);
 
     /** Cache used to fetch the issuer entity configuration from. */
-    @NonnullAfterInit private MetadataCache<EntityStatement> entityConfigurationCache;
+    @NonnullAfterInit private MetadataCache<RemoteEntityConfigurationContainer> entityConfigurationCache;
 
     /** Cache containing responses from Trust Mark APIs. */
     @NonnullAfterInit private MetadataCache<TrustMarkResponseContainer> trustMarkCache;
@@ -90,7 +90,7 @@ public class DefaultTrustMarkFromMetadataCacheFetchingFunction extends AbstractI
      * 
      * @param cache cache used to fetch the issuer entity configuration from
      */
-    public void setEntityConfigurationCache(@Nonnull final MetadataCache<EntityStatement> cache) {
+    public void setEntityConfigurationCache(@Nonnull final MetadataCache<RemoteEntityConfigurationContainer> cache) {
         checkSetterPreconditions();
         entityConfigurationCache = Constraint.isNotNull(cache, "Entity Configuration cache cannot be null");
     }
@@ -174,7 +174,7 @@ public class DefaultTrustMarkFromMetadataCacheFetchingFunction extends AbstractI
         checkComponentActive();
         final String entityId = trustedEntity.getEntityId();
         final URI uri = EntityStatementHelper.fetchEndpointUriFromFederationEntity(entityConfigurationCache,
-                entityId, "trust_mark_endpoint", objectMapper);
+                entityId, "trust_mark_endpoint", Instant.now().plusSeconds(300)); //TODO
         if (uri == null) {
             log.warn("Could not fetch trust mark endpoint for {}", entityId);
             return null;
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ResolveTrustChains.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ResolveTrustChains.java
index e45a880..ca1f133 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ResolveTrustChains.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ResolveTrustChains.java
@@ -31,11 +31,13 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 
 import com.nimbusds.oauth2.sdk.id.ClientID;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 
 import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityConfiguration;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityIDCriterion;
-import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityStatementCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.RemoteTrustChainsContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.SubjectEntityStatementCriterion;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.context.RelyingPartyTrustChainContext;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultPreSelectedTrustChainIDsLookupStrategy;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultTrustChainIDsLookupStrategy;
@@ -68,7 +70,7 @@ public class ResolveTrustChains extends AbstractTrustChainResolutionAction {
     @Nonnull private Logger log = LoggerFactory.getLogger(ResolveTrustChains.class);
 
     /** Metadata cache for trust chains. */
-    @NonnullAfterInit private MetadataCache<List<List<EntityStatement>>> trustChainCache;
+    @NonnullAfterInit private MetadataCache<RemoteTrustChainsContainer> trustChainCache;
 
     /** Strategy used to obtain the client id value of the request. */
     @NonnullAfterInit private Function<MessageContext, ClientID> clientIDLookupStrategy;
@@ -80,7 +82,7 @@ public class ResolveTrustChains extends AbstractTrustChainResolutionAction {
     @Nonnull private Function<List<EntityStatement>, List<String>> trustChainIDsLookupStrategy;
 
     /** Strategy used to fetch entity configuration delivered to the trust chain cache. */
-    @Nonnull private Function<ProfileRequestContext, EntityStatement> entityConfigurationLookupStrategy;
+    @Nonnull private Function<ProfileRequestContext, EntityConfiguration> entityConfigurationLookupStrategy;
 
     /** Condition to require entity configuration via {@link #entityConfigurationLookupStrategy}. */
     @Nonnull private Predicate<ProfileRequestContext> requireEntityConfigurationCondition;
@@ -104,7 +106,7 @@ public class ResolveTrustChains extends AbstractTrustChainResolutionAction {
      * 
      * @param cache metadata cache
      */
-    public void setTrustChainCache(@Nonnull final MetadataCache<List<List<EntityStatement>>> cache) {
+    public void setTrustChainCache(@Nonnull final MetadataCache<RemoteTrustChainsContainer> cache) {
         checkSetterPreconditions();
         trustChainCache = Constraint.isNotNull(cache, "TrustChainCache cannot be null");
     }
@@ -148,7 +150,7 @@ public class ResolveTrustChains extends AbstractTrustChainResolutionAction {
      * @param strategy lookup strategy
      */
     public void setEntityConfigurationLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext, EntityStatement> strategy) {
+            @Nonnull final Function<ProfileRequestContext, EntityConfiguration> strategy) {
         checkSetterPreconditions();
         entityConfigurationLookupStrategy = Constraint.isNotNull(strategy,
                 "EntityConfigurationLookupStrategy cannot be null");
@@ -215,7 +217,7 @@ public class ResolveTrustChains extends AbstractTrustChainResolutionAction {
             ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_CREDENTIALS);
             return;
         }
-        final List<List<List<EntityStatement>>> cacheResult;
+        final List<RemoteTrustChainsContainer> cacheResult;
         try {
             cacheResult = trustChainCache.get(criteriaSet);
         } catch (final MetadataCacheException e) {
@@ -234,12 +236,12 @@ public class ResolveTrustChains extends AbstractTrustChainResolutionAction {
 
         final RelyingPartyTrustChainContext trustChainContext =
                 getTrustChainContextCreationStrategy().apply(profileRequestContext);
-        trustChainContext.setResolvedTrustChains(cacheResult.get(0));
+        trustChainContext.setResolvedTrustChains(cacheResult.get(0).getTrustChains());
         final List<Pair<List<EntityStatement>, Map<String,Map<String,Object>>>> policyCompliantChains =
                 new ArrayList<>();
 
         String errorEventId = null;
-        for (final List<EntityStatement> chain : cacheResult.get(0)) {
+        for (final List<EntityStatement> chain : cacheResult.get(0).getTrustChains()) {
             assert chain != null;
             if (!preSelectedChain.isEmpty() && !preSelectedChain.equals(trustChainIDsLookupStrategy.apply(chain))) {
                 log.debug("{} Ignored resolved trust chain that doesn't match with preselected chain", getLogPrefix());
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 a0cc180..3a69a4a 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
@@ -38,11 +38,12 @@ import org.slf4j.Logger;
 
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.SignedJWT;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityIDCriterion;
-import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityStatementCriterion;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.TrustMarkOwnersCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.RemoteTrustChainsContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.SubjectEntityStatementCriterion;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.context.RelyingPartyTrustChainContext;
 import net.shibboleth.idp.profile.AbstractProfileAction;
 import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
@@ -91,7 +92,7 @@ public class ResolveTrustMarks extends AbstractProfileAction {
     @Nonnull private Predicate<ProfileRequestContext> trustedTrustMarkIssuersOnlyCondition;
 
     /** Metadata cache for trust chains (for trust mark issuers). */
-    @NonnullAfterInit private MetadataCache<List<List<EntityStatement>>> trustChainCache;
+    @NonnullAfterInit private MetadataCache<RemoteTrustChainsContainer> trustChainCache;
 
     /** Trust engine used to validate a trust mark signature. */
     @NonnullAfterInit private TrustEngine<SignedJWT> trustEngine;
@@ -194,7 +195,7 @@ public class ResolveTrustMarks extends AbstractProfileAction {
      * 
      * @param cache metadata cache
      */
-    public void setTrustChainCache(@Nonnull final MetadataCache<List<List<EntityStatement>>> cache) {
+    public void setTrustChainCache(@Nonnull final MetadataCache<RemoteTrustChainsContainer> cache) {
         checkSetterPreconditions();
         trustChainCache = Constraint.isNotNull(cache, "TrustChainCache cannot be null");
     }
@@ -358,12 +359,12 @@ public class ResolveTrustMarks extends AbstractProfileAction {
         
         final Map<String, List<SignedJWT>> verifiedTrustMarks = new HashMap<>();
         for (final EntityStatement statement : selectedTrustChain) {
-            final List<SignedJWT> trustMarks = chainTrustMarks.get(statement.getEntityID().getValue());
+            final List<SignedJWT> trustMarks = chainTrustMarks.get(statement.getSubject());
             if (trustMarks == null || trustMarks.isEmpty()) {
                 break;
             }
             verifiedTrustMarks.put(
-                    statement.getEntityID().getValue(),
+                    statement.getSubject(),
                     trustMarks.stream()
                         .filter(entry -> onlyTrustedIssuers ?
                                 checkTrustedIssuer(entry, trustedIssuers, trustedOwners) : true)
@@ -474,22 +475,22 @@ public class ResolveTrustMarks extends AbstractProfileAction {
         final String issuer = trustMarkClaims.getIssuer();
         assert issuer != null;
         log.debug("{} Resolving trust chain for {}", getLogPrefix(), issuer);
-        final List<List<List<EntityStatement>>> cacheResult;
+        final List<RemoteTrustChainsContainer> cacheResult;
         try {
             cacheResult = trustChainCache.get(new CriteriaSet(new SubjectEntityIDCriterion(issuer)));
         } catch (final MetadataCacheException e) {
             log.warn("{} Exception while fetching trust chains for {}", getLogPrefix(), issuer, e);
             return false;
         }
-        if (cacheResult.isEmpty() || cacheResult.get(0).isEmpty()) {
+        if (cacheResult.isEmpty() || cacheResult.get(0).getTrustChains().isEmpty()) {
             log.warn("{} No trust chains resolved for {}", getLogPrefix(), issuer);
             return false;
         }
-        final List<EntityStatement> trustMarkChain = cacheResult.get(0).get(0);
+        final List<EntityStatement> trustMarkChain = cacheResult.get(0).getTrustChains().get(0);
         final EntityStatement trustMarkIssuer = trustMarkChain.get(0);
         assert trustMarkIssuer != null;
         final CriteriaSet criteria = new CriteriaSet(new SubjectEntityStatementCriterion(trustMarkIssuer));
-        log.trace("{} Validating entity statement {}", getLogPrefix(), trustMarkIssuer.getSignedStatement().serialize());
+        log.trace("{} Validating entity statement {}", getLogPrefix(), trustMarkIssuer.getJwt().serialize());
         try {
             if (trustEngine.validate(jwt, criteria)) {
                 final String id = getTrustMarkId(jwt);
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/SelectTrustChain.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/SelectTrustChain.java
index f9868f6..8a6a26e 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/SelectTrustChain.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/SelectTrustChain.java
@@ -27,8 +27,7 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
 import org.slf4j.Logger;
 
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.context.RelyingPartyTrustChainContext;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultTrustChainSelectionStrategy;
 import net.shibboleth.idp.profile.AbstractProfileAction;
@@ -168,7 +167,7 @@ public class SelectTrustChain extends AbstractProfileAction {
         final RelyingPartyContext relyingPartyContext =
                 relyingPartyContextCreationStrategy.apply(profileRequestContext);
         relyingPartyContext.setRelyingPartyId(
-                selectedTrustChain.get(selectedTrustChain.size() - 1).getEntityID().getValue());
+                selectedTrustChain.get(selectedTrustChain.size() - 1).getSubject());
         relyingPartyContext.setVerified(true);
     }
 }
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/StoreAutomaticRegistration.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/StoreAutomaticRegistration.java
index fe5529b..40ae86a 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/StoreAutomaticRegistration.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/StoreAutomaticRegistration.java
@@ -29,10 +29,10 @@ import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
 import org.slf4j.Logger;
 
 import com.nimbusds.oauth2.sdk.id.ClientID;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
 
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.context.RelyingPartyTrustChainContext;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultSelectedTrustChainMetadataLookupStrategy;
 import net.shibboleth.idp.profile.AbstractProfileAction;
@@ -160,7 +160,7 @@ public class StoreAutomaticRegistration extends AbstractProfileAction {
         assert selectedTrustChain != null;
 
         clientInformation = new OIDCClientInformation(
-                new ClientID(selectedTrustChain.getFirst().get(0).getEntityID().getValue()), metadata);
+                new ClientID(selectedTrustChain.getFirst().get(0).getSubject()), metadata);
 
         expiration = trustChainContext.getSelectedMetadataExpiration();
         if (expiration == null) {
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateAutomaticRegistrationProfileConfiguration.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateAutomaticRegistrationProfileConfiguration.java
index cd61365..262b4eb 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateAutomaticRegistrationProfileConfiguration.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateAutomaticRegistrationProfileConfiguration.java
@@ -30,10 +30,10 @@ import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
 import org.slf4j.Logger;
 
 import com.nimbusds.oauth2.sdk.id.ClientID;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
 
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.navigate.LocalMetadataPolicyLookupFunction;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.config.navigate.MandatoryTrustMarksLookupFunction;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.context.RelyingPartyTrustChainContext;
@@ -206,7 +206,7 @@ public class ValidateAutomaticRegistrationProfileConfiguration extends AbstractP
             log.debug("{} Applying local metadata policy into the client metadata", getLogPrefix());
             clientInformation = localMetadataPolicyMergingStrategy.apply(
                     new OIDCClientInformation(
-                            new ClientID(selectedTrustChain.getFirst().get(0).getEntityID().getValue()),
+                            new ClientID(selectedTrustChain.getFirst().get(0).getSubject()),
                             metadata), localMetadataPolicy);
             if (clientInformation == null) {
                 log.error("{} Could not apply the local metadata policy into the client metadata", getLogPrefix());
@@ -216,7 +216,7 @@ public class ValidateAutomaticRegistrationProfileConfiguration extends AbstractP
             selectedTrustChain.setSecond(Map.of("openid_relying_party", clientInformation.toJSONObject()));
         } else {
             clientInformation = new OIDCClientInformation(
-                    new ClientID(selectedTrustChain.getFirst().get(0).getEntityID().getValue()), metadata);
+                    new ClientID(selectedTrustChain.getFirst().get(0).getSubject()), metadata);
 
         }
         final String clientId = clientInformation.getID().getValue();
@@ -248,10 +248,10 @@ public class ValidateAutomaticRegistrationProfileConfiguration extends AbstractP
         final OIDCMetadataContext oidcCtx = new OIDCMetadataContext();
         clientInformation.getOIDCMetadata().setCustomField(
                 ClientInformationExtensionSupport.KEY_VALIDATED_TRUST_ANCHOR,
-                trustChain.get(trustChain.size() - 1).getEntityID().getValue());
+                trustChain.get(trustChain.size() - 1).getSubject());
         clientInformation.getOIDCMetadata().setCustomField(
                 ClientInformationExtensionSupport.KEY_VALIDATED_TRUST_CHAIN,
-                trustChain.stream().map(es -> es.getSignedStatement().serialize()).toList());
+                trustChain.stream().map(es -> es.getJwt().serialize()).toList());
         if (verifiedTrustMarks != null) {
             clientInformation.getOIDCMetadata().setCustomField(
                     ClientInformationExtensionSupport.KEY_VALIDATED_TRUST_MARK_IDS, verifiedTrustMarks);
@@ -271,7 +271,7 @@ public class ValidateAutomaticRegistrationProfileConfiguration extends AbstractP
     @Nullable private Instant resolveTrustChainExpiration(@Nonnull final List<EntityStatement> trustChain) {
         Instant metadataExpiration = null;
         for (final EntityStatement statement : trustChain) {
-            final Instant statementExpiration = statement.getClaimsSet().getExpirationTime().toInstant();
+            final Instant statementExpiration = statement.getParsedPayload().getExpiration();
             metadataExpiration = metadataExpiration == null ? statementExpiration : 
                 statementExpiration.isBefore(metadataExpiration) ? statementExpiration : metadataExpiration;
         }
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateProvidedEntityConfiguration.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateProvidedEntityConfiguration.java
index 0065b06..d791eac 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateProvidedEntityConfiguration.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateProvidedEntityConfiguration.java
@@ -23,9 +23,8 @@ import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-
 import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 import net.shibboleth.idp.profile.AbstractProfileAction;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateProvidedTrustChain.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateProvidedTrustChain.java
index 3b60cc9..e606bb0 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateProvidedTrustChain.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateProvidedTrustChain.java
@@ -26,8 +26,7 @@ import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.context.RelyingPartyTrustChainContext;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateSelectedTrustChain.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateSelectedTrustChain.java
index dea9e2d..40139e3 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateSelectedTrustChain.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ValidateSelectedTrustChain.java
@@ -26,10 +26,10 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
 import org.slf4j.Logger;
 
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-
 import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ResolveEntityRequest;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityIDCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.RemoteEntityConfigurationContainer;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.context.RelyingPartyTrustChainContext;
 import net.shibboleth.idp.profile.AbstractProfileAction;
 import net.shibboleth.oidc.metadata.cache.MetadataCache;
@@ -64,7 +64,7 @@ public class ValidateSelectedTrustChain extends AbstractProfileAction {
     @Nonnull private Logger log = LoggerFactory.getLogger(ValidateSelectedTrustChain.class);
 
     /** Metadata cache for entity configurations. */
-    @NonnullAfterInit private MetadataCache<EntityStatement> entityConfigurationCache;
+    @NonnullAfterInit private MetadataCache<RemoteEntityConfigurationContainer> entityConfigurationCache;
 
     /** Strategy used to lookup the trust chain context. */
     @Nonnull private Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextLookupStrategy;
@@ -97,7 +97,7 @@ public class ValidateSelectedTrustChain extends AbstractProfileAction {
      * 
      * @param cache What to set.
      */
-    public void setEntityConfigurationCache(@Nonnull final MetadataCache<EntityStatement> cache) {
+    public void setEntityConfigurationCache(@Nonnull final MetadataCache<RemoteEntityConfigurationContainer> cache) {
         checkSetterPreconditions();
         entityConfigurationCache = Constraint.isNotNull(cache, "Entity configuration metadata cache cannot be null");
     }
@@ -191,7 +191,7 @@ public class ValidateSelectedTrustChain extends AbstractProfileAction {
         final List<String> trustAnchors = validatedRequest.getTrustAnchors();
         final List<EntityStatement> candidateChain = selectedTrustChain.getFirst();
         assert candidateChain != null;
-        final String candidateAnchor = candidateChain.get(candidateChain.size() - 1).getEntityID().getValue();
+        final String candidateAnchor = candidateChain.get(candidateChain.size() - 1).getSubject();
         if (!trustAnchors.contains(candidateAnchor)) {
             log.debug("{} Selected trust chain candidate has unrequested trust anchor {}", getLogPrefix(),
                     candidateAnchor);
@@ -218,8 +218,9 @@ public class ValidateSelectedTrustChain extends AbstractProfileAction {
     protected boolean isSubjectValid(@Nonnull final String subject) {
         final SubjectEntityIDCriterion subjectCriterion = new SubjectEntityIDCriterion(subject);
         try {
-            final List<EntityStatement> result = entityConfigurationCache.get(new CriteriaSet(subjectCriterion));
-            if (result.size() == 1) {
+            final List<RemoteEntityConfigurationContainer> result =
+                    entityConfigurationCache.get(new CriteriaSet(subjectCriterion));
+            if (result.size() == 1 && result.get(0).getEntityStatement() != null) {
                 return true;
             }
         } catch (final MetadataCacheException e) {
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultSelectedTrustChainIDsLookupStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultSelectedTrustChainIDsLookupStrategy.java
index 5d42418..743904b 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultSelectedTrustChainIDsLookupStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultSelectedTrustChainIDsLookupStrategy.java
@@ -21,8 +21,7 @@ import java.util.function.Function;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.context.RelyingPartyTrustChainContext;
 import net.shibboleth.shared.logic.Constraint;
 
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultSelectedTrustChainImmediateSuperiorLookupStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultSelectedTrustChainImmediateSuperiorLookupStrategy.java
index 9df4df3..31e953c 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultSelectedTrustChainImmediateSuperiorLookupStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultSelectedTrustChainImmediateSuperiorLookupStrategy.java
@@ -35,8 +35,7 @@ public class DefaultSelectedTrustChainImmediateSuperiorLookupStrategy
                 .map(pair -> pair.getFirst())
                 .filter(list -> list.size() >= 3)
                 .map(list -> list.get(1))
-                .map(entityStatement -> entityStatement.getClaimsSet().getIssuer())
-                .map(issuer -> issuer.getValue())
+                .map(entityStatement -> entityStatement.getIssuer())
                 .orElse(null);
     }
 }
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultSelectedTrustChainTrustAnchorLookupStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultSelectedTrustChainTrustAnchorLookupStrategy.java
index 233f70b..6ac7900 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultSelectedTrustChainTrustAnchorLookupStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultSelectedTrustChainTrustAnchorLookupStrategy.java
@@ -35,7 +35,7 @@ public class DefaultSelectedTrustChainTrustAnchorLookupStrategy
                 .map(pair -> pair.getFirst())
                 .filter(list -> list.size() >= 3)
                 .map(list -> list.get(list.size() - 1))
-                .map(entityStatement -> entityStatement.getEntityID().getValue())
+                .map(entityStatement -> entityStatement.getSubject())
                 .orElse(null);
     }
 }
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultTrustChainIDsLookupStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultTrustChainIDsLookupStrategy.java
index 7af8e2b..5c6302d 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultTrustChainIDsLookupStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultTrustChainIDsLookupStrategy.java
@@ -21,7 +21,7 @@ import java.util.stream.IntStream;
 
 import javax.annotation.Nullable;
 
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 
 /**
  * Default strategy for looking up the entity IDs of a trust chain.
@@ -35,7 +35,7 @@ public class DefaultTrustChainIDsLookupStrategy implements Function<List<EntityS
                 .filter(chain -> chain != null && !chain.isEmpty())
                 .map(chain -> IntStream.range(0, chain.size())
                         .mapToObj(i -> chain.get(i))
-                        .map(statement -> statement.getEntityID().getValue())
+                        .map(statement -> statement.getSubject())
                         .distinct()
                         .toList())
                 .orElse(null);
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultTrustChainMetadataPolicyMergingStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultTrustChainMetadataPolicyMergingStrategy.java
index 3477420..4c02322 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultTrustChainMetadataPolicyMergingStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultTrustChainMetadataPolicyMergingStrategy.java
@@ -29,8 +29,9 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.JavaType;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.type.MapType;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubordinateStatementPayload;
 import net.shibboleth.oidc.metadata.policy.MetadataPolicy;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.collection.CollectionSupport;
@@ -123,17 +124,19 @@ public class DefaultTrustChainMetadataPolicyMergingStrategy extends AbstractIden
         final Map<String, MetadataPolicy> result = new HashMap<>();
         for (int i = trustChain.size(); i > 0; i--) {
             final EntityStatement entityStatement = trustChain.get(i - 1);
-            final Map<String, Object> policy = entityStatement.getClaimsSet().getMetadataPolicyJSONObject();
-            if (policy == null || policy.isEmpty()) {
-                continue;
-            }
+            if (entityStatement.getParsedPayload() instanceof SubordinateStatementPayload ssp) {
+                final Map<String, Map<String, MetadataPolicy>> policy = ssp.getMetadataPolicy();
+                if (policy == null || policy.isEmpty()) {
+                    continue;
+                }
 
-            if (StringSupport.trimOrNull(entityType) != null) {
-                try {
-                    assert entityType != null;
-                    result.putAll(mergePolicies(result, parseMetadataPolicy(entityStatement, entityType)));
-                } catch (final ConstraintViolationException e) {
-                    throw new ConstraintViolationException("Could not merge federation metadata policies");
+                if (StringSupport.trimOrNull(entityType) != null) {
+                    try {
+                        assert entityType != null;
+                        result.putAll(mergePolicies(result, policy.get(entityType)));
+                    } catch (final ConstraintViolationException e) {
+                        throw new ConstraintViolationException("Could not merge federation metadata policies");
+                    }
                 }
             }
         }
@@ -164,37 +167,4 @@ public class DefaultTrustChainMetadataPolicyMergingStrategy extends AbstractIden
         throw new ConstraintViolationException("Merge failed");
     }
 
-    /**
-     * Parse a map of metadata policies from the given entity statement for a specified entity type.
-     * 
-     * @param entityStatement the source for the map of metadata policies
-     * @param entityType the entity type to use
-     * @return the map of metadata policies for the entity type or null if not found
-     * @throws ConstraintViolationException if the map of policies could not be parsed from the entity statement
-     */
-    @Nullable
-    protected Map<String, MetadataPolicy> parseMetadataPolicy(
-            @Nonnull final EntityStatement entityStatement,
-            @Nonnull final String entityType) throws ConstraintViolationException {
-        final Object metadataPolicyClaim =
-                entityStatement.getClaimsSet().getClaim("metadata_policy");
-        if (metadataPolicyClaim != null) {
-            final JavaType stringType = objectMapper.constructType(String.class);
-            final JavaType metadataPolicyType = objectMapper.constructType(MetadataPolicy.class);
-            final MapType metadataPolicyMapType =
-                    objectMapper.getTypeFactory().constructMapType(Map.class, stringType, metadataPolicyType);
-            final MapType metadataPolicyByEntityTypeMapType =
-                    objectMapper.getTypeFactory().constructMapType(Map.class, stringType, metadataPolicyMapType);
-            try {
-                final Map<String, Map<String, MetadataPolicy>> result =
-                        objectMapper.readValue(metadataPolicyClaim.toString(), metadataPolicyByEntityTypeMapType);
-                if (result != null) {
-                    return result.get(entityType);
-                }
-            } catch (final JsonProcessingException e) {
-                log.warn("Could not parse trust mark issuers from the trust chain", e);
-            }
-        }
-        return null;
-    }
 }
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultTrustChainSelectionStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultTrustChainSelectionStrategy.java
index d636b34..5ac234f 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultTrustChainSelectionStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultTrustChainSelectionStrategy.java
@@ -22,8 +22,7 @@ import javax.annotation.Nullable;
 
 import org.slf4j.Logger;
 
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.context.RelyingPartyTrustChainContext;
 import net.shibboleth.shared.collection.Pair;
 import net.shibboleth.shared.primitive.LoggerFactory;
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/ExplicitClientRegistrationRequestClientIDLookupFunction.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/ExplicitClientRegistrationRequestClientIDLookupFunction.java
index f0d8d71..4e788c8 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/ExplicitClientRegistrationRequestClientIDLookupFunction.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/ExplicitClientRegistrationRequestClientIDLookupFunction.java
@@ -24,9 +24,9 @@ import org.opensaml.messaging.context.MessageContext;
 import org.opensaml.messaging.context.navigate.ContextDataLookupFunction;
 
 import com.nimbusds.oauth2.sdk.id.ClientID;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 
 import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ExplicitClientRegistrationRequest;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 
 /**
  * A function that returns client from the entity configuration in the explicit registration request.
@@ -58,9 +58,9 @@ public class ExplicitClientRegistrationRequestClientIDLookupFunction
         }
         final EntityStatement entityConfiguration = request.getEntityConfiguration();
         if (entityConfiguration != null) {
-            return new ClientID(entityConfiguration.getEntityID());
+            return new ClientID(entityConfiguration.getSubject());
         }
         final List<EntityStatement> trustChain = request.getTrustChain();
-        return trustChain != null && !trustChain.isEmpty() ? new ClientID(trustChain.get(0).getEntityID()) : null;
+        return trustChain != null && !trustChain.isEmpty() ? new ClientID(trustChain.get(0).getSubject()) : null;
     }
 }
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/ExplicitClientRegistrationRequestEntityConfigurationLookupFunction.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/ExplicitClientRegistrationRequestEntityConfigurationLookupFunction.java
index 6136dae..8a5ae35 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/ExplicitClientRegistrationRequestEntityConfigurationLookupFunction.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/ExplicitClientRegistrationRequestEntityConfigurationLookupFunction.java
@@ -22,9 +22,8 @@ import javax.annotation.concurrent.ThreadSafe;
 import org.opensaml.messaging.context.navigate.ContextDataLookupFunction;
 import org.opensaml.profile.context.ProfileRequestContext;
 
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-
 import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ExplicitClientRegistrationRequest;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 
 /**
  * A function that returns entity configuration set in the explicit registration request.
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/ExplicitClientRegistrationRequestJWKSetLookupFunction.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/ExplicitClientRegistrationRequestJWKSetLookupFunction.java
index 69c4572..245b880 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/ExplicitClientRegistrationRequestJWKSetLookupFunction.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/ExplicitClientRegistrationRequestJWKSetLookupFunction.java
@@ -24,9 +24,9 @@ import org.opensaml.messaging.context.navigate.ContextDataLookupFunction;
 import org.opensaml.profile.context.ProfileRequestContext;
 
 import com.nimbusds.jose.jwk.JWKSet;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 
 import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ExplicitClientRegistrationRequest;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 
 /**
  * A function that returns {@link JWKSet} from the entity configuration in the explicit registration request.
@@ -59,9 +59,9 @@ public class ExplicitClientRegistrationRequestJWKSetLookupFunction
         }
         final EntityStatement entityConfiguration = request.getEntityConfiguration();
         if (entityConfiguration != null) {
-            return entityConfiguration.getClaimsSet().getJWKSet();
+            return entityConfiguration.getParsedPayload().getJwks();
         }
         final List<EntityStatement> trustChain = request.getTrustChain();
-        return trustChain != null && !trustChain.isEmpty() ? trustChain.get(0).getClaimsSet().getJWKSet() : null;
+        return trustChain != null && !trustChain.isEmpty() ? trustChain.get(0).getParsedPayload().getJwks() : null;
     }
 }
\ No newline at end of file
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/ExplicitClientRegistrationRequestTrustChainLookupFunction.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/ExplicitClientRegistrationRequestTrustChainLookupFunction.java
index e49daad..a5aa0a0 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/ExplicitClientRegistrationRequestTrustChainLookupFunction.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/ExplicitClientRegistrationRequestTrustChainLookupFunction.java
@@ -23,9 +23,8 @@ import javax.annotation.concurrent.ThreadSafe;
 import org.opensaml.messaging.context.navigate.ContextDataLookupFunction;
 import org.opensaml.profile.context.ProfileRequestContext;
 
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
-
 import net.shibboleth.idp.plugin.oidc.op.oidfed.messaging.impl.ExplicitClientRegistrationRequest;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 
 /**
  * A function that returns pushed trust chain set in the explicit registration request.
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityConfigurationCredentialResolver.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/DefaultEntityConfigurationCredentialResolver.java
similarity index 89%
rename from idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityConfigurationCredentialResolver.java
rename to idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/DefaultEntityConfigurationCredentialResolver.java
index a5f0f24..eaa33eb 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultEntityConfigurationCredentialResolver.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/DefaultEntityConfigurationCredentialResolver.java
@@ -12,7 +12,7 @@
  * limitations under the License.
  */
 
-package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
+package net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential;
 
 import java.util.Objects;
 
@@ -23,8 +23,9 @@ import org.opensaml.security.credential.Credential;
 import org.slf4j.Logger;
 
 import com.nimbusds.jose.jwk.JWKSet;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.SubjectEntityStatementCriterion;
 import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
 import net.shibboleth.shared.primitive.LoggerFactory;
 import net.shibboleth.shared.resolver.CriteriaSet;
@@ -71,9 +72,9 @@ public class DefaultEntityConfigurationCredentialResolver extends BasicJOSEObjec
         }
         final JWKSet jwks;
         final EntityStatement subjectStatement = subjectCriterion.getValue();
-        if (subjectStatement.getEntityID().getValue().equals(
-                subjectStatement.getClaimsSet().getIssuer().getValue())) {
-                    jwks = subjectStatement.getClaimsSet().getJWKSet();;
+        if (subjectStatement.getSubject().equals(
+                subjectStatement.getIssuer())) {
+                    jwks = subjectStatement.getParsedPayload().getJwks();
         } else {
             throw new ResolverException(
                     "Unexpected contents in the SubjectEntityStatementCriterion: subject does not match issuer");
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultLocalTrustAnchorCredentialResolver.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/DefaultLocalTrustAnchorCredentialResolver.java
similarity index 93%
rename from idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultLocalTrustAnchorCredentialResolver.java
rename to idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/DefaultLocalTrustAnchorCredentialResolver.java
index c663131..aedce1c 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultLocalTrustAnchorCredentialResolver.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/DefaultLocalTrustAnchorCredentialResolver.java
@@ -12,7 +12,7 @@
  * limitations under the License.
  */
 
-package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
+package net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -27,6 +27,8 @@ import org.slf4j.Logger;
 import com.nimbusds.jose.jwk.JWK;
 import com.nimbusds.jose.jwk.JWKSet;
 
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.LocalKeyContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.SubjectEntityStatementCriterion;
 import net.shibboleth.oidc.metadata.cache.MetadataCache;
 import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
 import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
@@ -71,7 +73,7 @@ public class DefaultLocalTrustAnchorCredentialResolver extends BasicJOSEObjectCr
             throw new ResolverException(
                     "Credential criteria set did not contain an instance of SubjectEntityStatementCriterion");
         }
-        final String entityId = subjectCriterion.getValue().getEntityID().getValue();
+        final String entityId = subjectCriterion.getValue().getSubject();
         log.debug("Attempting to find trusted keys for {}", entityId);
         
         final List<Map<String, LocalKeyContainer>> keyContainers;
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultPayloadJOSEObjectCredentialResolver.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/DefaultPayloadJOSEObjectCredentialResolver.java
similarity index 97%
rename from idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultPayloadJOSEObjectCredentialResolver.java
rename to idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/DefaultPayloadJOSEObjectCredentialResolver.java
index cba6fd1..8852db9 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultPayloadJOSEObjectCredentialResolver.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/DefaultPayloadJOSEObjectCredentialResolver.java
@@ -12,7 +12,7 @@
  * limitations under the License.
  */
 
-package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
+package net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential;
 
 import java.text.ParseException;
 import java.util.Map;
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/security/credential/DefaultPayloadSignatureValidationCredentialResolver.java
similarity index 94%
rename from idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultPayloadSignatureValidationCredentialResolver.java
rename to idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/DefaultPayloadSignatureValidationCredentialResolver.java
index af1ad36..a4b4f41 100644
--- 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/security/credential/DefaultPayloadSignatureValidationCredentialResolver.java
@@ -12,7 +12,7 @@
  * limitations under the License.
  */
 
-package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
+package net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential;
 
 import java.text.ParseException;
 import java.util.Map;
@@ -27,6 +27,7 @@ import org.slf4j.Logger;
 import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jwt.SignedJWT;
 
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SignatureValidationKeyContainerJwtCriterion;
 import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
 import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.primitive.LoggerFactory;
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultSubordinateStatementCredentialResolver.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/DefaultSubordinateStatementCredentialResolver.java
similarity index 86%
rename from idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultSubordinateStatementCredentialResolver.java
rename to idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/DefaultSubordinateStatementCredentialResolver.java
index 6231528..827cbef 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultSubordinateStatementCredentialResolver.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/DefaultSubordinateStatementCredentialResolver.java
@@ -12,7 +12,7 @@
  * limitations under the License.
  */
 
-package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
+package net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential;
 
 import java.util.Objects;
 
@@ -23,8 +23,10 @@ import org.opensaml.security.credential.Credential;
 import org.slf4j.Logger;
 
 import com.nimbusds.jose.jwk.JWKSet;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 
+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.SubjectEntityStatementCriterion;
 import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
 import net.shibboleth.shared.primitive.LoggerFactory;
 import net.shibboleth.shared.resolver.CriteriaSet;
@@ -71,8 +73,7 @@ public class DefaultSubordinateStatementCredentialResolver extends BasicJOSEObje
         }
         final JWKSet jwks;
         final EntityStatement subjectStatement = subjectCriterion.getValue();
-        if (subjectStatement.getEntityID().getValue().equals(
-                subjectStatement.getClaimsSet().getIssuer().getValue())) {
+        if (subjectStatement.getSubject().equals(subjectStatement.getIssuer())) {
             throw new ResolverException(
                     "Unexpected contents in the SubjectEntityStatementCriterion: subject matches issuer");
         } else {
@@ -84,15 +85,13 @@ public class DefaultSubordinateStatementCredentialResolver extends BasicJOSEObje
                         "Credential criteria set did not contain an instance of IssuerEntityStatementCriterion");
             }
             final EntityStatement issuerStatement = issuerCriterion.getValue();
-            if (!issuerStatement.getEntityID().getValue().equals(
-                    subjectStatement.getClaimsSet().getIssuer().getValue())) {
+            if (!issuerStatement.getSubject().equals(subjectStatement.getIssuer())) {
                 throw new ResolverException("Credential criteria do not match for subject and issuer");
             }
-            if (!issuerStatement.getEntityID().getValue().equals(
-                    issuerStatement.getClaimsSet().getIssuer().getValue())) {
+            if (!issuerStatement.getSubject().equals(issuerStatement.getIssuer())) {
                 throw new ResolverException("Issuer entity statement is not self signed");
             }
-            jwks = issuerStatement.getClaimsSet().getJWKSet();
+            jwks = issuerStatement.getParsedPayload().getJwks();
         }
 
         if (jwks == null || jwks.isEmpty()) {
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustMarkOwnerCredentialResolver.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/DefaultTrustMarkOwnerCredentialResolver.java
similarity index 94%
rename from idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustMarkOwnerCredentialResolver.java
rename to idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/DefaultTrustMarkOwnerCredentialResolver.java
index 14c9d9c..cbd258a 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustMarkOwnerCredentialResolver.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/DefaultTrustMarkOwnerCredentialResolver.java
@@ -12,7 +12,7 @@
  * limitations under the License.
  */
 
-package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
+package net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential;
 
 import java.text.ParseException;
 import java.util.ArrayList;
@@ -29,6 +29,8 @@ import org.slf4j.Logger;
 import com.nimbusds.jose.jwk.JWK;
 import com.nimbusds.jose.jwk.JWKSet;
 
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityIDCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.TrustMarkOwnersCriterion;
 import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
 import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.primitive.LoggerFactory;
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 5fde339..3dd4b7f 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
@@ -52,28 +52,28 @@
     <bean id="shibboleth.oidfed.EntityConfigurationMetadataCache" parent="shibboleth.oidc.CacheBuilder">
         <constructor-arg>
             <bean p:cacheId="DefaultEntityConfigurationMetadataCache" parent="shibboleth.oidfed.EntityConfigurationMetadataCacheBuilderSpec"
-                p:cleanupTaskInterval="PT30S"/>
+                p:cleanupTaskInterval="%{idp.oidfed.cache.entityConfiguration.cleanupTaskInterval:PT5M}"/>
         </constructor-arg>
     </bean>
 
     <bean id="shibboleth.oidfed.SubordinateEntityStatementMetadataCache" parent="shibboleth.oidc.CacheBuilder">
         <constructor-arg>
             <bean p:cacheId="DefaultSubordinateEntityStarementMetadataCache" parent="shibboleth.oidfed.SubordinateEntityStatementMetadataCacheBuilderSpec"
-                p:cleanupTaskInterval="PT30S"/>
+                p:cleanupTaskInterval="%{idp.oidfed.cache.subordinateStatement.cleanupTaskInterval:PT5M}"/>
         </constructor-arg>
     </bean>
 
     <bean id="shibboleth.oidfed.TrustChainMetadataCache" parent="shibboleth.oidc.CacheBuilder">
         <constructor-arg>
             <bean p:cacheId="DefaultTrustChainMetadataCache" parent="shibboleth.oidfed.TrustChainMetadataCacheBuilderSpec"
-                p:cleanupTaskInterval="PT30S"/>
+                p:cleanupTaskInterval="%{idp.oidfed.cache.trustChain.cleanupTaskInterval:PT5M}"/>
         </constructor-arg>
     </bean>
 
     <bean id="shibboleth.oidfed.ResolveEntityTrustChainMetadataCache" parent="shibboleth.oidc.CacheBuilder">
         <constructor-arg>
             <bean p:cacheId="DefaultResolveEntityTrustChainMetadataCache" parent="shibboleth.oidfed.ResolveEntityTrustChainMetadataCacheBuilderSpec"
-                p:cleanupTaskInterval="PT30S"/>
+                p:cleanupTaskInterval="%{idp.oidfed.cache.resolveEntity.cleanupTaskInterval:PT5M}"/>
         </constructor-arg>
     </bean>
 
@@ -83,35 +83,53 @@
         </constructor-arg>
     </bean>
 
+    <bean id="DefaultResponseContainerExpirationTimeStrategy"
+        class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.DefaultResponseContainerExpirationTimeStrategy"/>
+
     <bean id="shibboleth.oidfed.EntityConfigurationMetadataCacheBuilderSpec"
         class="net.shibboleth.oidc.metadata.cache.impl.DynamicMetadataCacheBuilderSpec"
         p:criteriaToIdentifierStrategy-ref="shibboleth.oidfed.DefaultSubjectEntityIDCriteriaToIdentifierStrategy"
-        p:metadataExpirationTimeStrategy-ref="shibboleth.oidfed.DefaultEntityStatementExpirationTimeStrategy"
-        p:minCacheDuration="%{idp.oidfed.entityConfiguration.maxRefreshDelay:PT1S}"
-        p:maxCacheDuration="%{idp.oidfed.entityConfiguration.maxRefreshDelay:PT30S}">
+        p:minCacheDuration="%{idp.oidfed.cache.entityConfiguration.minRefreshDelay:PT1M}"
+        p:maxCacheDuration="%{idp.oidfed.cache.entityConfiguration.maxRefreshDelay:PT30M}"
+        p:metadataExpirationTimeStrategy-ref="DefaultResponseContainerExpirationTimeStrategy">
         <property name="identifierExtractionStrategy">
-            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultEntityStatementSubjectIdentifierExtractionStrategy"/>
+            <bean parent="shibboleth.Functions.Expression" c:expression="#input?.getEntityId()"/>
         </property>
         <property name="fetchStrategy">
-            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultEntityStatementFetchingStrategy"
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ec.DefaultEntityConfigurationFetchingStrategy"
                 p:httpClient="#{getObject('shibboleth.oidfed.HttpClient') ?: getObject('shibboleth.InternalHttpClient')}"
-                p:httpClientSecurityParameters="#{getObject('shibboleth.oidfed.NonBrowser.HttpClientSecurityParameters')}">
-                <property name="criteriaToEndpointStrategy">
-                    <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultEntityConfigurationEndpointExtractionStrategy"
-                        p:criteriaToEntityIdStrategy-ref="shibboleth.oidfed.DefaultSubjectEntityIDCriteriaToIdentifierStrategy"/>
+                p:httpClientSecurityParameters="#{getObject('shibboleth.oidfed.NonBrowser.HttpClientSecurityParameters')}"
+                p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper">
+                <property name="criteriaToValidContainerLifetimeStrategy">
+                    <bean parent="shibboleth.Functions.Constant">
+                        <constructor-arg>
+                            <bean class="java.time.Duration" factory-method="parse" c:_0="%{idp.oidfed.cache.entityConfiguration.validContainerLifetime:PT5M}" />
+                        </constructor-arg>
+                    </bean>
+                </property>
+                <property name="criteriaToInvalidContainerLifetimeStrategy">
+                    <bean parent="shibboleth.Functions.Constant">
+                        <constructor-arg>
+                            <bean class="java.time.Duration" factory-method="parse" c:_0="%{idp.oidfed.cache.entityConfiguration.invalidContainerLifetime:PT1M}" />
+                        </constructor-arg>
+                    </bean>
                 </property>
             </bean>
         </property>
         <property name="metadataFilterStrategy">
-            <bean parent="shibboleth.BiFunctions.Compose">
-                <constructor-arg name="f">
-                    <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultEntityStatementSignatureValidationFilterStrategy"
-                        p:trustEngine-ref="shibboleth.oidfed.DefaultEntityConfigurationTrustEngine"/>
-                </constructor-arg>
-                <constructor-arg name="g">
-                    <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultEntityStatementContentValidationFilterStrategy"
-                        p:claimsValidator-ref="%{idp.oidfed.entityConfiguration.claimsValidator:shibboleth.oidfed.DefaultEntityConfigurationClaimsValidator}"/>
-                </constructor-arg>
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.DefaultEntityStatementMetadataFilterStrategy">
+                <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.DefaultEntityConfigurationTrustEngine">
+                            <property name="criteriaSetLookupStrategy">
+                                <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ec.DefaultEntityConfigurationCriteriaSetLookupFunction"/>
+                            </property>
+                        </bean>
+                        <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ec.DefaultEntityConfigurationContentValidationCondition"
+                            p:claimsValidator-ref="%{idp.oidfed.entityConfiguration.claimsValidator:shibboleth.oidfed.DefaultEntityConfigurationClaimsValidator}"/>
+                    </util:list>
+                </property>
             </bean>
         </property>
     </bean>
@@ -149,20 +167,20 @@
     <bean id="shibboleth.oidfed.DefaultEntityConfigurationTrustEngine"
         class="net.shibboleth.oidc.security.impl.ExplicitKeySignedJWTTrustEngine">
         <constructor-arg index="0">
-            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultEntityConfigurationCredentialResolver" />
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential.DefaultEntityConfigurationCredentialResolver" />
         </constructor-arg>
         <constructor-arg index="1">
-            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultPayloadJOSEObjectCredentialResolver" />
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential.DefaultPayloadJOSEObjectCredentialResolver" />
         </constructor-arg>
     </bean>
 
     <bean id="shibboleth.oidfed.DefaultSubordinateStatementTrustEngine"
         class="net.shibboleth.oidc.security.impl.ExplicitKeySignedJWTTrustEngine">
         <constructor-arg index="0">
-            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultSubordinateStatementCredentialResolver" />
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential.DefaultSubordinateStatementCredentialResolver" />
         </constructor-arg>
         <constructor-arg index="1">
-            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultPayloadJOSEObjectCredentialResolver" />
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential.DefaultPayloadJOSEObjectCredentialResolver" />
         </constructor-arg>
     </bean>
 
@@ -174,14 +192,14 @@
 
     <bean id="shibboleth.oidfed.SubordinateEntityStatementMetadataCacheBuilderSpec"
         class="net.shibboleth.oidc.metadata.cache.impl.DynamicMetadataCacheBuilderSpec"
-        p:metadataExpirationTimeStrategy-ref="shibboleth.oidfed.DefaultEntityStatementExpirationTimeStrategy"
-        p:minCacheDuration="%{idp.oidfed.entityConfiguration.maxRefreshDelay:PT1S}"
-        p:maxCacheDuration="%{idp.oidfed.entityConfiguration.maxRefreshDelay:PT30S}">
+        p:minCacheDuration="%{idp.oidfed.cache.subordinateStatement.minRefreshDelay:PT1M}"
+        p:maxCacheDuration="%{idp.oidfed.cache.subordinateStatement.maxRefreshDelay:PT30M}"
+        p:metadataExpirationTimeStrategy-ref="DefaultResponseContainerExpirationTimeStrategy">
         <property name="identifierExtractionStrategy">
-            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultSubordinateStatementIdentifierExtractionStrategy"/>
+            <bean parent="shibboleth.Functions.Expression" c:expression="#input?.getIdentifier()"/>
         </property>
         <property name="criteriaToIdentifierStrategy">
-            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultSubordinateStatementCriteriaToIdentifierStrategy"
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ss.DefaultSubordinateStatementCriteriaToIdentifierStrategy"
                 p:criteriaToSubjectEntityIdStrategy-ref="shibboleth.oidfed.DefaultSubjectEntityIDCriteriaToIdentifierStrategy">
                 <property name="criteriaToIssuerEntityIdStrategy">
                     <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultIssuerEntityIDCriteriaToIdentifierStrategy"/>
@@ -189,47 +207,92 @@
             </bean>
         </property>
         <property name="metadataFilterStrategy">
-            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultSubordinateStatementSignatureValidationFilterStrategy"
-                p:entityConfigurationCache-ref="shibboleth.oidfed.EntityConfigurationMetadataCache"
-                p:trustEngine-ref="shibboleth.oidfed.DefaultSubordinateStatementTrustEngine"/>
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.DefaultEntityStatementMetadataFilterStrategy">
+                <property name="validationConditions">
+                    <util:list value-type="java.util.function.BiPredicate">
+                        <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ss.DefaultSubordinateStatementSignatureValidationCondition"
+                            p:trustEngine-ref="shibboleth.oidfed.DefaultSubordinateStatementTrustEngine"
+                            p:entityConfigurationCache-ref="shibboleth.oidfed.EntityConfigurationMetadataCache"/>
+                        <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ec.DefaultEntityConfigurationContentValidationCondition"
+                            p:claimsValidator-ref="%{idp.oidfed.subordinateStatement.claimsValidator:shibboleth.oidfed.DefaultSubordinateStatementClaimsValidator}"/>
+                    </util:list>
+                </property>
+            </bean>
         </property>
         <property name="fetchStrategy">
-            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultEntityStatementFetchingStrategy"
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ss.DefaultSubordinateStatementFetchingStrategy"
                 p:httpClient="#{getObject('shibboleth.oidfed.HttpClient') ?: getObject('shibboleth.InternalHttpClient')}"
-                p:httpClientSecurityParameters="#{getObject('shibboleth.oidfed.NonBrowser.HttpClientSecurityParameters')}">
-                <property name="criteriaToEndpointStrategy">
-                    <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultSubordinateStatementEndpointExtractionStrategy"
-                        p:criteriaToSubjectEntityIdStrategy-ref="shibboleth.oidfed.DefaultSubjectEntityIDCriteriaToIdentifierStrategy"
-                        p:entityConfigurationCache-ref="shibboleth.oidfed.EntityConfigurationMetadataCache">
-                        <property name="criteriaToIssuerEntityIdStrategy">
-                            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultIssuerEntityIDCriteriaToIdentifierStrategy"/>
-                        </property>
+                p:httpClientSecurityParameters="#{getObject('shibboleth.oidfed.NonBrowser.HttpClientSecurityParameters')}"
+                p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper"
+                p:entityConfigurationCache-ref="shibboleth.oidfed.EntityConfigurationMetadataCache">
+                <property name="criteriaToValidContainerLifetimeStrategy">
+                    <bean parent="shibboleth.Functions.Constant">
+                        <constructor-arg>
+                            <bean class="java.time.Duration" factory-method="parse" c:_0="%{idp.oidfed.cache.subordinateStatement.validContainerLifetime:PT5M}" />
+                        </constructor-arg>
+                    </bean>
+                </property>
+                <property name="criteriaToInvalidContainerLifetimeStrategy">
+                    <bean parent="shibboleth.Functions.Constant">
+                        <constructor-arg>
+                            <bean class="java.time.Duration" factory-method="parse" c:_0="%{idp.oidfed.cache.subordinateStatement.invalidContainerLifetime:PT1M}" />
+                        </constructor-arg>
                     </bean>
                 </property>
             </bean>
         </property>
     </bean>
 
+    <bean id="shibboleth.oidfed.DefaultSubordinateStatementClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator">
+        <property name="claimValidators">
+            <util:list value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
+                <bean class="net.shibboleth.oidc.security.jwt.claims.impl.IssuedAtClaimsValidator"
+                    p:clockSkew="%{idp.policy.clockSkew:PT1M}"
+                    p:messageLifetime="%{idp.policy.messageLifetime:PT1M}"
+                    p:requiredRule="true" />
+                <bean class="net.shibboleth.oidc.security.jwt.claims.impl.RequiredClaimsValidator"
+                    p:requiredClaims="iss" />
+                <bean class="net.shibboleth.oidc.security.jwt.claims.impl.RequiredClaimsValidator"
+                    p:requiredClaims="sub" />
+                <bean class="net.shibboleth.oidc.security.jwt.claims.impl.RequiredClaimsValidator"
+                    p:requiredClaims="jwks" />
+                <bean class="net.shibboleth.oidc.security.jwt.claims.impl.ExpiryClaimsValidator"
+                    p:clockSkew="%{idp.policy.clockSkew:PT1M}" />
+                <bean class="net.shibboleth.oidc.security.jwt.claims.impl.ProhibitedClaimsValidator"
+                    p:prohibitedClaims="authority_hints" />
+                <bean class="net.shibboleth.oidc.security.jwt.claims.impl.ProhibitedClaimsValidator"
+                    p:prohibitedClaims="trust_anchor_hints" />
+                <bean class="net.shibboleth.oidc.security.jwt.claims.impl.ProhibitedClaimsValidator"
+                    p:prohibitedClaims="trust_marks" />
+                <bean class="net.shibboleth.oidc.security.jwt.claims.impl.ProhibitedClaimsValidator"
+                    p:prohibitedClaims="trust_mark_issuers" />
+                <bean class="net.shibboleth.oidc.security.jwt.claims.impl.ProhibitedClaimsValidator"
+                    p:prohibitedClaims="trust_mark_owners" />
+                <bean class="net.shibboleth.oidc.security.jwt.claims.impl.ProhibitedClaimsValidator"
+                    p:prohibitedClaims="aud" />
+            </util:list>
+        </property>
+    </bean>
+
     <bean id="shibboleth.oidfed.TrustChainMetadataCacheBuilderSpec"
         class="net.shibboleth.oidc.metadata.cache.impl.DynamicMetadataCacheBuilderSpec"
         p:criteriaToIdentifierStrategy-ref="shibboleth.oidfed.DefaultSubjectEntityIDCriteriaToIdentifierStrategy"
-        p:minCacheDuration="%{idp.oidfed.entityConfiguration.maxRefreshDelay:PT1S}"
-        p:maxCacheDuration="%{idp.oidfed.entityConfiguration.maxRefreshDelay:PT30S}">
+        p:minCacheDuration="%{idp.oidfed.trustChain.minRefreshDelay:PT1M}"
+        p:maxCacheDuration="%{idp.oidfed.trustChain.maxRefreshDelay:PT30M}"
+        p:metadataExpirationTimeStrategy-ref="DefaultResponseContainerExpirationTimeStrategy">
         <property name="identifierExtractionStrategy">
-            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainSubjectIdentifierExtractionStrategy" />
-        </property>
-        <property name="metadataExpirationTimeStrategy">
-            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainExpirationTimeStrategy"/>
+            <bean parent="shibboleth.Functions.Expression" c:expression="#input?.getEntityId()"/>
         </property>
         <property name="metadataFilterStrategy">
-            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainSignatureValidationFilterStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.tc.DefaultRemoteTrustChainSignatureValidationFilterStrategy">
                 <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" />
+                            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential.DefaultPayloadSignatureValidationCredentialResolver" />
                         </constructor-arg>
                         <constructor-arg index="1">
-                            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultPayloadJOSEObjectCredentialResolver" />
+                            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential.DefaultPayloadJOSEObjectCredentialResolver" />
                         </constructor-arg>
                     </bean>
                 </property>
@@ -238,11 +301,11 @@
                         <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"
+                                    <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential.DefaultLocalTrustAnchorCredentialResolver"
                                         c:cache-ref="shibboleth.oidfed.LocalTrustAnchorsMetadataCache" />
                                  </constructor-arg>
                                  <constructor-arg index="1">
-                                     <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultPayloadJOSEObjectCredentialResolver" />
+                                     <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential.DefaultPayloadJOSEObjectCredentialResolver" />
                                  </constructor-arg>
                             </bean>
                         </property>
@@ -251,13 +314,28 @@
             </bean>
         </property>
         <property name="fetchStrategy">
-            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainFetchingStrategy"
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.tc.DefaultTrustChainFetchingStrategy"
                 p:criteriaToSubjectEntityIdStrategy-ref="shibboleth.oidfed.DefaultSubjectEntityIDCriteriaToIdentifierStrategy"
                 p:entityConfigurationCache-ref="shibboleth.oidfed.EntityConfigurationMetadataCache"
                 p:subordinateStatementCache-ref="shibboleth.oidfed.SubordinateEntityStatementMetadataCache"
                 p:localTrustAnchorsCache-ref="shibboleth.oidfed.LocalTrustAnchorsMetadataCache"
                 p:federationPolicyConstraints-ref="%{idp.oidfed.FederationPolicyConstraints:shibboleth.oidfed.DefaultFederationPolicyConstraints}"
-                p:objectMapper-ref="shibboleth.oidfed.policy.JSONObjectMapper"/>
+                p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper">
+                <property name="criteriaToValidContainerLifetimeStrategy">
+                    <bean parent="shibboleth.Functions.Constant">
+                        <constructor-arg>
+                            <bean class="java.time.Duration" factory-method="parse" c:_0="%{idp.oidfed.cache.trustChain.validContainerLifetime:PT5M}" />
+                        </constructor-arg>
+                    </bean>
+                </property>
+                <property name="criteriaToInvalidContainerLifetimeStrategy">
+                    <bean parent="shibboleth.Functions.Constant">
+                        <constructor-arg>
+                            <bean class="java.time.Duration" factory-method="parse" c:_0="%{idp.oidfed.cache.trustChain.invalidContainerLifetime:PT1M}" />
+                        </constructor-arg>
+                    </bean>
+                </property>
+            </bean>
         </property>
     </bean>
 
@@ -280,7 +358,7 @@
                 <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.DefaultEntityConfigurationCredentialResolver" />
+                            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential.DefaultEntityConfigurationCredentialResolver" />
                         </constructor-arg>
                         <constructor-arg index="1">
                             <bean class="net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver" />
@@ -293,7 +371,7 @@
             <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultResolveEntityTrustChainFetchingStrategy"
                 p:httpClient="#{getObject('shibboleth.oidfed.HttpClient') ?: getObject('shibboleth.InternalHttpClient')}"
                 p:httpClientSecurityParameters="#{getObject('shibboleth.oidfed.NonBrowser.HttpClientSecurityParameters')}"
-                p:objectMapper-ref="shibboleth.oidfed.policy.JSONObjectMapper"/>
+                p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper"/>
         </property>
     </bean>
 
@@ -307,7 +385,7 @@
         </entry>
         <entry key="allowed_entity_types">
             <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.constraints.impl.DefaultAllowedEntityTypesConstraint"
-                p:objectMapper-ref="shibboleth.oidfed.policy.JSONObjectMapper"/>
+                p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper"/>
         </entry>
     </util:map>
 
@@ -362,7 +440,7 @@
             <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustMarkStatusFetchingStrategy"
                 p:httpClient="#{getObject('shibboleth.oidfed.HttpClient') ?: getObject('shibboleth.InternalHttpClient')}"
                 p:httpClientSecurityParameters="#{getObject('shibboleth.oidfed.NonBrowser.HttpClientSecurityParameters')}"
-                p:objectMapper-ref="shibboleth.oidfed.policy.JSONObjectMapper"/>
+                p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper"/>
         </property>
     </bean>
 
@@ -392,7 +470,7 @@
             <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustMarkFetchingStrategy"
                 p:httpClient="#{getObject('shibboleth.oidfed.HttpClient') ?: getObject('shibboleth.InternalHttpClient')}"
                 p:httpClientSecurityParameters="#{getObject('shibboleth.oidfed.NonBrowser.HttpClientSecurityParameters')}"
-                p:objectMapper-ref="shibboleth.oidfed.policy.JSONObjectMapper"/>
+                p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper"/>
         </property>
         <property name="metadataExpirationTimeStrategy">
             <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultNimbusResponseContainerExpirationTimeStrategy"/>
@@ -402,10 +480,10 @@
     <bean id="shibboleth.oidfed.DefaultDelegatedTrustMarkTrustEngine"
         class="net.shibboleth.oidc.security.impl.ExplicitKeySignedJWTTrustEngine">
         <constructor-arg index="0">
-            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustMarkOwnerCredentialResolver" />
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential.DefaultTrustMarkOwnerCredentialResolver" />
         </constructor-arg>
         <constructor-arg index="1">
-            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultPayloadJOSEObjectCredentialResolver" />
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential.DefaultPayloadJOSEObjectCredentialResolver" />
         </constructor-arg>
     </bean>
 
@@ -419,8 +497,7 @@
         parent="shibboleth.Functions.Constant" c:target="#{getObject('shibboleth.oidfed.DefaultLocalTrustAnchorsFilename') ?: 'undefined'}" />
 
 
-    <bean id="shibboleth.oidfed.policy.JSONObjectMapper" parent="shibboleth.oidc.JSONObjectMapper" />
-
+    <bean id="shibboleth.oidfed.JWTPayloadJSONObjectMapper" parent="shibboleth.oidc.JSONObjectMapper" />
     <bean id="shibboleth.oidfed.policy.JSONSimpleModule" class="com.fasterxml.jackson.databind.module.SimpleModule"/>
 
     <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
@@ -436,7 +513,29 @@
     </bean>
 
     <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
-        <property name="targetObject" ref="shibboleth.oidfed.policy.JSONObjectMapper" />
+        <property name="targetObject" ref="shibboleth.oidfed.policy.JSONSimpleModule" />
+        <property name="targetMethod" value="addDeserializer" />
+        <property name="arguments">
+            <list>
+                <value>#{ T(com.nimbusds.jose.jwk.JWKSet)}</value>
+                <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.jackson.JWKSetDeserializer"/>
+            </list>
+        </property>
+    </bean>
+
+    <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
+        <property name="targetObject" ref="shibboleth.oidfed.policy.JSONSimpleModule" />
+        <property name="targetMethod" value="addDeserializer" />
+        <property name="arguments">
+            <list>
+                <value>#{ T(java.time.Instant)}</value>
+                <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.jackson.InstantDeserializer"/>
+            </list>
+        </property>
+    </bean>
+
+    <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
+        <property name="targetObject" ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper" />
         <property name="targetMethod" value="registerModule" />
         <property name="arguments">
             <list>
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 5f2355b..1d6f6f1 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
@@ -21,7 +21,7 @@
         class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.CallResolveEntityApi"
         p:entityConfigurationCache-ref="shibboleth.oidfed.EntityConfigurationMetadataCache"
         p:resolveEntityTrustChainMetadataCache-ref="shibboleth.oidfed.ResolveEntityTrustChainMetadataCache"
-        p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper"
+        p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper"
         p:trustedEntitiesLookupStrategy="#{getObject('shibboleth.oidfed.TrustedRemoteResolverEntitiesLookupStrategy') ?: getObject('shibboleth.oidfed.DefaultTrustedRemoteResolverEntitiesLookupStrategy')}"
         p:preSelectedTrustChainIdsLookupStrategy="#{getObject('shibboleth.oidfed.PreSelectedTrustChainIDsLookupStrategy') ?: getObject('shibboleth.oidfed.DefaultPreSelectedTrustChainIDsLookupStrategy')}">
         <property name="clientIDLookupStrategy">
@@ -57,7 +57,7 @@
         class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultTrustChainMetadataPolicyMergingStrategy"
         p:metadataPolicyMergingStrategy-ref="#{'%{idp.oidfed.authorize.MetadataPolicMergingyStrategy:MetadataPolicMergingyStrategy}'.trim()}"
         p:localMetadataPolicyStrategy-ref="#{'%{idp.oidfed.authorize.LocalMetadataPolicyStrategy:DefaultLocalMetadataPolicyStrategy}'.trim()}"
-        p:objectMapper-ref="shibboleth.oidfed.policy.JSONObjectMapper"/>
+        p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper"/>
 
     <bean id="MetadataPolicMergingyStrategy"
         class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.policy.impl.DefaultFederationMetadataPolicyMergingStrategy"
@@ -119,8 +119,7 @@
                 p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper" />
         </property>
         <property name="trustedTrustMarkOwnersLookupStrategy">
-            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainTrustedTrustMarkOwnersLookupStrategy"
-                p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper" />
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainTrustedTrustMarkOwnersLookupStrategy"/>
         </property>
     </bean>
 
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 0d156df..c9c63b1 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
@@ -29,7 +29,7 @@
                 scope="prototype"
                 p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
                 p:removeIpAddressFromEndpointUri="%{idp.oidc.logging.removeIpAddressFromProtocolMessage:false}"
-                p:objectMapper-ref="#{'%{idp.oidc.logging.objectMapper:shibboleth.oidc.JSONObjectMapper}'.trim()}"/>
+                p:objectMapper-ref="#{'%{idp.oidc.logging.objectMapper:shibboleth.oidfed.JWTPayloadJSONObjectMapper}'.trim()}"/>
                 <!-- TODO: support custom parsing? p:customRequestParser="#{getObject('%{idp.oidc.requestParser.FederationRegisterRequest:}'.trim())}"/>-->
         </constructor-arg>
     </bean>
@@ -47,16 +47,16 @@
         <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:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper">
                 <property name="trustChainSignatureValidationFilterStrategy">
-                    <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainSignatureValidationFilterStrategy">
+                    <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.tc.DefaultProvidedTrustChainSignatureValidationFilterStrategy">
                         <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" />
+                                    <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential.DefaultPayloadSignatureValidationCredentialResolver" />
                                 </constructor-arg>
                                 <constructor-arg index="1">
-                                    <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultPayloadJOSEObjectCredentialResolver" />
+                                    <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential.DefaultPayloadJOSEObjectCredentialResolver" />
                                 </constructor-arg>
                             </bean>
                         </property>
@@ -65,11 +65,11 @@
                                 <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"
+                                            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential.DefaultLocalTrustAnchorCredentialResolver"
                                                 c:cache-ref="shibboleth.oidfed.LocalTrustAnchorsMetadataCache" />
                                          </constructor-arg>
                                          <constructor-arg index="1">
-                                             <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultPayloadJOSEObjectCredentialResolver" />
+                                             <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential.DefaultPayloadJOSEObjectCredentialResolver" />
                                          </constructor-arg>
                                     </bean>
                                 </property>
@@ -114,7 +114,7 @@
         p:clientIDLookupStrategy-ref="shibboleth.ClientIDLookupStrategy"
         p:entityConfigurationCache-ref="shibboleth.oidfed.EntityConfigurationMetadataCache"
         p:resolveEntityTrustChainMetadataCache-ref="shibboleth.oidfed.ResolveEntityTrustChainMetadataCache"
-        p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper"
+        p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper"
         p:trustedEntitiesLookupStrategy="#{getObject('shibboleth.oidfed.TrustedRemoteResolverEntitiesLookupStrategy') ?: getObject('shibboleth.oidfed.DefaultTrustedRemoteResolverEntitiesLookupStrategy')}"
         p:preSelectedTrustChainIdsLookupStrategy="#{getObject('shibboleth.oidfed.PreSelectedTrustChainIDsLookupStrategy') ?: getObject('shibboleth.oidfed.DefaultPreSelectedTrustChainIDsLookupStrategy')}"
         p:requireEntityConfigurationCondition-ref="shibboleth.Conditions.TRUE"
@@ -143,16 +143,33 @@
         class="net.shibboleth.oidc.metadata.cache.impl.FetchThroughMetadataCacheBuilderSpec"
         p:criteriaToIdentifierStrategy-ref="shibboleth.oidfed.DefaultSubjectEntityIDCriteriaToIdentifierStrategy">
         <property name="identifierExtractionStrategy">
-            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainSubjectIdentifierExtractionStrategy" />
+            <bean parent="shibboleth.Functions.Expression" c:expression="#input?.getEntityId()"/>
         </property>
         <property name="fetchStrategy">
-            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainFetchingStrategy"
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.tc.DefaultTrustChainFetchingStrategy"
                 p:criteriaToSubjectEntityIdStrategy-ref="shibboleth.oidfed.DefaultSubjectEntityIDCriteriaToIdentifierStrategy"
                 p:entityConfigurationCache-ref="shibboleth.oidfed.EntityConfigurationMetadataCache"
                 p:subordinateStatementCache-ref="shibboleth.oidfed.SubordinateEntityStatementMetadataCache"
                 p:localTrustAnchorsCache-ref="shibboleth.oidfed.LocalTrustAnchorsMetadataCache"
                 p:federationPolicyConstraints-ref="%{idp.oidfed.FederationPolicyConstraints:shibboleth.oidfed.DefaultFederationPolicyConstraints}"
-                p:objectMapper-ref="shibboleth.oidfed.policy.JSONObjectMapper" />
+                p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper">
+                <property name="criteriaToValidContainerLifetimeStrategy">
+                    <bean parent="shibboleth.Functions.Constant">
+                        <constructor-arg>
+                            <!-- Doesn't matter for the fetch-through cache -->
+                            <bean class="java.time.Duration" factory-method="parse" c:_0="PT5M" />
+                        </constructor-arg>
+                    </bean>
+                </property>
+                <property name="criteriaToInvalidContainerLifetimeStrategy">
+                    <bean parent="shibboleth.Functions.Constant">
+                        <constructor-arg>
+                            <!-- Doesn't matter for the fetch-through cache -->
+                            <bean class="java.time.Duration" factory-method="parse" c:_0="PT5M" />
+                        </constructor-arg>
+                    </bean>
+                </property>
+            </bean>
         </property>
     </bean>
 
@@ -164,7 +181,7 @@
         class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultTrustChainMetadataPolicyMergingStrategy"
         p:metadataPolicyMergingStrategy-ref="#{'%{idp.oidfed.register.MetadataPolicMergingyStrategy:MetadataPolicMergingyStrategy}'.trim()}"
         p:localMetadataPolicyStrategy-ref="#{'%{idp.oidfed.register.LocalMetadataPolicyStrategy:DefaultLocalMetadataPolicyStrategy}'.trim()}"
-        p:objectMapper-ref="shibboleth.oidfed.policy.JSONObjectMapper"/>
+        p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper"/>
 
     <bean id="MetadataPolicMergingyStrategy"
         class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.policy.impl.DefaultFederationMetadataPolicyMergingStrategy"
@@ -210,8 +227,7 @@
                 p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper" />
         </property>
         <property name="trustedTrustMarkOwnersLookupStrategy">
-            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainTrustedTrustMarkOwnersLookupStrategy"
-                p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper" />
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainTrustedTrustMarkOwnersLookupStrategy" />
         </property>
         <property name="activationCondition">
             <bean parent="shibboleth.Conditions.Expression"
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-beans.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-beans.xml
index 8e6859a..c5a6652 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-beans.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-beans.xml
@@ -99,7 +99,7 @@
         class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.navigate.DefaultTrustChainMetadataPolicyMergingStrategy"
         p:metadataPolicyMergingStrategy-ref="#{'%{idp.oidfed.resolve-entity.MetadataPolicMergingyStrategy:MetadataPolicMergingyStrategy}'.trim()}"
         p:localMetadataPolicyStrategy-ref="#{'%{idp.oidfed.resolve-entity.LocalMetadataPolicyStrategy:DefaultLocalMetadataPolicyStrategy}'.trim()}"
-        p:objectMapper-ref="shibboleth.oidfed.policy.JSONObjectMapper"/>
+        p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper"/>
 
     <bean id="MetadataPolicMergingyStrategy"
         class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.policy.impl.DefaultFederationMetadataPolicyMergingStrategy"
@@ -140,8 +140,7 @@
                 p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper" />
         </property>
         <property name="trustedTrustMarkOwnersLookupStrategy">
-            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainTrustedTrustMarkOwnersLookupStrategy"
-                p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper" />
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainTrustedTrustMarkOwnersLookupStrategy" />
         </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 f248ab5..349941d 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
@@ -186,7 +186,7 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
     }
 
     protected String subordinateStatementUrl(final String fetchEndpoint, final String subject) {
-        return fetchEndpoint + "?sub=" + subject;
+        return fetchEndpoint + "?sub=" + URLEncoder.encode(subject, Charset.forName("UTF-8"));
     }
 
     protected String resolveEntityUrl(final String resolveEndpoint, final String subject,
@@ -380,7 +380,6 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
                 .expirationTime(Date.from(Instant.now().plusSeconds(300)))
                 .claim("jwks", new JWKSet(subjectKey).toJSONObject(true))
                 .claim("metadata", metadata)
-                .claim("authority_hints", new String[] { anchorId })
                 .build();
         final EntityStatement rpConfiguration =
                 TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, trustedAnchorKey, claimsSet);
@@ -395,7 +394,6 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
                 .claim("jwks", new JWKSet(subjetKey).toJSONObject(true))
                 .claim("metadata", Map.of("openid_relying_party", new OIDCClientMetadata().toJSONObject()))
                 .claim("metadata_policy", rpPolicy)
-                .claim("authority_hints", authorityHints)
                 .build();
         final EntityStatement rpConfiguration =
                 TrustChainTestUtil.entityStatement(JWSAlgorithm.RS256, issuerKey, claimsSet);
@@ -403,15 +401,13 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
     }
 
     protected String subordinateStatement(final String issuer, final JWK issuerKey, final JWK subjetKey,
-            final String subjectId, final Map<String, Object> rpPolicy, final Map<String, Object> constraints,
-            final String... authorityHints) {
+            final String subjectId, final Map<String, Object> rpPolicy, final Map<String, Object> constraints) {
         final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().issuer(issuer).subject(subjectId)
                 .issueTime(Date.from(Instant.now()))
                 .expirationTime(Date.from(Instant.now().plusSeconds(300)))
                 .claim("jwks", new JWKSet(subjetKey).toJSONObject(true))
                 .claim("metadata", Map.of("openid_relying_party", new OIDCClientMetadata().toJSONObject()))
                 .claim("metadata_policy", rpPolicy)
-                .claim("authority_hints", authorityHints)
                 .claim("constraints", constraints)
                 .build();
         final EntityStatement rpConfiguration =
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 ec302d1..de05a17 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
@@ -75,6 +75,7 @@ public class UserInfoFlowAutomaticRegistrationTest extends AbstractFederationFlo
         request.addHeader("Authorization", token.toAuthorizationHeader());
         final FlowExecutionResult result = flowExecutor.launchExecution(UserInfoTest.FLOW_ID, null, externalContext);
         final UserInfoSuccessResponse response = parseSuccessResponse(result, UserInfoSuccessResponse.class);
+        Assert.assertNotNull(response, "Expected success response for " + clientId);
         Assert.assertEquals(response.getUserInfo().getSubject().getValue(), subject);
         final UserInfo userInfo = response.getUserInfo();
         Assert.assertNotNull(userInfo);
@@ -222,7 +223,7 @@ public class UserInfoFlowAutomaticRegistrationTest extends AbstractFederationFlo
     }
 
     @Test
-    public void testFaoös_nonMatchingTrustChain() throws Exception {
+    public void testFails_nonMatchingTrustChain() throws Exception {
         final String clientId = uniqueClientId();
         rpConfigureMockHttpClient(clientId);
         final BearerAccessToken token = buildToken(clientId, List.of(clientId, uniqueIntermediateId()));
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/EntityConfigurationMetadataCacheTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/EntityConfigurationMetadataCacheTest.java
index a55de27..42588d3 100644
--- a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/EntityConfigurationMetadataCacheTest.java
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/EntityConfigurationMetadataCacheTest.java
@@ -30,10 +30,11 @@ import org.testng.annotations.Test;
 import com.nimbusds.jose.JWSAlgorithm;
 import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jwt.JWTClaimsSet;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 
 import net.shibboleth.idp.plugin.oidc.op.oidfed.TrustChainTestUtil;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ResponseContainerExpirationCriterion;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityIDCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.RemoteEntityConfigurationContainer;
 import net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.AbstractFederationFlowTest;
 import net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.EntityConfigurationFlowTest;
 import net.shibboleth.oidc.metadata.cache.MetadataCache;
@@ -43,6 +44,7 @@ import net.shibboleth.shared.resolver.CriteriaSet;
 /**
  * Unit tests for the default entity configuration metadata cache.
  */
+ at SuppressWarnings("null")
 public class EntityConfigurationMetadataCacheTest extends AbstractFederationFlowTest {
 
     protected EntityConfigurationMetadataCacheTest() {
@@ -51,7 +53,7 @@ public class EntityConfigurationMetadataCacheTest extends AbstractFederationFlow
 
     @Autowired
     @Qualifier("shibboleth.oidfed.EntityConfigurationMetadataCache")
-    MetadataCache<EntityStatement> entityConfigurationCache;
+    MetadataCache<RemoteEntityConfigurationContainer> entityConfigurationCache;
 
     @Test
     public void testValidEntityConfiguration()
@@ -67,10 +69,12 @@ public class EntityConfigurationMetadataCacheTest extends AbstractFederationFlow
 
         mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
         try {
-            final List<EntityStatement> result =
-                    entityConfigurationCache.get(new CriteriaSet(new SubjectEntityIDCriterion(entityId)));
+            final List<RemoteEntityConfigurationContainer> result =
+                    entityConfigurationCache.get(new CriteriaSet(new SubjectEntityIDCriterion(entityId),
+                            new ResponseContainerExpirationCriterion(Instant.now().plusSeconds(300))));
             Assert.assertNotNull(result);
             Assert.assertEquals(result.size(), 1);
+            Assert.assertNotNull(result.get(0).getEntityStatement());
         } catch (MetadataCacheException e) {
             Assert.fail("Could not resolve entity configuration", e);
         }
@@ -222,8 +226,9 @@ public class EntityConfigurationMetadataCacheTest extends AbstractFederationFlow
 
     protected void assertNoEntityConfiguration(final String entityId) {
         try {
-            final List<EntityStatement> result =
-                    entityConfigurationCache.get(new CriteriaSet(new SubjectEntityIDCriterion(entityId)));
+            final List<RemoteEntityConfigurationContainer> result =
+                    entityConfigurationCache.get(new CriteriaSet(new SubjectEntityIDCriterion(entityId),
+                            new ResponseContainerExpirationCriterion(Instant.now().plusSeconds(300))));
             Assert.assertNotNull(result);
             Assert.assertEquals(result.size(), 0);
         } catch (MetadataCacheException e) {
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/EntityConfigurationMetadataCacheTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/SubordinateStatementMetadataCacheTest.java
similarity index 55%
copy from idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/EntityConfigurationMetadataCacheTest.java
copy to idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/SubordinateStatementMetadataCacheTest.java
index a55de27..d5b2ab9 100644
--- a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/EntityConfigurationMetadataCacheTest.java
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/SubordinateStatementMetadataCacheTest.java
@@ -30,10 +30,12 @@ import org.testng.annotations.Test;
 import com.nimbusds.jose.JWSAlgorithm;
 import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jwt.JWTClaimsSet;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 
 import net.shibboleth.idp.plugin.oidc.op.oidfed.TrustChainTestUtil;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.IssuerEntityIDCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ResponseContainerExpirationCriterion;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityIDCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.RemoteSubordinateStatementContainer;
 import net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.AbstractFederationFlowTest;
 import net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.EntityConfigurationFlowTest;
 import net.shibboleth.oidc.metadata.cache.MetadataCache;
@@ -43,34 +45,40 @@ import net.shibboleth.shared.resolver.CriteriaSet;
 /**
  * Unit tests for the default entity configuration metadata cache.
  */
-public class EntityConfigurationMetadataCacheTest extends AbstractFederationFlowTest {
+ at SuppressWarnings("null")
+public class SubordinateStatementMetadataCacheTest extends AbstractFederationFlowTest {
 
-    protected EntityConfigurationMetadataCacheTest() {
+    protected SubordinateStatementMetadataCacheTest() {
         super(EntityConfigurationFlowTest.FLOW_ID);
     }
 
     @Autowired
-    @Qualifier("shibboleth.oidfed.EntityConfigurationMetadataCache")
-    MetadataCache<EntityStatement> entityConfigurationCache;
+    @Qualifier("shibboleth.oidfed.SubordinateEntityStatementMetadataCache")
+    MetadataCache<RemoteSubordinateStatementContainer> subordinateStatementCache;
 
     @Test
-    public void testValidEntityConfiguration()
+    public void testValidSubordinateStatement()
             throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
         final String entityId = uniqueClientId();
-        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(entityId)
                 .issueTime(Date.from(Instant.now()))
                 .expirationTime(Date.from(Instant.now().plusSeconds(300)))
                 .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
                 .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
-        final String entityConfiguration = TrustChainTestUtil.signedJwt(
-                JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+        final String subordinateStatement = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
 
-        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+                mockResponse(subordinateStatement));
         try {
-            final List<EntityStatement> result =
-                    entityConfigurationCache.get(new CriteriaSet(new SubjectEntityIDCriterion(entityId)));
+            final List<RemoteSubordinateStatementContainer> result =
+                    subordinateStatementCache.get(new CriteriaSet(new SubjectEntityIDCriterion(entityId),
+                            new ResponseContainerExpirationCriterion(Instant.now().plusSeconds(300)), 
+                            new IssuerEntityIDCriterion(anchorId)));
             Assert.assertNotNull(result);
             Assert.assertEquals(result.size(), 1);
+            Assert.assertNotNull(result.get(0).getEntityStatement());
         } catch (MetadataCacheException e) {
             Assert.fail("Could not resolve entity configuration", e);
         }
@@ -80,32 +88,18 @@ public class EntityConfigurationMetadataCacheTest extends AbstractFederationFlow
     public void testSignatureWithNonMathchingKey()
             throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
         final String entityId = uniqueClientId();
-        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(entityId)
                 .issueTime(Date.from(Instant.now()))
                 .expirationTime(Date.from(Instant.now().plusSeconds(300)))
                 .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
                 .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
-        final String entityConfiguration = TrustChainTestUtil.signedJwt(
-                JWSAlgorithm.RS256, anchorKey, "entity-statement+jwt", builder.build()).serialize();
-
-        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
-        assertNoEntityConfiguration(entityId);
-    }
-
-    @Test
-    public void testNonMatchingSubjectVsIssuer()
-            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
-        final String entityId = uniqueClientId();
-        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(uniqueClientId()).subject(entityId)
-                .issueTime(Date.from(Instant.now()))
-                .expirationTime(Date.from(Instant.now().plusSeconds(300)))
-                .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
-                .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
-        final String entityConfiguration = TrustChainTestUtil.signedJwt(
+        final String subordinateStatement = TrustChainTestUtil.signedJwt(
                 JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
 
-        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
-        assertNoEntityConfiguration(entityId);
+        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+                mockResponse(subordinateStatement));
+        assertNoSubordinateStatement(entityId);
     }
 
     @Test
@@ -113,119 +107,137 @@ public class EntityConfigurationMetadataCacheTest extends AbstractFederationFlow
             throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
         final String entityId = uniqueClientId();
         final String claimsEntityId = uniqueClientId();
-        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(claimsEntityId).subject(claimsEntityId)
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(claimsEntityId)
                 .issueTime(Date.from(Instant.now()))
                 .expirationTime(Date.from(Instant.now().plusSeconds(300)))
                 .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
                 .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
-        final String entityConfiguration = TrustChainTestUtil.signedJwt(
-                JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+        final String subordinateStatement = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
 
-        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
-        assertNoEntityConfiguration(entityId);
+        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+                mockResponse(subordinateStatement));
+        assertNoSubordinateStatement(entityId);
     }
 
     @Test
     public void testValidClaims_invalidTypeHeader()
             throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
         final String entityId = uniqueClientId();
-        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(entityId)
                 .issueTime(Date.from(Instant.now()))
                 .expirationTime(Date.from(Instant.now().plusSeconds(300)))
                 .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
                 .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
-        final String entityConfiguration = TrustChainTestUtil.signedJwt(
-                JWSAlgorithm.RS256, leafKey, "jwt", builder.build()).serialize();
+        final String subordinateStatement = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, trustedAnchorKey, "jwt", builder.build()).serialize();
+
+        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+                mockResponse(subordinateStatement));
 
-        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
-        assertNoEntityConfiguration(entityId);
+        assertNoSubordinateStatement(entityId);
     }
 
     @Test
     public void testIssuedInFuture()
             throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
         final String entityId = uniqueClientId();
-        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(entityId)
                 .issueTime(Date.from(Instant.now().plusSeconds(300)))
                 .expirationTime(Date.from(Instant.now().plusSeconds(300)))
                 .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
                 .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
-        final String entityConfiguration = TrustChainTestUtil.signedJwt(
-                JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+        final String subordinateStatement = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
 
-        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
-        assertNoEntityConfiguration(entityId);
+        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+                mockResponse(subordinateStatement));
+        assertNoSubordinateStatement(entityId);
     }
 
     @Test
     public void testMissingSub()
             throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
         final String entityId = uniqueClientId();
-        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId)
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId)
                 .issueTime(Date.from(Instant.now()))
                 .expirationTime(Date.from(Instant.now().plusSeconds(300)))
                 .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
                 .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
-        final String entityConfiguration = TrustChainTestUtil.signedJwt(
-                JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+        final String subordinateStatement = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
 
-        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
-        assertNoEntityConfiguration(entityId);
+        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+                mockResponse(subordinateStatement));
+        assertNoSubordinateStatement(entityId);
     }
 
     @Test
     public void testExpired()
             throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
         final String entityId = uniqueClientId();
-        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(entityId)
                 .issueTime(Date.from(Instant.now()))
                 .expirationTime(Date.from(Instant.now().minusSeconds(300)))
                 .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
                 .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
-        final String entityConfiguration = TrustChainTestUtil.signedJwt(
-                JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+        final String subordinateStatement = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
 
-        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
-        assertNoEntityConfiguration(entityId);
+        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+                mockResponse(subordinateStatement));
+        assertNoSubordinateStatement(entityId);
     }
 
     @Test
     public void testMissingJwks()
             throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
         final String entityId = uniqueClientId();
-        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(entityId)
                 .issueTime(Date.from(Instant.now()))
                 .expirationTime(Date.from(Instant.now().plusSeconds(300)))
                 .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
-        final String entityConfiguration = TrustChainTestUtil.signedJwt(
-                JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+        final String subordinateStatement = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
 
-        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
-        assertNoEntityConfiguration(entityId);
+        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+                mockResponse(subordinateStatement));
+        assertNoSubordinateStatement(entityId);
     }
 
     @Test
     public void testInvalidJwksClaim()
             throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
         final String entityId = uniqueClientId();
-        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(entityId)
                 .issueTime(Date.from(Instant.now()))
                 .expirationTime(Date.from(Instant.now().plusSeconds(300)))
                 .claim("jwks", Map.of("federation_entity", Collections.emptyMap()))
                 .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
-        final String entityConfiguration = TrustChainTestUtil.signedJwt(
-                JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+        final String subordinateStatement = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
 
-        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
-        assertNoEntityConfiguration(entityId);
+        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+                mockResponse(subordinateStatement));
+        assertNoSubordinateStatement(entityId);
     }
 
-    protected void assertNoEntityConfiguration(final String entityId) {
+    protected void assertNoSubordinateStatement(final String entityId) {
         try {
-            final List<EntityStatement> result =
-                    entityConfigurationCache.get(new CriteriaSet(new SubjectEntityIDCriterion(entityId)));
+            final List<RemoteSubordinateStatementContainer> result =
+                    subordinateStatementCache.get(new CriteriaSet(new SubjectEntityIDCriterion(entityId),
+                            new ResponseContainerExpirationCriterion(Instant.now().plusSeconds(300)), 
+                            new IssuerEntityIDCriterion(anchorId)));
             Assert.assertNotNull(result);
-            Assert.assertEquals(result.size(), 0);
+            Assert.assertEquals(result.size(), 1);
+            Assert.assertNull(result.get(0).getEntityStatement());
         } catch (MetadataCacheException e) {
             Assert.fail("Could not resolve entity configuration", e);
         }
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/EntityConfigurationMetadataCacheTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/TrustChainMetadataCacheTest.java
similarity index 56%
copy from idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/EntityConfigurationMetadataCacheTest.java
copy to idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/TrustChainMetadataCacheTest.java
index a55de27..803ac08 100644
--- a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/EntityConfigurationMetadataCacheTest.java
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/TrustChainMetadataCacheTest.java
@@ -30,10 +30,12 @@ import org.testng.annotations.Test;
 import com.nimbusds.jose.JWSAlgorithm;
 import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jwt.JWTClaimsSet;
-import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
 
 import net.shibboleth.idp.plugin.oidc.op.oidfed.TrustChainTestUtil;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.IssuerEntityIDCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.ResponseContainerExpirationCriterion;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityIDCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.RemoteTrustChainsContainer;
 import net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.AbstractFederationFlowTest;
 import net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.EntityConfigurationFlowTest;
 import net.shibboleth.oidc.metadata.cache.MetadataCache;
@@ -43,69 +45,74 @@ import net.shibboleth.shared.resolver.CriteriaSet;
 /**
  * Unit tests for the default entity configuration metadata cache.
  */
-public class EntityConfigurationMetadataCacheTest extends AbstractFederationFlowTest {
+ at SuppressWarnings("null")
+public class TrustChainMetadataCacheTest extends AbstractFederationFlowTest {
 
-    protected EntityConfigurationMetadataCacheTest() {
+    protected TrustChainMetadataCacheTest() {
         super(EntityConfigurationFlowTest.FLOW_ID);
     }
 
     @Autowired
-    @Qualifier("shibboleth.oidfed.EntityConfigurationMetadataCache")
-    MetadataCache<EntityStatement> entityConfigurationCache;
+    @Qualifier("shibboleth.oidfed.TrustChainMetadataCache")
+    MetadataCache<RemoteTrustChainsContainer> trustChainCache;
 
     @Test
-    public void testValidEntityConfiguration()
+    public void testValidTrustChainResolved()
             throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
         final String entityId = uniqueClientId();
+        
         final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
                 .issueTime(Date.from(Instant.now()))
                 .expirationTime(Date.from(Instant.now().plusSeconds(300)))
                 .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
-                .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+                .claim("metadata", Map.of("federation_entity", Collections.emptyMap()))
+                .claim("authority_hints", List.of(anchorId));
         final String entityConfiguration = TrustChainTestUtil.signedJwt(
                 JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
 
         mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+
+        final JWTClaimsSet.Builder builder2 = new JWTClaimsSet.Builder().issuer(anchorId).subject(entityId)
+                .issueTime(Date.from(Instant.now()))
+                .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+                .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
+                .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+        final String subordinateStatement = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder2.build()).serialize();
+
+        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+                mockResponse(subordinateStatement));
         try {
-            final List<EntityStatement> result =
-                    entityConfigurationCache.get(new CriteriaSet(new SubjectEntityIDCriterion(entityId)));
+            final List<RemoteTrustChainsContainer> result =
+                    trustChainCache.get(new CriteriaSet(new SubjectEntityIDCriterion(entityId),
+                            new ResponseContainerExpirationCriterion(Instant.now().plusSeconds(300)), 
+                            new IssuerEntityIDCriterion(anchorId)));
             Assert.assertNotNull(result);
             Assert.assertEquals(result.size(), 1);
+            Assert.assertNotNull(result.get(0).getTrustChains());
+            Assert.assertEquals(result.get(0).getTrustChains().size(), 1);
         } catch (MetadataCacheException e) {
             Assert.fail("Could not resolve entity configuration", e);
         }
     }
-
+/*
     @Test
     public void testSignatureWithNonMathchingKey()
             throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
         final String entityId = uniqueClientId();
-        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
-                .issueTime(Date.from(Instant.now()))
-                .expirationTime(Date.from(Instant.now().plusSeconds(300)))
-                .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
-                .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
-        final String entityConfiguration = TrustChainTestUtil.signedJwt(
-                JWSAlgorithm.RS256, anchorKey, "entity-statement+jwt", builder.build()).serialize();
-
-        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
-        assertNoEntityConfiguration(entityId);
-    }
-
-    @Test
-    public void testNonMatchingSubjectVsIssuer()
-            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
-        final String entityId = uniqueClientId();
-        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(uniqueClientId()).subject(entityId)
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(entityId)
                 .issueTime(Date.from(Instant.now()))
                 .expirationTime(Date.from(Instant.now().plusSeconds(300)))
                 .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
                 .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
-        final String entityConfiguration = TrustChainTestUtil.signedJwt(
+        final String subordinateStatement = TrustChainTestUtil.signedJwt(
                 JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
 
-        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
-        assertNoEntityConfiguration(entityId);
+        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+                mockResponse(subordinateStatement));
+        assertNoSubordinateStatement(entityId);
     }
 
     @Test
@@ -113,122 +120,141 @@ public class EntityConfigurationMetadataCacheTest extends AbstractFederationFlow
             throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
         final String entityId = uniqueClientId();
         final String claimsEntityId = uniqueClientId();
-        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(claimsEntityId).subject(claimsEntityId)
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(claimsEntityId)
                 .issueTime(Date.from(Instant.now()))
                 .expirationTime(Date.from(Instant.now().plusSeconds(300)))
                 .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
                 .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
-        final String entityConfiguration = TrustChainTestUtil.signedJwt(
-                JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+        final String subordinateStatement = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
 
-        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
-        assertNoEntityConfiguration(entityId);
+        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+                mockResponse(subordinateStatement));
+        assertNoSubordinateStatement(entityId);
     }
 
     @Test
     public void testValidClaims_invalidTypeHeader()
             throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
         final String entityId = uniqueClientId();
-        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(entityId)
                 .issueTime(Date.from(Instant.now()))
                 .expirationTime(Date.from(Instant.now().plusSeconds(300)))
                 .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
                 .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
-        final String entityConfiguration = TrustChainTestUtil.signedJwt(
-                JWSAlgorithm.RS256, leafKey, "jwt", builder.build()).serialize();
+        final String subordinateStatement = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, trustedAnchorKey, "jwt", builder.build()).serialize();
 
-        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
-        assertNoEntityConfiguration(entityId);
+        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+                mockResponse(subordinateStatement));
+
+        assertNoSubordinateStatement(entityId);
     }
 
     @Test
     public void testIssuedInFuture()
             throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
         final String entityId = uniqueClientId();
-        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(entityId)
                 .issueTime(Date.from(Instant.now().plusSeconds(300)))
                 .expirationTime(Date.from(Instant.now().plusSeconds(300)))
                 .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
                 .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
-        final String entityConfiguration = TrustChainTestUtil.signedJwt(
-                JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+        final String subordinateStatement = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
 
-        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
-        assertNoEntityConfiguration(entityId);
+        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+                mockResponse(subordinateStatement));
+        assertNoSubordinateStatement(entityId);
     }
 
     @Test
     public void testMissingSub()
             throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
         final String entityId = uniqueClientId();
-        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId)
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId)
                 .issueTime(Date.from(Instant.now()))
                 .expirationTime(Date.from(Instant.now().plusSeconds(300)))
                 .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
                 .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
-        final String entityConfiguration = TrustChainTestUtil.signedJwt(
-                JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+        final String subordinateStatement = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
 
-        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
-        assertNoEntityConfiguration(entityId);
+        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+                mockResponse(subordinateStatement));
+        assertNoSubordinateStatement(entityId);
     }
 
     @Test
     public void testExpired()
             throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
         final String entityId = uniqueClientId();
-        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(entityId)
                 .issueTime(Date.from(Instant.now()))
                 .expirationTime(Date.from(Instant.now().minusSeconds(300)))
                 .claim("jwks", new JWKSet(leafKey).toJSONObject(true))
                 .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
-        final String entityConfiguration = TrustChainTestUtil.signedJwt(
-                JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+        final String subordinateStatement = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
 
-        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
-        assertNoEntityConfiguration(entityId);
+        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+                mockResponse(subordinateStatement));
+        assertNoSubordinateStatement(entityId);
     }
 
     @Test
     public void testMissingJwks()
             throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
         final String entityId = uniqueClientId();
-        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(entityId)
                 .issueTime(Date.from(Instant.now()))
                 .expirationTime(Date.from(Instant.now().plusSeconds(300)))
                 .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
-        final String entityConfiguration = TrustChainTestUtil.signedJwt(
-                JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+        final String subordinateStatement = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
 
-        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
-        assertNoEntityConfiguration(entityId);
+        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+                mockResponse(subordinateStatement));
+        assertNoSubordinateStatement(entityId);
     }
 
     @Test
     public void testInvalidJwksClaim()
             throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
         final String entityId = uniqueClientId();
-        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(entityId).subject(entityId)
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(entityId)
                 .issueTime(Date.from(Instant.now()))
                 .expirationTime(Date.from(Instant.now().plusSeconds(300)))
                 .claim("jwks", Map.of("federation_entity", Collections.emptyMap()))
                 .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
-        final String entityConfiguration = TrustChainTestUtil.signedJwt(
-                JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+        final String subordinateStatement = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
 
-        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
-        assertNoEntityConfiguration(entityId);
+        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+                mockResponse(subordinateStatement));
+        assertNoSubordinateStatement(entityId);
     }
 
-    protected void assertNoEntityConfiguration(final String entityId) {
+    protected void assertNoSubordinateStatement(final String entityId) {
         try {
-            final List<EntityStatement> result =
-                    entityConfigurationCache.get(new CriteriaSet(new SubjectEntityIDCriterion(entityId)));
+            final List<RemoteSubordinateStatementContainer> result =
+                    subordinateStatementCache.get(new CriteriaSet(new SubjectEntityIDCriterion(entityId),
+                            new ResponseContainerExpirationCriterion(Instant.now().plusSeconds(300)), 
+                            new IssuerEntityIDCriterion(anchorId)));
             Assert.assertNotNull(result);
-            Assert.assertEquals(result.size(), 0);
+            Assert.assertEquals(result.size(), 1);
+            Assert.assertNull(result.get(0).getEntityStatement());
         } catch (MetadataCacheException e) {
             Assert.fail("Could not resolve entity configuration", e);
         }
         
     }
+    */
 }
diff --git a/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed.properties b/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed.properties
index a844603..d682aae 100644
--- a/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed.properties
+++ b/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/oidfed/oidfed.properties
@@ -1 +1,4 @@
-idp.oidfed.entity.authorityHints = https://anchor1.example.org, https://anchor2.example.org
\ No newline at end of file
+idp.oidfed.entity.authorityHints = https://anchor1.example.org, https://anchor2.example.org
+
+idp.oidfed.cache.entityConfiguration.invalidContainerLifetime = PT0S
+idp.oidfed.cache.entityConfiguration.minRefreshDelay = PT0S
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index b84cf69..386a9cd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -210,7 +210,7 @@
                             <Implementation-Title>${project.artifactId}</Implementation-Title>
                             <Implementation-Version>${project.version}</Implementation-Version>
                             <Implementation-Vendor>shibboleth.net</Implementation-Vendor>
-                            <Sealed>true</Sealed>
+                            <Sealed>false</Sealed> <!-- TODO: temporarily set to false during refactoring-->
                         </manifestEntries>
                     </archive>
                 </configuration>

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


More information about the commits mailing list