[java-idp-plugin-oidc-op-oidfed] branch dev/CACHE-REFACTOR updated: Improved parsing and validation of entity statements

Codeberg noreply at shibboleth.net
Mon Feb 9 16:38:56 UTC 2026


This is an automated email from the git hooks/post-receive script.

codeberg pushed a commit to branch dev/CACHE-REFACTOR
in repository java-idp-plugin-oidc-op-oidfed.

View the commit online:
https://codeberg.org/Shibboleth/java-idp-plugin-oidc-op-oidfed/commit/0d154fc2f0144240bb5599cd31cae648ed7194d7

The following commit(s) were added to refs/heads/dev/CACHE-REFACTOR by this push:
     new 0d154fc  Improved parsing and validation of entity statements
0d154fc is described below

commit 0d154fc2f0144240bb5599cd31cae648ed7194d7
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Mon Feb 9 18:38:28 2026 +0200

    Improved parsing and validation of entity statements
    
    - TrustMarkOwner simplifies Jackson parsing of entity configurations
    - MetadataImpl doesn't allow null values as mandated by the spec
      - Possible null values after policy enforcer are removed
    - New JWT claims validators
      - Verify non-empty requirements for authority hints and trust anchor hints
      - Verify syntax for constraints and trust_mark_owners
    - Improved testing
---
 .../payload/EntityConfigurationPayload.java        |   4 +-
 .../metadata/payload/claim/TrustMarkOwner.java     |  49 +++++
 .../metadata/cache/TrustMarkOwnersCriterion.java   |   7 +-
 ...DefaultEntityConfigurationFetchingStrategy.java |   4 +-
 ...efaultSubordinateStatementFetchingStrategy.java |   4 +-
 .../impl/AbstractFederationPolicyConstraint.java   |   7 +-
 .../metadata/payload/claim/impl/MetadataImpl.java  |  41 +++-
 .../payload/claim/impl/TrustMarkOwnerImpl.java     | 120 +++++++++++
 .../impl/EntityConfigurationPayloadImpl.java       |   7 +-
 .../impl/AbstractTrustChainResolutionAction.java   |   7 +-
 .../op/oidfed/profile/impl/ResolveTrustMarks.java  |  13 +-
 ...tChainTrustedTrustMarkOwnersLookupStrategy.java |   7 +-
 .../DefaultTrustMarkOwnerCredentialResolver.java   |  20 +-
 .../impl/ConstraintsSyntaxClaimsValidator.java     | 100 ++++++++++
 .../impl/NonEmptyStringArrayClaimsValidator.java   |  88 +++++++++
 .../impl/TrustMarkOwnersClaimsValidator.java       |  81 ++++++++
 .../META-INF/net.shibboleth.idp/postconfig.xml     |  17 ++
 .../EntityConfigurationMetadataCacheTest.java      | 219 +++++++++++++++++++++
 .../SubordinateStatementMetadataCacheTest.java     | 213 ++++++++++++++++++++
 19 files changed, 964 insertions(+), 44 deletions(-)

diff --git a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/payload/EntityConfigurationPayload.java b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/payload/EntityConfigurationPayload.java
index 25cd0a7..2d5db58 100644
--- a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/payload/EntityConfigurationPayload.java
+++ b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/payload/EntityConfigurationPayload.java
@@ -19,6 +19,8 @@ import java.util.Map;
 
 import javax.annotation.Nullable;
 
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.payload.claim.TrustMarkOwner;
+
 /**
  * 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
@@ -60,6 +62,6 @@ public interface EntityConfigurationPayload extends EntityStatementPayload {
      * 
      * @return trust mark owners
      */
-    @Nullable public Map<String, Map<String, Object>> getTrustMarkOwners();
+    @Nullable public Map<String, TrustMarkOwner> getTrustMarkOwners();
 
 }
diff --git a/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/payload/claim/TrustMarkOwner.java b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/payload/claim/TrustMarkOwner.java
new file mode 100644
index 0000000..114ece6
--- /dev/null
+++ b/idp-oidfed-op-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/payload/claim/TrustMarkOwner.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.payload.claim;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+import com.nimbusds.jose.jwk.JWKSet;
+
+/**
+ * Trust mark owner claim to be used with a map of trust_mark_owners as defined by the OpenID Federation 1.0 Section
+ * 3.1.2.
+ */
+public interface TrustMarkOwner {
+
+    /**
+     * Get the subject.
+     * 
+     * @return subject
+     */
+    @Nullable public String getSub();
+
+    /**
+     * Get the JWK set.
+     * 
+     * @return JWK set
+     */
+    @Nullable public JWKSet getJwks();
+
+    /**
+     * Get the map of custom claims.
+     * 
+     * @return The map of any other claims not directly mapped.
+     */
+    @Nullable public Map<String, Object> getCustomClaims();
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/TrustMarkOwnersCriterion.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/TrustMarkOwnersCriterion.java
index a17184d..b63fe28 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/TrustMarkOwnersCriterion.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/TrustMarkOwnersCriterion.java
@@ -19,6 +19,7 @@ import java.util.Objects;
 
 import javax.annotation.Nonnull;
 
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.payload.claim.TrustMarkOwner;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.resolver.Criterion;
 
@@ -28,14 +29,14 @@ import net.shibboleth.shared.resolver.Criterion;
 public class TrustMarkOwnersCriterion implements Criterion {
 
     /** The trust mark owners. */
-    @Nonnull private final Map<String, Map<String, Object>> owners;
+    @Nonnull private final Map<String, TrustMarkOwner> owners;
 
     /**
      * Constructor.
      *
      * @param trustMarkOwners the truts mark owners, must not be null
      */
-    public TrustMarkOwnersCriterion(@Nonnull final Map<String, Map<String, Object>> trustMarkOwners) {
+    public TrustMarkOwnersCriterion(@Nonnull final Map<String, TrustMarkOwner> trustMarkOwners) {
         owners = Constraint.isNotNull(trustMarkOwners, "Trust Mark owners cannot be null");
     }
 
@@ -45,7 +46,7 @@ public class TrustMarkOwnersCriterion implements Criterion {
      * @return the trust mark owners value
      */
     @Nonnull
-    public Map<String, Map<String, Object>> getValue() {
+    public Map<String, TrustMarkOwner> getValue() {
         return owners;
     }
 
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/configuration/DefaultEntityConfigurationFetchingStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/configuration/DefaultEntityConfigurationFetchingStrategy.java
index 2c9ad0b..dabaa7d 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/configuration/DefaultEntityConfigurationFetchingStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/configuration/DefaultEntityConfigurationFetchingStrategy.java
@@ -104,7 +104,9 @@ public class DefaultEntityConfigurationFetchingStrategy
             }
 
             try {
-                final SignedJWT jwt = SignedJWT.parse(EntityUtils.toString(response.getEntity()));
+                final String content = EntityUtils.toString(response.getEntity());
+                log.trace("Attempting to parse signed JWT from content: {}", content);
+                final SignedJWT jwt = SignedJWT.parse(content);
                 if (!JWT_TYPE_HEADER.equals(jwt.getHeader().getType())) {
                     log.warn("Unexpected JWT type header {}", jwt.getHeader().getType());
                     return new EntityConfigurationContainer(entityId, null, validExpiration, invalidExpiration);
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/subordinate/DefaultSubordinateStatementFetchingStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/subordinate/DefaultSubordinateStatementFetchingStrategy.java
index 7535bd7..41069f8 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/subordinate/DefaultSubordinateStatementFetchingStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/cache/subordinate/DefaultSubordinateStatementFetchingStrategy.java
@@ -170,7 +170,9 @@ public class DefaultSubordinateStatementFetchingStrategy
             }
 
             try {
-                final SignedJWT jwt = SignedJWT.parse(EntityUtils.toString(response.getEntity()));
+                final String content = EntityUtils.toString(response.getEntity());
+                log.trace("Attempting to parse signed JWT from content: {}", content);
+                final SignedJWT jwt = SignedJWT.parse(content);
                 if (!JWT_TYPE_HEADER.equals(jwt.getHeader().getType())) {
                     log.warn("Unexpected JWT type header {}", jwt.getHeader().getType());
                     return new SubordinateStatementContainer(id, null, validExpiration, invalidExpiration);
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 424c62c..f4a5aea 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
@@ -24,6 +24,7 @@ import org.slf4j.Logger;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubordinateStatement;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.constraints.FederationPolicyConstraint;
+import net.shibboleth.shared.annotation.constraint.Live;
 import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
 import net.shibboleth.shared.logic.ConstraintViolationException;
 import net.shibboleth.shared.primitive.LoggerFactory;
@@ -42,7 +43,7 @@ public abstract class AbstractFederationPolicyConstraint<T extends Object>
     /** {@inheritDoc} */
     @Override
     public boolean validate(@Nullable final Object constraint, @Nonnull final SubordinateStatement subordinateStatement,
-            @Nonnull final List<EntityStatement<?>> trustChain) {
+            @Nonnull @Live final List<EntityStatement<?>> trustChain) {
         checkComponentActive();
         try {
             log.trace("Attempting to parse raw constraint value: {}", constraint);
@@ -72,10 +73,10 @@ public abstract class AbstractFederationPolicyConstraint<T extends Object>
      * 
      * @param constraintData the non-null constraint value
      * @param subordinateStatement subordinate statement that contains the constraint
-     * @param trustChain trust chain to be evaluated
+     * @param trustChain trust chain to be evaluated and optionally updated: it is required to be modifiable
      * @return true if the trust chain is valid for this constraint, false otherwise.
      */
     protected abstract boolean doValidate(@Nonnull final T constraintData,
             @Nonnull final SubordinateStatement subordinateStatement,
-            @Nonnull final List<EntityStatement<?>> trustChain);
+            @Nonnull @Live final List<EntityStatement<?>> trustChain);
 }
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/payload/claim/impl/MetadataImpl.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/payload/claim/impl/MetadataImpl.java
index 74b08d8..fbc3784 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/payload/claim/impl/MetadataImpl.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/payload/claim/impl/MetadataImpl.java
@@ -27,6 +27,8 @@ import com.google.common.base.MoreObjects;
 
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.payload.claim.Metadata;
 import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.ConstraintViolationException;
 
 /**
  * Metadata claim as defined by the OpenID Federation 1.0 Section 5.
@@ -67,6 +69,10 @@ public class MetadataImpl implements Metadata {
      * @param allClaims claims used for populating this object
      */
     public MetadataImpl(@Nonnull final Map<String, Map<String, Object>> allClaims) {
+        for (final String claim : allClaims.keySet()) {
+            verifyNoNullValues(allClaims.get(claim), Constraint.isNotNull(claim,
+                    "Metadata for entity type " + claim + " contains a null claim key"));
+        }
         final Map<String,Map<String,Object>> input = new HashMap<>(allClaims);
         if (allClaims.containsKey("federation_entity")) {
             federationEntityMetadata = allClaims.get("federation_entity");
@@ -110,7 +116,7 @@ public class MetadataImpl implements Metadata {
      * @param metadata federation entity metadata
      */
     public void setFederationEntityMetadata(@Nullable final Map<String,Object> metadata) {
-        federationEntityMetadata = metadata;
+        federationEntityMetadata = verifyNoNullValues(metadata, "federation_entity");
     }
 
     /**
@@ -128,7 +134,7 @@ public class MetadataImpl implements Metadata {
      * @param metadata OIDC RP metadata
      */
     public void setOpenidRelyingPartyMetadata(@Nullable final Map<String,Object> metadata) {
-        openidRelyingPartyMetadata = metadata;
+        openidRelyingPartyMetadata = verifyNoNullValues(metadata, "openid_relying_party");
     }
 
     /**
@@ -146,7 +152,7 @@ public class MetadataImpl implements Metadata {
      * @param metadata OIDC OP metadata
      */
     public void setOpenidProviderMetadata(@Nullable final Map<String,Object> metadata) {
-        openidProviderMetadata = metadata;
+        openidProviderMetadata = verifyNoNullValues(metadata, "openid_provider");
     }
 
     /**
@@ -164,7 +170,7 @@ public class MetadataImpl implements Metadata {
      * @param metadata OAuth AS metadata
      */
     public void setOauthAuthorizationServerMetadata(@Nullable final Map<String,Object> metadata) {
-        oauthAuthorizationServerMetadata = metadata;
+        oauthAuthorizationServerMetadata = verifyNoNullValues(metadata, "oauth_authorization_server");
     }
 
     /**
@@ -182,7 +188,7 @@ public class MetadataImpl implements Metadata {
      * @param metadata OAuth client metadata
      */
     public void setOauthClientMetadata(@Nullable final Map<String,Object> metadata) {
-        oauthClientMetadata = metadata;
+        oauthClientMetadata = verifyNoNullValues(metadata, "oauth_client");
     }
 
     /**
@@ -200,7 +206,7 @@ public class MetadataImpl implements Metadata {
      * @param metadata OAuth protected resource metadata
      */
     public void setOauthResourceMetadata(@Nullable final Map<String,Object> metadata) {
-        oauthClientMetadata = metadata;
+        oauthClientMetadata = verifyNoNullValues(metadata, "oauth_resource");
     }
 
     /**
@@ -221,7 +227,7 @@ public class MetadataImpl implements Metadata {
      */
     @JsonAnySetter
     public void setCustomClaims(final String name, final Map<String,Object> value) {
-        customClaims.put(name, value);
+        customClaims.put(name, Constraint.isNotNull(value, "Metadata entity type " + name + " cannot be null"));
     }
 
     /**
@@ -252,7 +258,26 @@ public class MetadataImpl implements Metadata {
         claims.putAll(getCustomClaims());
         return CollectionSupport.copyToMap(claims);
     }
-    
+
+    /**
+     * Verifies the given map meets syntax requirements: is not null and does not have null claim values.
+     * 
+     * @param map the map to be verified
+     * @param entityType the entity type
+     * @return verified map
+     * @throws ConstraintViolationException if the map does not meet the requirements
+     */
+    protected Map<String,Object> verifyNoNullValues(@Nullable final Map<String,Object> map,
+            @Nonnull final String entityType)  throws ConstraintViolationException {
+        Constraint.isNotNull(map, "Metadata entity type " + entityType + " cannot be null");
+        assert map != null;
+        for (final String claim : map.keySet()) {
+            Constraint.isNotNull(map.get(claim), "Metadata for entity type " + entityType 
+                    + " contains a claim with null value: " + claim);
+        }
+        return map;
+    }
+
     /** {@inheritDoc} */
     @Override public String toString() {
         return MoreObjects.toStringHelper(this)
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/payload/claim/impl/TrustMarkOwnerImpl.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/payload/claim/impl/TrustMarkOwnerImpl.java
new file mode 100644
index 0000000..c6b7644
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/payload/claim/impl/TrustMarkOwnerImpl.java
@@ -0,0 +1,120 @@
+/*
+ * 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.payload.claim.impl;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+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;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.payload.claim.TrustMarkOwner;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Trust mark owner claim to be used with a map of trust_mark_owners as defined by the OpenID Federation 1.0 Section
+ * 3.1.2.
+ */
+public class TrustMarkOwnerImpl implements TrustMarkOwner {
+
+    /** Subject. */
+    @JsonProperty("sub") private String sub;
+
+    /** A JSON Web Key Set representing the public part of the owner's Federation Entity signing keys. */
+    @JsonProperty("jwks") private JWKSet jwks;
+
+    /** The map of any other claims not directly mapped. */
+    private final Map<String, Object> customClaims = new HashMap<>();
+
+    /**
+     * Constructor.
+     */
+    public TrustMarkOwnerImpl() {
+        // no op
+    }
+
+    /**
+     * Get the subject.
+     * 
+     * @return subject
+     */
+    @Nullable public String getSub() {
+        return sub;
+    }
+
+    /**
+     * Set the subject.
+     * 
+     * @param subject subject
+     */
+    public void setSub(@Nullable final String subject) {
+        sub = subject;
+    }
+
+    /**
+     * Get the map of custom claims.
+     * 
+     * @return The map of any other claims not directly mapped.
+     */
+    @JsonAnyGetter
+    public Map<String, Object> getCustomClaims() {
+        return customClaims;
+    }
+
+    /**
+     * Get the JWK set.
+     * 
+     * @return JWK set
+     */
+    public JWKSet getJwks() {
+        return jwks;
+    }
+
+    /**
+     * Set the JWK set.
+     * 
+     * @param jwkSet JWK set
+     */
+    public void setJwks(final JWKSet jwkSet) {
+        jwks = jwkSet;
+    }
+
+    /**
+     * 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(@Nonnull @NotEmpty final String name, @Nullable final Object value) {
+        customClaims.put(Constraint.isNotEmpty(name, "Claim name cannot be null"), value);
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("sub", getSub())
+                .add("jwks", getJwks())
+                .add("customClaims", getCustomClaims()).toString();
+    }
+
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/payload/impl/EntityConfigurationPayloadImpl.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/payload/impl/EntityConfigurationPayloadImpl.java
index a752753..f22dddc 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/payload/impl/EntityConfigurationPayloadImpl.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/payload/impl/EntityConfigurationPayloadImpl.java
@@ -24,6 +24,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
 import com.google.common.base.MoreObjects;
 
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.payload.EntityConfigurationPayload;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.payload.claim.TrustMarkOwner;
 
 /**
  * Entity Configuration payload claims as defined by the OpenID Federation 1.0 Section 3.2. This class extends the
@@ -47,7 +48,7 @@ public class EntityConfigurationPayloadImpl extends EntityStatementPayloadImpl i
     @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;
+    @JsonProperty("trust_mark_owners") private Map<String, TrustMarkOwner> trustMarkOwners;
 
     /**
      * Constructor.
@@ -169,7 +170,7 @@ public class EntityConfigurationPayloadImpl extends EntityStatementPayloadImpl i
      * 
      * @return trust mark owners
      */
-    @Nullable public Map<String, Map<String, Object>> getTrustMarkOwners() {
+    @Nullable public Map<String, TrustMarkOwner> getTrustMarkOwners() {
         return trustMarkOwners;
     }
 
@@ -178,7 +179,7 @@ public class EntityConfigurationPayloadImpl extends EntityStatementPayloadImpl i
      * 
      * @param owners trust mark owners
      */
-    public void setTrustMarkOwners(@Nullable Map<String, Map<String, Object>> owners) {
+    public void setTrustMarkOwners(@Nullable Map<String, TrustMarkOwner> owners) {
         trustMarkOwners = owners;
     }
 
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 d96ac06..94aee56 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
@@ -309,7 +309,12 @@ public class AbstractTrustChainResolutionAction extends AbstractProfileAction {
                     final MetadataPolicy policy = mergedPolicies.get(claim);
                     try {
                         final Object enforcedValue = enforceValue(claim, requestMetadata.get(claim), policy);
-                        requestMetadata.put(claim, enforcedValue);
+                        if (enforcedValue != null) {
+                            requestMetadata.put(claim, enforcedValue);
+                        } else {
+                            log.debug("{} Enforcer returned null for entity {} claim {}", getLogPrefix(), entityType,
+                                    claim);
+                        }
                     } catch (final ConstraintViolationException e) {
                         log.warn("{} The requested metadata is not compliant with the policy", getLogPrefix());
                         return OidFederationEventIds.INVALID_METADATA_AGAINST_POLICY;
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 30bb795..9b2feff 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
@@ -44,6 +44,7 @@ import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.SubjectEntityIDCr
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.SubjectEntityStatementCriterion;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.TrustMarkOwnersCriterion;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.trustchain.TrustChainsContainer;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.payload.claim.TrustMarkOwner;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.context.RelyingPartyTrustChainContext;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.profile.context.VerifiedTrustChain;
 import net.shibboleth.idp.profile.AbstractProfileAction;
@@ -86,7 +87,7 @@ public class ResolveTrustMarks extends AbstractProfileAction {
 
     /** Strategy used to lookup trusted trust mark owners for the trust chain. */
     @NonnullAfterInit
-    private Function<List<EntityStatement<?>>, Map<String, Map<String, Object>>> trustedTrustMarkOwnersLookupStrategy;
+    private Function<List<EntityStatement<?>>, Map<String, TrustMarkOwner>> trustedTrustMarkOwnersLookupStrategy;
 
     /** Condition to solely take trusted trust mark issuers into account. */
     @Nonnull private Predicate<ProfileRequestContext> trustedTrustMarkIssuersOnlyCondition;
@@ -173,7 +174,7 @@ public class ResolveTrustMarks extends AbstractProfileAction {
      * @param strategy lookup strategy
      */
     public void setTrustedTrustMarkOwnersLookupStrategy(
-            @Nonnull final Function<List<EntityStatement<?>>, Map<String, Map<String, Object>>> strategy) {
+            @Nonnull final Function<List<EntityStatement<?>>, Map<String, TrustMarkOwner>> strategy) {
         checkSetterPreconditions();
         trustedTrustMarkOwnersLookupStrategy =
                 Constraint.isNotNull(strategy, "trustedTrustMarkOwnersLookupStrategy cannot be null");
@@ -343,7 +344,7 @@ public class ResolveTrustMarks extends AbstractProfileAction {
         log.debug("{} Trusted trust mark issuers {}", getLogPrefix(), trustedIssuers);
         assert trustedIssuers != null;
 
-        final Map<String, Map<String, Object>> trustedOwners =
+        final Map<String, TrustMarkOwner> trustedOwners =
                 Optional.ofNullable(trustedTrustMarkOwnersLookupStrategy.apply(selectedTrustChain))
                     .orElseGet(NonnullSupplier.of(CollectionSupport.emptyMap()));
         log.debug("{} Trusted trust mark owners {}", getLogPrefix(), trustedOwners);
@@ -407,7 +408,7 @@ public class ResolveTrustMarks extends AbstractProfileAction {
      */
     protected boolean checkTrustedIssuer(@Nullable final SignedJWT jwt,
             @Nonnull final Map<String, List<String>> trustedIssuers,
-            @Nonnull final Map<String, Map<String, Object>> trustedOwners) {
+            @Nonnull final Map<String, TrustMarkOwner> trustedOwners) {
         if (jwt == null) {
             return false;
         }
@@ -452,7 +453,7 @@ public class ResolveTrustMarks extends AbstractProfileAction {
      * @return true if trust mark verification was successful, false otherwise
      */
     protected boolean verifyTrustMark(@Nullable final SignedJWT jwt,
-            @Nonnull final Map<String, Map<String, Object>> trustedOwners,
+            @Nonnull final Map<String, TrustMarkOwner> trustedOwners,
             @Nonnull final ProfileRequestContext profileRequestContext) {
         if (jwt == null) {
             return false;
@@ -511,7 +512,7 @@ public class ResolveTrustMarks extends AbstractProfileAction {
      */
     protected boolean validateDelegatedTrustMark(@Nonnull final JWTClaimsSet trustMarkClaims,
             @Nonnull final String id,
-            @Nonnull final Map<String, Map<String, Object>> trustedOwners,
+            @Nonnull final Map<String, TrustMarkOwner> trustedOwners,
             @Nonnull final ProfileRequestContext profileRequestContext) {
         log.debug("{} Validating delegated trust mark {}", getLogPrefix(), id);
         try {
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultTrustChainTrustedTrustMarkOwnersLookupStrategy.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultTrustChainTrustedTrustMarkOwnersLookupStrategy.java
index 91c3c2f..e5aefc3 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultTrustChainTrustedTrustMarkOwnersLookupStrategy.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/navigate/DefaultTrustChainTrustedTrustMarkOwnersLookupStrategy.java
@@ -26,6 +26,7 @@ import org.slf4j.Logger;
 
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityStatement;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.payload.EntityConfigurationPayload;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.payload.claim.TrustMarkOwner;
 import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
@@ -35,7 +36,7 @@ import net.shibboleth.shared.primitive.LoggerFactory;
  */
 @ThreadSafe
 public class DefaultTrustChainTrustedTrustMarkOwnersLookupStrategy
-    implements Function<List<EntityStatement<?>>, Map<String, Map<String, Object>>> {
+    implements Function<List<EntityStatement<?>>, Map<String, TrustMarkOwner>> {
 
     /** Class logger. */
     @Nonnull private final Logger log =
@@ -43,11 +44,11 @@ public class DefaultTrustChainTrustedTrustMarkOwnersLookupStrategy
 
     /** {@inheritDoc} */
     @Nullable @Override
-    public Map<String, Map<String, Object>> apply(@Nullable final List<EntityStatement<?>> trustChain) {
+    public Map<String, TrustMarkOwner> apply(@Nullable final List<EntityStatement<?>> trustChain) {
         if (trustChain == null || trustChain.size() < 3) {
             return null;
         }
-        final Map<String, Map<String, Object>> ownersClaim =
+        final Map<String, TrustMarkOwner> ownersClaim =
                 trustChain.get(trustChain.size() - 1).getParsedPayload() instanceof EntityConfigurationPayload ecp ?
                         ecp.getTrustMarkOwners() : null;
         log.debug("Parsed trust_mark_owners claim {}", ownersClaim);
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/DefaultTrustMarkOwnerCredentialResolver.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/DefaultTrustMarkOwnerCredentialResolver.java
index d2176be..763ba7c 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/DefaultTrustMarkOwnerCredentialResolver.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/DefaultTrustMarkOwnerCredentialResolver.java
@@ -14,11 +14,9 @@
 
 package net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential;
 
-import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-import java.util.stream.Collectors;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -31,6 +29,7 @@ import com.nimbusds.jose.jwk.JWKSet;
 
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.SubjectEntityIDCriterion;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.cache.TrustMarkOwnersCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.payload.claim.TrustMarkOwner;
 import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
 import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.primitive.LoggerFactory;
@@ -65,21 +64,14 @@ public class DefaultTrustMarkOwnerCredentialResolver extends BasicJOSEObjectCred
                     "Credential criteria set did not contain an instance of SubjectEntityIDCriterion");
         }
         final String entityId = subjectCriterion.getValue();
-        final Map<String, Map<String, Object>> owners = ownersCriterion.getValue();
+        final Map<String, TrustMarkOwner> owners = ownersCriterion.getValue();
         if (owners.isEmpty() || owners.get(entityId) == null) {
             log.debug("No trusted owners entry found for {}", entityId);
             return CollectionSupport.emptyList();
         }
-        final Map<String, Object> ownerConfiguration = owners.get(entityId);
-        if (ownerConfiguration.get("jwks") instanceof Map<?,?> map) {
-            final JWKSet jwkSet;
-            try {
-                jwkSet = JWKSet.parse(map.entrySet().stream()
-                        .collect(Collectors.toMap(e -> e.getKey().toString(), e -> e.getValue())));
-            } catch (final ParseException e) {
-                log.debug("Could not parse JWKSet from the jwks claim", e);
-                return CollectionSupport.emptyList();        
-            }
+        final TrustMarkOwner ownerConfiguration = owners.get(entityId);
+        final JWKSet jwkSet = ownerConfiguration.getJwks();
+        if (jwkSet != null && !jwkSet.isEmpty()) {
             final List<Credential> credentials = new ArrayList<>();
             for (final JWK jwk : jwkSet.getKeys()) {
                 if (jwk != null) {
@@ -92,7 +84,7 @@ public class DefaultTrustMarkOwnerCredentialResolver extends BasicJOSEObjectCred
             log.debug("Returning credentials {} for {}", credentials, entityId);
             return credentials;
         }
-        log.debug("Could not parse jwks from {}", ownerConfiguration.get("jwks"));
+        log.debug("Could not find jwks from {}", ownerConfiguration);
         return CollectionSupport.emptyList();        
     }
 
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/jwt/claims/impl/ConstraintsSyntaxClaimsValidator.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/jwt/claims/impl/ConstraintsSyntaxClaimsValidator.java
new file mode 100644
index 0000000..3e225bb
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/jwt/claims/impl/ConstraintsSyntaxClaimsValidator.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.security.jwt.claims.impl;
+
+import java.text.ParseException;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.oidc.jwt.claims.AbstractClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
+
+/**
+ * A {@link ClaimsValidator} for validating the syntax for standard constraints.
+ */
+public class ConstraintsSyntaxClaimsValidator extends AbstractClaimsValidator {
+
+    /** Generic prefix to be used with the {@link JWTValidationException}. */
+    public static final String ERROR_PREFIX = "Unexpected contents for constraints: ";
+
+    /** {@inheritDoc} */
+    protected void doValidate(@Nonnull final JWTClaimsSet claims, 
+            @Nonnull final ProfileRequestContext context) throws JWTValidationException {
+        try {
+            final Map<String, Object> constraints = claims.getJSONObjectClaim("constraints");
+            if (constraints == null || constraints.isEmpty()) {
+                return;
+            }
+            if (constraints.containsKey("max_path_length")) {
+                if (!(constraints.get("max_path_length") instanceof Number)) {
+                    throw new JWTValidationException(ERROR_PREFIX + "max_path_length is not a number");
+                }
+            }
+            if (constraints.containsKey("naming_constraints")) {
+                verifyNamingConstraints(constraints.get("naming_constraints"));
+            }
+            if (constraints.containsKey("allowed_entity_types")) {
+                if (constraints.get("allowed_entity_types") instanceof List list) {
+                    for (final Object item : list) {
+                        if (!(item instanceof String)) {
+                            throw new JWTValidationException(ERROR_PREFIX
+                                    + "allowed_entity_types value is not a list of strings");
+                        }
+                    }
+                } else {
+                    throw new JWTValidationException("allowed_entity_types is not a list");
+                }
+            }
+        } catch (final ParseException e) {
+            throw new JWTValidationException(ERROR_PREFIX + "could not parse a map", e);
+        }
+    }
+
+    /**
+     * Verifies the 'naming_constraints' syntax.
+     * 
+     * @param value the naming_constraints value
+     * @throws JWTValidationException if the value syntax is invalid
+     */
+    protected void verifyNamingConstraints(@Nullable final Object value) throws JWTValidationException {
+        final String errorPrefix = ERROR_PREFIX + "naming_constraints ";
+        if (value instanceof Map<?,?> map) {
+            for (final Object key : map.keySet()) {
+                if (!(key instanceof String)) {
+                    throw new JWTValidationException(errorPrefix + "key is not a string");
+                }
+                if (map.get(key) instanceof List<?> list) {
+                    for (final Object item : list) {
+                        if (!(item instanceof String)) {
+                            throw new JWTValidationException(errorPrefix + "value is not a list of strings");
+                        }
+                    }
+                } else {
+                    throw new JWTValidationException(errorPrefix + "value is not a list");
+                }
+            }
+        } else {
+            throw new JWTValidationException(errorPrefix + "is not a map");
+        }
+    }
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/jwt/claims/impl/NonEmptyStringArrayClaimsValidator.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/jwt/claims/impl/NonEmptyStringArrayClaimsValidator.java
new file mode 100644
index 0000000..ea100a8
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/jwt/claims/impl/NonEmptyStringArrayClaimsValidator.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.security.jwt.claims.impl;
+
+import java.text.ParseException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.oidc.jwt.claims.AbstractClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * A {@link ClaimsValidator} for validating that the configured claims are non-empty string arrays if present.
+ */
+ at ThreadSafeAfterInit
+public class NonEmptyStringArrayClaimsValidator extends AbstractClaimsValidator {
+
+    /** The names of the JWT claims that must be non-empty string arrays if they exist, empty set if none. */ 
+    @Nonnull @NonnullElements private Set<String> nonEmptyArrayClaims;
+
+    /**
+     * Constructor.
+     */
+    public NonEmptyStringArrayClaimsValidator() {
+        nonEmptyArrayClaims = CollectionSupport.emptySet();
+    }
+
+    /**
+     * Set the non-empty string array claims.
+     * 
+     * @param claims the non-empty string array claims.
+     */
+    public void setNonEmptyArrayClaims(@Nullable final Collection<String> claims) {   
+        ifInitializedThrowUnmodifiabledComponentException();
+        
+        if (claims !=null) {
+            nonEmptyArrayClaims = CollectionSupport.copyToSet(StringSupport.normalizeStringCollection(claims));
+        } else {
+            nonEmptyArrayClaims = CollectionSupport.emptySet();
+        }
+    }
+
+    /** {@inheritDoc} */
+    protected void doValidate(@Nonnull final JWTClaimsSet claims, 
+            @Nonnull final ProfileRequestContext context) throws JWTValidationException {
+
+        for (final String claim : nonEmptyArrayClaims) {
+            if (claims.getClaims().containsKey(claim)) {
+                final List<String> values;
+                try {
+                    values = claims.getStringListClaim(claim);
+                } catch (final ParseException e) {
+                    throw new JWTValidationException("Could not parse " + claim + " into a list of strings");
+                }
+                if (values != null) {
+                    if (values.isEmpty()) {
+                        throw new JWTValidationException("Empty array is not allowed for " + claim);
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/jwt/claims/impl/TrustMarkOwnersClaimsValidator.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/jwt/claims/impl/TrustMarkOwnersClaimsValidator.java
new file mode 100644
index 0000000..1b067fc
--- /dev/null
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/jwt/claims/impl/TrustMarkOwnersClaimsValidator.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.security.jwt.claims.impl;
+
+import java.text.ParseException;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.oidc.jwt.claims.AbstractClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+
+/**
+ * A {@link ClaimsValidator} for validating the syntax of the optional trust_mark_owners claim. The value must be a
+ * String-keyed map of maps with mandatory sub and jwks claims.
+ */
+ at ThreadSafeAfterInit
+public class TrustMarkOwnersClaimsValidator extends AbstractClaimsValidator {
+
+    /** {@inheritDoc} */
+    protected void doValidate(@Nonnull final JWTClaimsSet claims, 
+            @Nonnull final ProfileRequestContext context) throws JWTValidationException {
+        try {
+            final Map<String,Object> trustMarkOwners = claims.getJSONObjectClaim("trust_mark_owners");
+            if (trustMarkOwners != null) {
+                for (final Object key : trustMarkOwners.keySet()) {
+                    if (key instanceof String string) {
+                        final Object raw = trustMarkOwners.get(string);
+                        if (raw instanceof Map<?, ?> map) {
+                            final Map<String, Object> trustMarkOwner = map.keySet().stream()
+                                    .filter(String.class::isInstance)
+                                    .map(String.class::cast)
+                                    .filter(k -> map.get(k) != null)
+                                    .collect(Collectors.toMap(k -> k, k -> map.get(k)));
+                            if (trustMarkOwner.get("sub") instanceof String subString) {
+                                if (subString.isEmpty()) {
+                                    throw new JWTValidationException("Unexpected contents for trust_mark_owners item: "
+                                        + "Subject is empty");
+                                }
+                            } else {
+                                throw new JWTValidationException("Unexpected contents for trust_mark_owners item: "
+                                        + "Subject is not a string");
+                            }
+                            if (trustMarkOwner.get("jwks") == null) {
+                                throw new JWTValidationException("Unexpected contents for trust_mark_owners item: "
+                                        + "Value for 'jwks' is missing");
+                            }
+                        } else {
+                            throw new JWTValidationException("Unexpected contents for trust_mark_owners item: "
+                                    + "Value is not a map");
+                        }
+                    } else {
+                        throw new JWTValidationException("Unexpected contents for trust_mark_owners item: "
+                                + "Key is not a string");
+                    }
+                }
+            }
+        } catch (final ParseException e) {
+            throw new JWTValidationException("Unexpected contents for trust_mark_owners: could not parse a map", e);
+        }
+    }
+}
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 74722fa..a64e8d5 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
@@ -172,6 +172,9 @@
                 <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.jwt.claims.impl.CritClaimsValidator"
                     p:recognizedClaims="%{idp.oidfed.cache.entityConfiguration.critClaims:%{idp.oidfed.cache.default.critClaims:}}" />
                 <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.jwt.claims.impl.TrustMarksClaimsValidator" />
+                <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.jwt.claims.impl.NonEmptyStringArrayClaimsValidator"
+                    p:nonEmptyArrayClaims="authority_hints,trust_anchor_hints" />
+                <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.jwt.claims.impl.TrustMarkOwnersClaimsValidator" />
             </util:list>
         </property>
     </bean>
@@ -309,6 +312,7 @@
                     p:prohibitedClaims="trust_mark_owners" />
                 <bean class="net.shibboleth.oidc.security.jwt.claims.impl.ProhibitedClaimsValidator"
                     p:prohibitedClaims="aud" />
+                <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.jwt.claims.impl.ConstraintsSyntaxClaimsValidator"/>
             </util:list>
         </property>
     </bean>
@@ -741,6 +745,17 @@
         </property>
     </bean>
 
+    <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
+        <property name="targetObject" ref="shibboleth.oidfed.JacksonSimpleTypeResolver" />
+        <property name="targetMethod" value="addMapping" />
+        <property name="arguments">
+            <list>
+                <value>#{ T(net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.payload.claim.TrustMarkOwner)}</value>
+                <value>#{ T(net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.payload.claim.impl.TrustMarkOwnerImpl)}</value>
+            </list>
+        </property>
+    </bean>
+
     <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
         <property name="targetObject" ref="shibboleth.oidfed.policy.JSONSimpleModule" />
         <property name="targetMethod" value="setAbstractTypes" />
@@ -1103,6 +1118,8 @@
                     p:recognizedClaims="%{idp.oidfed.cache.explicitRegistration.critClaims:%{idp.oidfed.cache.default.critClaims:}}" />
                 <bean class="net.shibboleth.oidc.security.jwt.claims.impl.RequiredClaimsValidator"
                     p:requiredClaims="authority_hints" />
+                <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.jwt.claims.impl.NonEmptyStringArrayClaimsValidator"
+                    p:nonEmptyArrayClaims="authority_hints,trust_anchor_hints" />
                 <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.jwt.claims.impl.TrustMarksClaimsValidator" />
             </util:list>
         </property>
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 f1e1cce..3aa17fd 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
@@ -23,6 +23,7 @@ import java.net.URISyntaxException;
 import java.time.Instant;
 import java.util.Collections;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -31,9 +32,18 @@ import org.springframework.beans.factory.annotation.Qualifier;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
+import com.nimbusds.jose.EncryptionMethod;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWEAlgorithm;
+import com.nimbusds.jose.JWEObject;
+import com.nimbusds.jose.JWEHeader;
 import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.Payload;
+import com.nimbusds.jose.crypto.RSAEncrypter;
 import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
+import com.nimbusds.jwt.SignedJWT;
 
 import net.shibboleth.idp.plugin.oidc.op.oidfed.TrustChainTestUtil;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.EntityConfiguration;
@@ -45,6 +55,7 @@ import net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.EntityConfiguration
 import net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed.support.CustomEntityConfigurationFilterStrategy;
 import net.shibboleth.oidc.metadata.cache.MetadataCache;
 import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.resolver.CriteriaSet;
 
 /**
@@ -121,6 +132,47 @@ public class EntityConfigurationMetadataCacheTest extends AbstractFederationFlow
         }
     }
 
+    @Test
+    public void testUnsignedEntityConfiguration()
+            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 = new PlainJWT(builder.build()).serialize();
+
+        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+        assertNoEntityConfiguration(entityId);
+    }
+
+    @Test
+    public void testEncryptedEntityConfiguration()
+            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 SignedJWT signedJwt = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build());
+        final JWEObject jwe = new JWEObject(new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM)
+                    .contentType("JWT")
+                    .build(),
+                new Payload(signedJwt));
+        try {
+            jwe.encrypt(new RSAEncrypter(leafKey.toRSAKey()));
+        } catch (JOSEException e) {
+            Assert.fail("Encryption failed", e);
+        }
+        final String entityConfiguration = jwe.serialize();
+
+        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+        assertNoEntityConfiguration(entityId);
+    }
+
     @Test
     public void testWithUnrecognizedCriticalClaim()
             throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
@@ -299,6 +351,77 @@ public class EntityConfigurationMetadataCacheTest extends AbstractFederationFlow
         assertNoEntityConfiguration(entityId);
     }
 
+    @Test
+    public void testEmptyAuthorityHintsClaim()
+            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("authority_hints", CollectionSupport.emptyList())
+                .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+        final String entityConfiguration = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+        assertNoEntityConfiguration(entityId);
+    }
+
+    @Test
+    public void testEmptyTrustAnchorHintsClaim()
+            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("trust_anchor_hints", CollectionSupport.emptyList())
+                .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+        final String entityConfiguration = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+        assertNoEntityConfiguration(entityId);
+    }
+
+    @Test
+    public void testNullFederationEntityMetadata()
+            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+        final String entityId = uniqueClientId();
+        final Map<String, Object> metadata = new HashMap<>();
+        metadata.put("federation_entity", null);
+        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", metadata);
+        final String entityConfiguration = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+        assertNoEntityConfiguration(entityId);
+    }
+
+    @Test
+    public void testNullCustomMetadata()
+            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+        final String entityId = uniqueClientId();
+        final Map<String, Object> metadata = new HashMap<>();
+        metadata.put("federation_entity", Collections.emptyMap());
+        metadata.put("custom_type", null);
+        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", metadata);
+        final String entityConfiguration = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+        assertNoEntityConfiguration(entityId);
+    }
+
     @Test
     public void testNullResponse()
             throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
@@ -316,6 +439,102 @@ public class EntityConfigurationMetadataCacheTest extends AbstractFederationFlow
         assertNoEntityConfiguration(entityId);
     }
 
+    @Test
+    public void testInvalidtTrustMark_nonMatchingType()
+            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+        final String entityId = uniqueClientId();
+        final String trustMark = TrustChainTestUtil.trustMark(JWSAlgorithm.RS256, trustMarkIssuerKey, trustMarkIssuerId,
+                entityId, "https://example.org/email-allowing-trust-mark", Instant.now().plusSeconds(300)).serialize();
+        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("trust_marks", List.of(Map.of(
+                        "trust_mark_type", "https://example.org/non-matching-type",
+                        "trust_mark", trustMark)))
+                .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+        final String entityConfiguration = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+        assertNoEntityConfiguration(entityId);
+    }
+
+    @Test
+    public void testInvalidtTrustMarkIssuers_invalidFormat()
+            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("trust_mark_issuers", List.of(trustMarkIssuerId))
+                .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+        final String entityConfiguration = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+        assertNoEntityConfiguration(entityId);
+    }
+
+    @Test
+    public void testInvalidtTrustMarkOwners_invalidJwks()
+            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+        final String entityId = uniqueClientId();
+        final Map<String,Object> trustMarkOwner = new HashMap<>();
+        trustMarkOwner.put("sub", trustMarkIssuerId);
+        trustMarkOwner.put("jwks", Collections.emptyMap());
+        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("trust_mark_owners", Map.of("https://example.org/email-allowing-trust-mark", trustMarkOwner))
+                .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+        final String entityConfiguration = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+        assertNoEntityConfiguration(entityId);
+    }
+
+    @Test
+    public void testInvalidtTrustMarkOwners_missingJwks()
+            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+        final String entityId = uniqueClientId();
+        final Map<String,Object> trustMarkOwner = new HashMap<>();
+        trustMarkOwner.put("sub", trustMarkIssuerId);
+        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("trust_mark_owners", Map.of("https://example.org/email-allowing-trust-mark", trustMarkOwner))
+                .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+        final String entityConfiguration = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+        assertNoEntityConfiguration(entityId);
+    }
+
+    @Test
+    public void testInvalidtTrustMarkOwners_missingSub()
+            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+        final String entityId = uniqueClientId();
+        final Map<String,Object> trustMarkOwner = new HashMap<>();
+        trustMarkOwner.put("jwks", new JWKSet(leafKey).toJSONObject(true));
+        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("trust_mark_owners", Map.of("https://example.org/email-allowing-trust-mark", trustMarkOwner))
+                .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+        final String entityConfiguration = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build()).serialize();
+
+        mapResponse(entityConfigurationUrl(entityId), mockResponse(entityConfiguration));
+        assertNoEntityConfiguration(entityId);
+    }
+
     protected void assertNoEntityConfiguration(final String entityId) {
         try {
             final List<EntityConfigurationContainer> result =
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/SubordinateStatementMetadataCacheTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/SubordinateStatementMetadataCacheTest.java
index cb71ce2..0f5b20a 100644
--- a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/SubordinateStatementMetadataCacheTest.java
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/cache/SubordinateStatementMetadataCacheTest.java
@@ -23,6 +23,7 @@ import java.net.URISyntaxException;
 import java.time.Instant;
 import java.util.Collections;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -31,9 +32,18 @@ import org.springframework.beans.factory.annotation.Qualifier;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
+import com.nimbusds.jose.EncryptionMethod;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWEAlgorithm;
+import com.nimbusds.jose.JWEHeader;
+import com.nimbusds.jose.JWEObject;
 import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.Payload;
+import com.nimbusds.jose.crypto.RSAEncrypter;
 import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
+import com.nimbusds.jwt.SignedJWT;
 
 import net.shibboleth.idp.plugin.oidc.op.oidfed.TrustChainTestUtil;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubordinateStatement;
@@ -128,6 +138,51 @@ public class SubordinateStatementMetadataCacheTest extends AbstractFederationFlo
         }
     }
 
+    @Test
+    public void testUnsignedSubordinateStatement()
+            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+        final String entityId = uniqueClientId();
+        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 subordinateStatement = new PlainJWT(builder.build()).serialize();
+
+        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+                mockResponse(subordinateStatement));
+        assertNoSubordinateStatement(entityId);
+    }
+
+    @Test
+    public void testEncryptedSubordinateStatement()
+            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+        final String entityId = uniqueClientId();
+        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 SignedJWT signedJwt = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, leafKey, "entity-statement+jwt", builder.build());
+        final JWEObject jwe = new JWEObject(new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM)
+                    .contentType("JWT")
+                    .build(),
+                new Payload(signedJwt));
+        try {
+            jwe.encrypt(new RSAEncrypter(leafKey.toRSAKey()));
+        } catch (JOSEException e) {
+            Assert.fail("Encryption failed", e);
+        }
+        final String subordinateStatement = jwe.serialize();
+
+        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+                mockResponse(subordinateStatement));
+        assertNoSubordinateStatement(entityId);
+    }
+
     @Test
     public void testWithUnrecognizedCriticalClaim()
             throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
@@ -293,6 +348,164 @@ public class SubordinateStatementMetadataCacheTest extends AbstractFederationFlo
         assertNoSubordinateStatement(entityId);
     }
 
+    @Test
+    public void testInvalidConstraints_maxPathLength()
+            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+        final String entityId = uniqueClientId();
+        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("constraints", Map.of("max_path_length", "non_integer"))
+                .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+        final String subordinateStatement = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
+
+        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+                mockResponse(subordinateStatement));
+        assertNoSubordinateStatement(entityId);
+    }
+
+    @Test
+    public void testInvalidConstraints_namingConstraint()
+            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+        final String entityId = uniqueClientId();
+        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("constraints", Map.of("naming_constraints", "not_map"))
+                .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+        final String subordinateStatement = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
+
+        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+                mockResponse(subordinateStatement));
+        assertNoSubordinateStatement(entityId);
+    }
+
+    @Test
+    public void testInvalidConstraints_allowedEntityTypes()
+            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+        final String entityId = uniqueClientId();
+        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("constraints", Map.of("allowed_entity_types", "not_list"))
+                .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+        final String subordinateStatement = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
+
+        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+                mockResponse(subordinateStatement));
+        assertNoSubordinateStatement(entityId);
+    }
+
+    @Test
+    public void testValidConstraints()
+            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+        final String entityId = uniqueClientId();
+        final Map<String,Object> constraints = new HashMap<>();
+        constraints.put("max_path_length", 5);
+        constraints.put("allowed_entity_types", List.of("openid_relying_party"));
+        constraints.put("naming_constraints", Map.of("permitted", List.of(".example.com")));
+        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("constraints", constraints)
+                .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+        final String subordinateStatement = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
+
+        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+                mockResponse(subordinateStatement));
+        try {
+            final List<SubordinateStatementContainer> 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);
+            final SubordinateStatement statement = result.get(0).getStatement();
+            Assert.assertNotNull(statement);
+            assert statement != null;
+            Assert.assertNull(statement.getParsedPayload().getCustomClaims()
+                    .get(CustomSubordinateStatementFilterStrategy.CUSTOM_CLAIM_NAME));
+        } catch (MetadataCacheException e) {
+            Assert.fail("Could not resolve entity configuration", e);
+        }
+    }
+
+    @Test
+    public void testForbiddenClaim_trustMark()
+            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+        final String entityId = uniqueClientId();
+        final String trustMark = TrustChainTestUtil.trustMark(JWSAlgorithm.RS256, trustMarkIssuerKey, trustMarkIssuerId,
+                entityId, "https://example.org/email-allowing-trust-mark", Instant.now().plusSeconds(300)).serialize();
+        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("trust_marks", List.of(Map.of(
+                        "trust_mark_type", "https://example.org/email-allowing-trust-mark",
+                        "trust_mark", trustMark)))
+                .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+        final String subordinateStatement = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
+
+        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+                mockResponse(subordinateStatement));
+        assertNoSubordinateStatement(entityId);
+    }
+
+    @Test
+    public void testForbiddenClaim_trustMarkIssuers()
+            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+        final String entityId = uniqueClientId();
+        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("trust_mark_issuers", List.of(trustMarkIssuerId))
+                .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+        final String subordinateStatement = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
+
+        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+                mockResponse(subordinateStatement));
+        assertNoSubordinateStatement(entityId);
+    }
+
+    @Test
+    public void testForbiddenClaim_trustMarkOwners()
+            throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {
+        final String entityId = uniqueClientId();
+        final Map<String,Object> trustMarkOwner = new HashMap<>();
+        trustMarkOwner.put("sub", trustMarkIssuerId);
+        trustMarkOwner.put("jwks", new JWKSet(leafKey).toJSONObject(true));
+        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("trust_mark_owners", Map.of("https://example.org/email-allowing-trust-mark", trustMarkOwner))
+                .claim("metadata", Map.of("federation_entity", Collections.emptyMap()));
+        final String subordinateStatement = TrustChainTestUtil.signedJwt(
+                JWSAlgorithm.RS256, trustedAnchorKey, "entity-statement+jwt", builder.build()).serialize();
+
+        mapResponse(entityConfigurationUrl(anchorId), mockResponse(trustedAnchorConfiguration()));
+        mapResponse(subordinateStatementUrl(anchorFetchEndpoint, entityId),
+                mockResponse(subordinateStatement));
+        assertNoSubordinateStatement(entityId);
+    }
+
     @Test
     public void testInvalidJwksClaim()
             throws MetadataCacheException, UnsupportedOperationException, IOException, URISyntaxException {

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


More information about the commits mailing list