[java-oidfed-common] 01/03: Import generic security classes from java-idp-plugin-oidc-op-oidfed

Codeberg noreply at shibboleth.net
Fri May 15 07:49:29 UTC 2026


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

codeberg pushed a commit to branch main
in repository java-oidfed-common.

View the commit online:
https://codeberg.org/Shibboleth/java-oidfed-common/commit/c0302fcf1921b3455fcbecc867db170b3f249947

commit c0302fcf1921b3455fcbecc867db170b3f249947
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri May 15 09:47:15 2026 +0300

    Import generic security classes from java-idp-plugin-oidc-op-oidfed
    
    Following impl packages were imported:
    - net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential
    - net.shibboleth.idp.plugin.oidc.op.oidfed.security.jose.impl
    - net.shibboleth.idp.plugin.oidc.op.oidfed.security.jwt.claims.impl
    The packages were renamed with the following login: 'net.shibboleth.idp.plugin.oidc.op.oidfed' -> 'net.shibboleth.oidfed'
---
 .../support/ClientInformationExtensionSupport.java |  95 +++++++++++++++
 oidfed-common-impl/pom.xml                         |  10 ++
 ...ormationFederationEntityCredentialResolver.java | 127 +++++++++++++++++++++
 ...faultEntityConfigurationCredentialResolver.java |  90 +++++++++++++++
 .../DefaultLocalTrustAnchorCredentialResolver.java | 109 ++++++++++++++++++
 ...DefaultPayloadJOSEObjectCredentialResolver.java |  84 ++++++++++++++
 ...yloadSignatureValidationCredentialResolver.java |  82 +++++++++++++
 ...aultSubordinateStatementCredentialResolver.java | 121 ++++++++++++++++++++
 .../DefaultTrustMarkOwnerCredentialResolver.java   |  91 +++++++++++++++
 .../DefaultTrustMarkStatusCredentialResolver.java  | 102 +++++++++++++++++
 ...ticationSignatureSigningParametersResolver.java | 117 +++++++++++++++++++
 .../impl/ConstraintsSyntaxClaimsValidator.java     | 100 ++++++++++++++++
 .../jwt/claims/impl/CritClaimsValidator.java       |  97 ++++++++++++++++
 ...faultMetadataPolicyOperatorsLookupStrategy.java | 103 +++++++++++++++++
 .../impl/MetadataPolicyCritClaimsValidator.java    | 105 +++++++++++++++++
 .../impl/NonEmptyStringArrayClaimsValidator.java   |  88 ++++++++++++++
 .../impl/TrustMarkOwnersClaimsValidator.java       |  81 +++++++++++++
 .../jwt/claims/impl/TrustMarksClaimsValidator.java | 109 ++++++++++++++++++
 18 files changed, 1711 insertions(+)

diff --git a/oidfed-common-api/src/main/java/net/shibboleth/oidfed/support/ClientInformationExtensionSupport.java b/oidfed-common-api/src/main/java/net/shibboleth/oidfed/support/ClientInformationExtensionSupport.java
new file mode 100644
index 0000000..42584b8
--- /dev/null
+++ b/oidfed-common-api/src/main/java/net/shibboleth/oidfed/support/ClientInformationExtensionSupport.java
@@ -0,0 +1,95 @@
+/*
+ * 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.oidfed.support;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+
+/**
+ * Helper methods for our client information extensions related to OpenID federation.
+ */
+public class ClientInformationExtensionSupport {
+
+    /** Identifier for validated trust anchor within client information. */
+    @Nonnull @NotEmpty public static final String KEY_VALIDATED_TRUST_ANCHOR = "oidfed_validated_trust_anchor";
+
+    /** Identifier for validated trust chain within client information. */
+    @Nonnull @NotEmpty public static final String KEY_VALIDATED_TRUST_CHAIN = "oidfed_validated_trust_chain";
+
+    /** Identifier for validated trust mark IDs within client information. */
+    @Nonnull @NotEmpty public static final String KEY_VALIDATED_TRUST_MARK_IDS = "oidfed_validated_trust_mark_ids";
+
+    /**
+     * Parse validated trust anchor from the given client information.
+     * 
+     * @param clientInformation client information
+     * @return validated trust anchor
+     */
+    @Nullable
+    public static String parseValidatedTrustAnchor(@Nonnull final OIDCClientInformation clientInformation) {
+        return Optional.ofNullable(clientInformation.getOIDCMetadata().getCustomField(KEY_VALIDATED_TRUST_ANCHOR))
+                .filter(String.class::isInstance)
+                .map(obj -> obj.toString())
+                .orElse(null);
+    }
+
+    /**
+     * Parse validated trust chain from the given client information.
+     * 
+     * @param clientInformation client information
+     * @return validated trust chain
+     */
+    @Nullable
+    public static List<String> parseValidatedTrustChain(@Nonnull final OIDCClientInformation clientInformation) {
+        return Optional.ofNullable(clientInformation.getOIDCMetadata().getCustomField(KEY_VALIDATED_TRUST_CHAIN))
+                .filter(List.class::isInstance)
+                .map(obj -> (List<?>) obj)
+                .map(list -> list.stream().map(String.class::cast).toList())
+                .orElse(null);
+    }
+
+    /**
+     * Parse validated trust mark IDs from the given client information..
+     * 
+     * @param clientInformation client information
+     * @return validated trust mark IDs
+     */
+    @Nullable public static Map<String, List<String>> parseValidatedTrustMarkIds(
+            @Nonnull final OIDCClientInformation clientInformation) {
+        return Optional.ofNullable(clientInformation.getOIDCMetadata().getCustomField(KEY_VALIDATED_TRUST_MARK_IDS))
+                .filter(Map.class::isInstance)
+                .map(obj -> (Map<?,?>) obj)
+                .map(map -> map.entrySet().stream()
+                        .filter(entry -> entry.getKey() instanceof String)
+                        .filter(entry -> entry.getValue() instanceof List<?>)
+                        .collect(Collectors.toMap(entry -> entry.getKey().toString(),
+                                entry -> ((List<?>) entry.getValue()).stream()
+                                .filter(Objects::nonNull)
+                                .map(Objects::toString)
+                                .toList())))
+                .orElse(null);
+    }
+
+}
diff --git a/oidfed-common-impl/pom.xml b/oidfed-common-impl/pom.xml
index 2558cd5..008ee7a 100644
--- a/oidfed-common-impl/pom.xml
+++ b/oidfed-common-impl/pom.xml
@@ -80,6 +80,16 @@
             <groupId>${opensaml.groupId}</groupId>
             <artifactId>opensaml-security-api</artifactId>
             <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-security-impl</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-xmlsec-impl</artifactId>
+            <scope>provided</scope>
         </dependency>
          <dependency>
             <groupId>${opensaml.groupId}</groupId>
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/ClientInformationFederationEntityCredentialResolver.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/ClientInformationFederationEntityCredentialResolver.java
new file mode 100644
index 0000000..0c1fcdc
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/ClientInformationFederationEntityCredentialResolver.java
@@ -0,0 +1,127 @@
+/*
+ * 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.oidfed.security.credential;
+
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.credential.Credential;
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import net.shibboleth.oidc.security.credential.JOSEObjectCredentialResolver;
+import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
+import net.shibboleth.oidc.security.jose.criterion.ClientInformationCriterion;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.cache.SubjectEntityStatementCriterion;
+import net.shibboleth.oidfed.metadata.util.EntityStatementHelper;
+import net.shibboleth.oidfed.support.ClientInformationExtensionSupport;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+
+/**
+ * A {@link JOSEObjectCredentialResolver} that resolves credentials from the entity configuration payload. The entity
+ * configuration is fetched via client custom claim
+ * {@link ClientInformationExtensionSupport#KEY_VALIDATED_TRUST_CHAIN}.
+ */
+public class ClientInformationFederationEntityCredentialResolver extends BasicJOSEObjectCredentialResolver {
+    
+    /** Class logger. */
+    @Nonnull
+    private final Logger log = LoggerFactory.getLogger(ClientInformationFederationEntityCredentialResolver.class);
+
+    /** Resolver for fetching federation entity credentials from entity configuration. */
+    @Nonnull private final JOSEObjectCredentialResolver entityConfigurationCredentialResolver;
+
+    /** Object mapper used for deserializing jwks from the entity configuration payload. */
+    @Nonnull private final ObjectMapper objectMapper;
+
+    /**
+     * Constructor.
+     *
+     * @param resolver The resolver for fetching federation entity credentials from entity configuration.
+     * @param mapper The object mapper used for deserializing jwks from the entity configuration payload.
+     */
+    public ClientInformationFederationEntityCredentialResolver(@Nonnull
+            @ParameterName(name="entityConfigurationCredentialResolver") final JOSEObjectCredentialResolver resolver,
+            @Nonnull @ParameterName(name="objectMapper") final ObjectMapper mapper) {
+        entityConfigurationCredentialResolver = Constraint.isNotNull(resolver,
+                "EntityConfigurationCredentialResolver cannot be null");
+        objectMapper = Constraint.isNotNull(mapper, "ObjectMapper cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull protected Iterable<Credential> resolveFromSource(@Nullable final CriteriaSet criteriaSet)
+            throws ResolverException {
+        
+        Constraint.isNotNull(criteriaSet, "CriteriaSet was null");
+
+        if (criteriaSet != null) {
+            final ClientInformationCriterion clientCrit = criteriaSet.get(ClientInformationCriterion.class);
+            if (clientCrit != null) {
+                return resolveFromMetadata(criteriaSet, clientCrit.getOidcClientInformation());
+            }
+        }
+            
+        log.debug("Criteria did not contain a ClientInformationCriterion could not perform resolution");
+        return CollectionSupport.emptySet();
+    }
+    
+    /**
+     * Resolve the keyset from the entity configuration payload.
+     *
+     * @param criteriaSet the criteria set
+     * @param information the RP/Client information
+     * 
+     * @return a collection of credentials from the entity configuration key set (if any).
+     */
+    @Nonnull protected Iterable<Credential> resolveFromMetadata(@Nonnull final CriteriaSet criteriaSet, 
+            @Nonnull final OIDCClientInformation information) throws ResolverException {
+
+        final OIDCClientMetadata metadata = information.getOIDCMetadata();
+
+        if (metadata.getCustomField(ClientInformationExtensionSupport.KEY_VALIDATED_TRUST_CHAIN)
+                instanceof List<?> list) {
+            final List<String> serialized =
+                    list.stream().filter(String.class::isInstance).map(String.class::cast).toList();
+            assert serialized != null;
+            final List<EntityStatement<?>> trustChain =
+                    EntityStatementHelper.deserializeTrustChain(serialized, objectMapper);
+            if (trustChain != null) {
+                final EntityStatement<?> configuration = trustChain.get(0);
+                assert configuration != null;
+                final SubjectEntityStatementCriterion configurationCriterion =
+                        new SubjectEntityStatementCriterion(configuration);
+                log.debug("Returning credentials resolved via entity configuration credential resolver");
+                return entityConfigurationCredentialResolver.resolve(new CriteriaSet(configurationCriterion));
+            }
+        } else {
+            log.debug("Could not find the validated trust chain from the client metadata");
+        }
+        log.trace("Returning empty set of credentials");
+        return CollectionSupport.emptySet();
+    }
+    
+}
\ No newline at end of file
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultEntityConfigurationCredentialResolver.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultEntityConfigurationCredentialResolver.java
new file mode 100644
index 0000000..8ef9825
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultEntityConfigurationCredentialResolver.java
@@ -0,0 +1,90 @@
+/*
+ * 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.oidfed.security.credential;
+
+import java.util.List;
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.credential.Credential;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.jwk.JWKSet;
+
+import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.cache.SubjectEntityStatementCriterion;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+
+/**
+ * Resolves credentials from the jwks-claim located in the signed JWT (entity configuration) payload. The JWT is fetched
+ * via {@link SubjectEntityStatementCriterion}. If the JWT is not self-signed (i.e. it's a subordinate statement), a
+ * {@link ResolverException} is thrown.
+ */
+public class DefaultEntityConfigurationCredentialResolver extends BasicJOSEObjectCredentialResolver {
+
+    /** Logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultEntityConfigurationCredentialResolver.class);
+
+    /** {@inheritDoc} */
+    @Override
+    protected Iterable<Credential> resolveFromSource(@Nullable final CriteriaSet criteriaSet) throws ResolverException {
+        if (criteriaSet == null) {
+            throw new ResolverException("No criteria set supplied");
+        }
+
+        final List<Credential> result = parseJwkSet(criteriaSet).getKeys().stream()
+                .map(jwk -> jwk != null ? buildJWKCredential(jwk, null) : null)
+                .filter(Objects::nonNull)
+                .map(Credential.class::cast)
+                .toList();
+        assert result != null;
+        return result;
+    }
+
+    /**
+     * Parses the JWKSet from the given criteria set.
+     * 
+     * @param criteriaSet criteria set containing source JWT for the JWKSet
+     * @return the JWKSet parsed from the JWT payload
+     * @throws ResolverException if the JWKSet could not be parsed or found
+     */
+    @Nonnull protected JWKSet parseJwkSet(@Nonnull final CriteriaSet criteriaSet) throws ResolverException {
+        final SubjectEntityStatementCriterion subjectCriterion = criteriaSet.get(SubjectEntityStatementCriterion.class);
+        if (subjectCriterion == null) {
+            log.debug("No mandatory SubjectEntityStatementCriterion criteria supplied, resolver could not process");
+            throw new ResolverException(
+                    "Credential criteria set did not contain an instance of SubjectEntityStatementCriterion");
+        }
+        final JWKSet jwks;
+        final EntityStatement<?> subjectStatement = subjectCriterion.getValue();
+        if (subjectStatement.getSubject().equals(
+                subjectStatement.getIssuer())) {
+                    jwks = subjectStatement.getParsedPayload().getJwks();
+        } else {
+            throw new ResolverException(
+                    "Unexpected contents in the SubjectEntityStatementCriterion: subject does not match issuer");
+        }
+
+        if (jwks == null || jwks.isEmpty()) {
+            throw new ResolverException("Could not parse mandatory jwks");
+        }
+        return jwks;
+    }
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultLocalTrustAnchorCredentialResolver.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultLocalTrustAnchorCredentialResolver.java
new file mode 100644
index 0000000..fd7108a
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultLocalTrustAnchorCredentialResolver.java
@@ -0,0 +1,109 @@
+/*
+ * 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.oidfed.security.credential;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.credential.Credential;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jose.jwk.JWKSet;
+
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
+import net.shibboleth.oidfed.metadata.cache.SubjectEntityStatementCriterion;
+import net.shibboleth.oidfed.metadata.cache.local.LocalKeyContainer;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+
+/**
+ * Default resolver for trusted trust anchor key resolution. A configurable {@link MetadataCache} is used for fetching
+ * the trusted/local public credentials for the entity referred via {@link SubjectEntityStatementCriterion}.
+ */
+public class DefaultLocalTrustAnchorCredentialResolver extends BasicJOSEObjectCredentialResolver {
+
+    /** Logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultLocalTrustAnchorCredentialResolver.class);
+
+    /** Cache containing local copies of trusted trust anchor keys. */
+    @Nonnull private MetadataCache<Map<String, LocalKeyContainer>> localTrustAnchorsCache;
+
+    /**
+     * Constructor.
+     *
+     * @param cache cache containing local copies of trusted trust anchor keys
+     */
+    public DefaultLocalTrustAnchorCredentialResolver(
+            @Nonnull @ParameterName(name="cache") final MetadataCache<Map<String, LocalKeyContainer>> cache) {
+        localTrustAnchorsCache = Constraint.isNotNull(cache, "Local Trust Anchors cache cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected Iterable<Credential> resolveFromSource(@Nullable final CriteriaSet criteriaSet) throws ResolverException {
+        if (criteriaSet == null) {
+            throw new ResolverException("No criteria supplied");
+        }
+        final SubjectEntityStatementCriterion subjectCriterion = criteriaSet.get(SubjectEntityStatementCriterion.class);
+        if (subjectCriterion == null) {
+            log.debug("No SubjectEntityStatementCriterion criteria supplied, resolver could not process");
+            throw new ResolverException(
+                    "Credential criteria set did not contain an instance of SubjectEntityStatementCriterion");
+        }
+        final String entityId = subjectCriterion.getValue().getIssuer();
+        log.debug("Attempting to find trusted keys for {}", entityId);
+        
+        final List<Map<String, LocalKeyContainer>> keyContainers;
+        try {
+            keyContainers = localTrustAnchorsCache.get(criteriaSet);
+        } catch (final MetadataCacheException e) {
+            throw new ResolverException("Could not resolve local trust anchor keys from the cache", e);
+        }
+        if (keyContainers.isEmpty() || !keyContainers.get(0).containsKey(entityId)) {
+            log.debug("No keys found for {}", entityId);
+            return CollectionSupport.emptyList();
+        }
+        final LocalKeyContainer keyContainer = keyContainers.get(0).get(entityId);
+        if (keyContainer == null || keyContainer.getJWKSet() == null) {
+            log.debug("No JWKSet found for {}", entityId);
+            return CollectionSupport.emptyList();
+        }
+        final JWKSet jwkSet = keyContainer.getJWKSet();
+        final List<Credential> credentials = new ArrayList<>();
+        assert jwkSet != null;
+        for (final JWK jwk : jwkSet.getKeys()) {
+            if (jwk != null) {
+                final Credential cred = buildJWKCredential(jwk, null);
+                if (cred != null) {
+                    credentials.add(cred);
+                }
+            }
+        }
+        log.debug("Returning credentials {} for {}", credentials, entityId);
+        return credentials;
+    }
+
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultPayloadJOSEObjectCredentialResolver.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultPayloadJOSEObjectCredentialResolver.java
new file mode 100644
index 0000000..b6d234d
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultPayloadJOSEObjectCredentialResolver.java
@@ -0,0 +1,84 @@
+/*
+ * 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.oidfed.security.credential;
+
+import java.text.ParseException;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.credential.Credential;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.JOSEObject;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
+import net.shibboleth.oidc.security.jose.criterion.JOSEObjectCriterion;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+
+/**
+ * Resolves credentials from the jwks-claim located in the signed JWT (entity statement) payload. The JWT is fetched
+ * via {@link JOSEObjectCriterion}.
+ */
+public class DefaultPayloadJOSEObjectCredentialResolver extends BasicJOSEObjectCredentialResolver {
+
+    /** Logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultPayloadJOSEObjectCredentialResolver.class);
+
+    /** {@inheritDoc} */
+    @Override
+    protected Iterable<Credential> resolveFromSource(@Nullable final CriteriaSet criteriaSet) throws ResolverException {
+        if (criteriaSet == null || !criteriaSet.contains(JOSEObjectCriterion.class)) {
+            throw new ResolverException("CriteriaSet does not contain JOSEObjectCriterion");
+        }
+
+        final JOSEObjectCriterion joseObjectCriteria = criteriaSet.get(JOSEObjectCriterion.class);
+        assert joseObjectCriteria != null;
+        final JOSEObject joseObject = joseObjectCriteria.getJOSEObject();
+        if (joseObject == null) {
+            throw new ResolverException("JOSEObjectCriterion did not contain an instance of JOSEObject");
+        }
+        try {
+            final SignedJWT jwt = SignedJWT.parse(joseObject.serialize());
+            final Map<String, Object> rawJwks = jwt.getJWTClaimsSet().getJSONObjectClaim("jwks");
+            if (rawJwks == null || rawJwks.isEmpty()) {
+                log.debug("No jwks found from the payload");
+                return CollectionSupport.emptyList();
+            }
+            final JWKSet jwks = JWKSet.parse(rawJwks);
+            final List<Credential> result = jwks.getKeys().stream()
+                    .filter(Objects::nonNull)
+                    .filter(jwk -> jwk.getAlgorithm() != null ? 
+                            jwk.getAlgorithm().equals(jwt.getHeader().getAlgorithm()) : true)
+                    .map(jwk -> jwk != null ? buildJWKCredential(jwk, null) : null)
+                    .filter(Objects::nonNull)
+                    .map(Credential.class::cast)
+                    .toList();
+            assert result != null;
+            return result;
+        } catch (final ParseException e) {
+            throw new ResolverException("Could not parse JWKSet from JOSEObject", e);
+        }
+    }
+
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultPayloadSignatureValidationCredentialResolver.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultPayloadSignatureValidationCredentialResolver.java
new file mode 100644
index 0000000..af34685
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultPayloadSignatureValidationCredentialResolver.java
@@ -0,0 +1,82 @@
+/*
+ * 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.oidfed.security.credential;
+
+import java.text.ParseException;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.credential.Credential;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
+import net.shibboleth.oidfed.metadata.cache.SignatureValidationKeyContainerJwtCriterion;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+
+/**
+ * Resolves credentials from the jwks-claim located in the signed JWT (entity statement) payload. The JWT is fetched
+ * via {@link SignatureValidationKeyContainerJwtCriterion}.
+ */
+public class DefaultPayloadSignatureValidationCredentialResolver extends BasicJOSEObjectCredentialResolver {
+
+    /** Logger. */
+    @Nonnull
+    private final Logger log = LoggerFactory.getLogger(DefaultPayloadSignatureValidationCredentialResolver.class);
+
+    /** {@inheritDoc} */
+    @Override
+    protected Iterable<Credential> resolveFromSource(@Nullable final CriteriaSet criteriaSet) throws ResolverException {
+        if (criteriaSet == null || !criteriaSet.contains(SignatureValidationKeyContainerJwtCriterion.class)) {
+            throw new ResolverException("CriteriaSet does not contain SignatureValidationKeyContainerJwtCriterion");
+        }
+
+        final SignatureValidationKeyContainerJwtCriterion keyContainer =
+                criteriaSet.get(SignatureValidationKeyContainerJwtCriterion.class);
+        assert keyContainer != null;
+        final SignedJWT jwt = keyContainer.getJwt();
+        if (jwt == null) {
+            throw new ResolverException(
+                    "SignatureValidationKeyContainerJwtCriterion did not contain an instance of SignedJWT");
+        }
+        try {
+            final Map<String, Object> rawJwks = jwt.getJWTClaimsSet().getJSONObjectClaim("jwks");
+            if (rawJwks == null || rawJwks.isEmpty()) {
+                log.debug("No jwks found from the payload");
+                return CollectionSupport.emptyList();
+            }
+            final JWKSet jwks = JWKSet.parse(rawJwks);
+            final List<Credential> result =  jwks.getKeys().stream()
+                    .map(jwk -> jwk != null ? buildJWKCredential(jwk, null) : null)
+                    .filter(Objects::nonNull)
+                    .map(Credential.class::cast)
+                    .toList();
+            assert result != null;
+            return result;
+        } catch (final ParseException e) {
+            throw new ResolverException("Could not parse JWKSet from JOSEObject", e);
+        }
+    }
+
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultSubordinateStatementCredentialResolver.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultSubordinateStatementCredentialResolver.java
new file mode 100644
index 0000000..bb1ee2c
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultSubordinateStatementCredentialResolver.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.oidfed.security.credential;
+
+import java.util.List;
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.credential.Credential;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.jwk.JWKSet;
+
+import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.cache.IssuerEntityStatementCriterion;
+import net.shibboleth.oidfed.metadata.cache.SubjectEntityStatementCriterion;
+import net.shibboleth.oidfed.metadata.cache.SubjectStatementCriterion;
+import net.shibboleth.oidfed.metadata.payload.BaseExpirableSubjectPayload;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+
+/**
+ * Resolves credentials from the jwks-claim located in the signed JWT (issuer of a subordinate statement) payload.
+ * First, a JWT is fetched via {@link SubjectEntityStatementCriterion}. Its issuer must match with the entity
+ * statement fetched via {@link IssuerEntityStatementCriterion}. The issuer must be a self-signed statement.
+ */
+public class DefaultSubordinateStatementCredentialResolver extends BasicJOSEObjectCredentialResolver {
+
+    /** Logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultSubordinateStatementCredentialResolver.class);
+
+    /** {@inheritDoc} */
+    @Override
+    protected Iterable<Credential> resolveFromSource(@Nullable final CriteriaSet criteriaSet) throws ResolverException {
+        if (criteriaSet == null) {
+            throw new ResolverException("No criteria set supplied");
+        }
+
+        final List<Credential> result =  parseJwkSet(criteriaSet).getKeys().stream()
+                .filter(Objects::nonNull)
+                .map(jwk -> jwk != null ? buildJWKCredential(jwk, null) : null)
+                .filter(Objects::nonNull)
+                .map(Credential.class::cast)
+                .toList();
+        assert result != null;
+        return result;
+    }
+
+    /**
+     * Parses the JWKSet from the given criteria set.
+     * 
+     * @param criteriaSet criteria set containing source JWT for the JWKSet
+     * @return the JWKSet parsed from the JWT payload
+     * @throws ResolverException if the JWKSet could not be parsed or found
+     */
+    @Nonnull protected JWKSet parseJwkSet(@Nonnull final CriteriaSet criteriaSet) throws ResolverException {
+        final BaseExpirableSubjectPayload subjectPayload = getSubjectStatementPayload(criteriaSet);
+        if (subjectPayload == null) {
+            log.debug("No mandatory criteria supplied for resolving subject, resolver could not process");
+            throw new ResolverException(
+                    "Credential criteria set did not contain criterion to resolve subject");
+        }
+        final JWKSet jwks;
+        if (subjectPayload.getSubject().equals(subjectPayload.getIssuer())) {
+            throw new ResolverException(
+                    "Unexpected contents in the subject statement: subject matches issuer");
+        } else {
+            final IssuerEntityStatementCriterion issuerCriterion =
+                    criteriaSet.get(IssuerEntityStatementCriterion.class);
+            if (issuerCriterion == null) {
+                log.debug("No mandatory IssuerEntityStatementCriterion supplied, resolver could not process");
+                throw new ResolverException(
+                        "Credential criteria set did not contain an instance of IssuerEntityStatementCriterion");
+            }
+            final EntityStatement<?> issuerStatement = issuerCriterion.getValue();
+            if (!issuerStatement.getSubject().equals(subjectPayload.getIssuer())) {
+                throw new ResolverException("Credential criteria do not match for subject and issuer");
+            }
+            if (!issuerStatement.getSubject().equals(issuerStatement.getIssuer())) {
+                throw new ResolverException("Issuer entity statement is not self signed");
+            }
+            jwks = issuerStatement.getParsedPayload().getJwks();
+        }
+
+        if (jwks == null || jwks.isEmpty()) {
+            throw new ResolverException("Could not parse mandatory jwks");
+        }
+        return jwks;
+    }
+
+    private BaseExpirableSubjectPayload getSubjectStatementPayload(
+            @Nonnull final CriteriaSet criteriaSet) {
+        final SubjectEntityStatementCriterion entityStatementCriterion =
+                criteriaSet.get(SubjectEntityStatementCriterion.class);
+        if (entityStatementCriterion == null) {
+            final SubjectStatementCriterion subjectCriterion = criteriaSet.get(SubjectStatementCriterion.class);
+            if (subjectCriterion == null) {
+                return null;
+            }
+            return subjectCriterion.getValue().getParsedPayload() instanceof BaseExpirableSubjectPayload payload
+                    ? payload : null;
+        }
+        return entityStatementCriterion.getValue().getParsedPayload();
+    }
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultTrustMarkOwnerCredentialResolver.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultTrustMarkOwnerCredentialResolver.java
new file mode 100644
index 0000000..cb21704
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultTrustMarkOwnerCredentialResolver.java
@@ -0,0 +1,91 @@
+/*
+ * 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.oidfed.security.credential;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.credential.Credential;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jose.jwk.JWKSet;
+
+import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
+import net.shibboleth.oidfed.metadata.cache.SubjectEntityIDCriterion;
+import net.shibboleth.oidfed.metadata.cache.TrustMarkOwnersCriterion;
+import net.shibboleth.oidfed.metadata.payload.claim.TrustMarkOwner;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+
+/**
+ * Default resolver for trust anchor owner key resolution. A {@link TrustMarkOwnersCriterion} is used for fetching the
+ * credentials for the trust mark owner fetched via {@link SubjectEntityIDCriterion}.
+ */
+public class DefaultTrustMarkOwnerCredentialResolver extends BasicJOSEObjectCredentialResolver {
+
+    /** Logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultTrustMarkOwnerCredentialResolver.class);
+
+    /** {@inheritDoc} */
+    @Override
+    protected Iterable<Credential> resolveFromSource(@Nullable final CriteriaSet criteriaSet) throws ResolverException {
+        if (criteriaSet == null) {
+            throw new ResolverException("No criteria supplied");
+        }
+        final TrustMarkOwnersCriterion ownersCriterion = criteriaSet.get(TrustMarkOwnersCriterion.class);
+        if (ownersCriterion == null) {
+            log.debug("No TrustMarkOwnersCriterion criteria supplised, resolver could not process");
+            throw new ResolverException(
+                    "Credential criteria set did not contain an instance of TrustMarkOwnersCriterion");
+        }
+        final SubjectEntityIDCriterion subjectCriterion = criteriaSet.get(SubjectEntityIDCriterion.class);
+        if (subjectCriterion == null) {
+            log.debug("No SubjectEntityIDCriterion criteria supplied, resolver could not process");
+            throw new ResolverException(
+                    "Credential criteria set did not contain an instance of SubjectEntityIDCriterion");
+        }
+        final String entityId = subjectCriterion.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 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) {
+                    final Credential cred = buildJWKCredential(jwk, null);
+                    if (cred != null) {
+                        credentials.add(cred);
+                    }
+                }
+            }
+            log.debug("Returning credentials {} for {}", credentials, entityId);
+            return credentials;
+        }
+        log.debug("Could not find jwks from {}", ownerConfiguration);
+        return CollectionSupport.emptyList();        
+    }
+
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultTrustMarkStatusCredentialResolver.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultTrustMarkStatusCredentialResolver.java
new file mode 100644
index 0000000..a6509a2
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultTrustMarkStatusCredentialResolver.java
@@ -0,0 +1,102 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidfed.security.credential;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.credential.Credential;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.jwk.JWKSet;
+
+import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
+import net.shibboleth.oidfed.metadata.BasePayload;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.cache.IssuerEntityStatementCriterion;
+import net.shibboleth.oidfed.metadata.cache.SubjectStatementCriterion;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+
+/**
+ * Resolves credentials from the jwks-claim located in the signed JWT (issuer of a trust mark status) payload.
+ * First, a JWT is fetched via {@link SubjectStatementCriterion}. Its issuer must match with the entity
+ * statement fetched via {@link IssuerEntityStatementCriterion}.
+ */
+public class DefaultTrustMarkStatusCredentialResolver extends BasicJOSEObjectCredentialResolver {
+
+    /** Logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultTrustMarkStatusCredentialResolver.class);
+
+    /** {@inheritDoc} */
+    @Override
+    protected Iterable<Credential> resolveFromSource(@Nullable final CriteriaSet criteriaSet) throws ResolverException {
+        if (criteriaSet == null) {
+            throw new ResolverException("No criteria set supplied");
+        }
+
+        final List<Credential> result =  parseJwkSet(criteriaSet).getKeys().stream()
+                .filter(Objects::nonNull)
+                .map(jwk -> jwk != null ? buildJWKCredential(jwk, null) : null)
+                .filter(Objects::nonNull)
+                .map(Credential.class::cast)
+                .toList();
+        assert result != null;
+        return result;
+    }
+
+    /**
+     * Parses the JWKSet from the given criteria set.
+     * 
+     * @param criteriaSet criteria set containing source JWT for the JWKSet
+     * @return the JWKSet parsed from the JWT payload
+     * @throws ResolverException if the JWKSet could not be parsed or found
+     */
+    @Nonnull protected JWKSet parseJwkSet(@Nonnull final CriteriaSet criteriaSet) throws ResolverException {
+        final BasePayload subjectPayload = Optional.ofNullable(criteriaSet.get(SubjectStatementCriterion.class))
+                .map(criterion -> criterion.getValue().getParsedPayload()).orElse(null);
+        if (subjectPayload == null) {
+            log.debug("No mandatory criteria supplied for resolving subject, resolver could not process");
+            throw new ResolverException(
+                    "Credential criteria set did not contain criterion to resolve subject");
+        }
+        final IssuerEntityStatementCriterion issuerCriterion =
+                criteriaSet.get(IssuerEntityStatementCriterion.class);
+        if (issuerCriterion == null) {
+            log.debug("No mandatory IssuerEntityStatementCriterion supplied, resolver could not process");
+            throw new ResolverException(
+                    "Credential criteria set did not contain an instance of IssuerEntityStatementCriterion");
+        }
+        final EntityStatement<?> issuerStatement = issuerCriterion.getValue();
+        if (!issuerStatement.getSubject().equals(subjectPayload.getIssuer())) {
+            throw new ResolverException("Credential criteria do not match for subject and issuer");
+            }
+        if (!issuerStatement.getSubject().equals(issuerStatement.getIssuer())) {
+            throw new ResolverException("Issuer entity statement is not self signed");
+        }
+        final JWKSet  jwks = issuerStatement.getParsedPayload().getJwks();
+
+        if (jwks == null || jwks.isEmpty()) {
+            throw new ResolverException("Could not parse mandatory jwks");
+        }
+        return jwks;
+    }
+
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jose/impl/EndpointAuthenticationSignatureSigningParametersResolver.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jose/impl/EndpointAuthenticationSignatureSigningParametersResolver.java
new file mode 100644
index 0000000..5785158
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jose/impl/EndpointAuthenticationSignatureSigningParametersResolver.java
@@ -0,0 +1,117 @@
+/*
+ * 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.oidfed.security.jose.impl;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+
+import net.shibboleth.oidc.security.jose.SignatureSigningParametersResolver;
+import net.shibboleth.oidc.security.jose.impl.BasicSignatureSigningParametersResolver;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.cache.FederationEndpointEntityStatementCriterion;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * An implementation of an {@link SignatureSigningParametersResolver} that extends the {@link
+ * BasicSignatureSigningParametersResolver} functionality by adding a configurable lookup strategy for fetching
+ * the desired algorithm value from {@link EntityStatement}. It is expected to be found from the criteria set.
+ */
+public class EndpointAuthenticationSignatureSigningParametersResolver extends BasicSignatureSigningParametersResolver
+    implements SignatureSigningParametersResolver {
+    
+    /** Class logger.*/
+    @Nonnull
+    private final Logger log = LoggerFactory.getLogger(EndpointAuthenticationSignatureSigningParametersResolver.class);
+
+    /** The default algorithm value used if lookup strategy returned null. */
+    @Nullable private final String defaultAlgorithmValue;
+
+    /**
+     * Constructor.
+     *
+     * @param defaultValue the default algorithm value used if lookup strategy returned null
+     */
+    public EndpointAuthenticationSignatureSigningParametersResolver(
+            @Nullable @ParameterName(name = "defaultAlgorithmValue") final String defaultValue) {
+        defaultAlgorithmValue = defaultValue;
+    }
+    
+    
+    /**
+     * Get the effective list of signature algorithm URIs to consider, including application of 
+     * include/exclude policy.
+     * 
+     * @param criteria the input criteria being evaluated
+     * @param includeExcludePredicate  the include/exclude predicate to use
+     * @return the list of effective algorithm URIs
+     */
+    @Override
+    @Nonnull protected List<String> getEffectiveSignatureAlgorithms(@Nonnull final CriteriaSet criteria, 
+            @Nonnull final Predicate<String> includeExcludePredicate) {
+        final List<String> accumulator = super.getEffectiveSignatureAlgorithms(criteria, includeExcludePredicate);
+        final List<String> algorithms =
+                Optional.ofNullable(criteria.get(FederationEndpointEntityStatementCriterion.class))
+                .map(criterion -> criterion.getValue())
+                .map(statement -> statement.getParsedPayload().getMetadata())
+                .map(metadata -> metadata.getFederationEntityMetadata())
+                .map(entityMetadata -> entityMetadata.get("endpoint_auth_signing_alg_values_supported"))
+                .filter(algs -> algs instanceof List<?>)
+                .map(algs -> (List<?>) algs)
+                .map(algs -> algs.stream().filter(String.class::isInstance).map(String.class::cast).toList())
+                .orElse(null);
+        log.trace("Resolved algorithms via criteria set: {}", algorithms);
+        if (algorithms == null || algorithms.isEmpty()) {
+            if (StringSupport.trimOrNull(defaultAlgorithmValue) != null) {
+                log.debug("No algorithms resolved via criteria set, using default");
+                assert defaultAlgorithmValue != null;
+                return convertIntoListIfEnabled(defaultAlgorithmValue, accumulator);
+            } 
+            log.error("No algorithms resolved via criteria set");
+            return CollectionSupport.emptyList();
+        }
+
+        final List<String> result = algorithms.stream().filter(algorithm -> accumulator.contains(algorithm)).toList();
+        assert result != null;
+        return result;
+    }
+
+    /**
+     * Returns the given algorithm in a {@link List} if it was enabled in the list of enabled algorithms. An empty
+     * list is returned if the algorithm was not enabled.
+     * 
+     * @param algorithm the algorithm to be checked against the list
+     * @param enabledAlgorithms the list of enabled algorithms
+     * @return the given algorithm as list if it was enabled, or an empty list if not
+     */
+    @Nonnull protected List<String> convertIntoListIfEnabled(@Nonnull final String algorithm,
+            @Nonnull final List<String> enabledAlgorithms) {
+        if (enabledAlgorithms.contains(algorithm)) {
+            return CollectionSupport.listOf(algorithm);
+        } else {
+            log.warn("The algorithm {} is not enabled, returning empty list", algorithm);
+            return CollectionSupport.emptyList();
+        }
+    }
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/ConstraintsSyntaxClaimsValidator.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/ConstraintsSyntaxClaimsValidator.java
new file mode 100644
index 0000000..f24fe88
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/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.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/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/CritClaimsValidator.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/CritClaimsValidator.java
new file mode 100644
index 0000000..a7b84e7
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/CritClaimsValidator.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.oidfed.security.jwt.claims.impl;
+
+import java.text.ParseException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+
+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.ThreadSafeAfterInit;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.primitive.NonnullSupplier;
+
+/**
+ * A {@link ClaimsValidator} for validating that crit is not an empty array, does not contain standard claim names and
+ * only contain values that are configured as recognized.
+ */
+ at ThreadSafeAfterInit
+public class CritClaimsValidator extends AbstractClaimsValidator {
+
+    /** The list of standard operators that cannot be included in the metadata_policy_crit array. */
+    @Nonnull public static final List<String> STANDARD_CLAIMS = CollectionSupport.listOf(
+            "iss", "sub", "iat", "exp", "jwks", "metadata", "crit", "authority_hints", "trust_anchor_hints",
+            "trust_marks", "trust_mark_issuers", "trust_mark_owners", "constraints", "metadata_policy",
+            "metadata_policy_crit", "source_endpoint", "aud", "trust_anchor");
+
+    /** The collection of recognized claims. */
+    @Nonnull private Collection<String> recognizedClaims;
+
+    public CritClaimsValidator() {
+        recognizedClaims = CollectionSupport.emptyList();
+    }
+
+    /**
+     * Set the collection of recognized claims
+     * 
+     * @param claims recognized claims
+     */
+    public void setRecognizedClaims(@Nullable final Collection<String> claims) {
+        checkSetterPreconditions();
+        final Collection<String> list =
+                Optional.ofNullable(claims).orElseGet(NonnullSupplier.of(CollectionSupport.emptyList()));
+        assert list != null;
+        recognizedClaims = list;
+    }
+
+    /** {@inheritDoc} */
+    protected void doValidate(@Nonnull final JWTClaimsSet claims, 
+            @Nonnull final ProfileRequestContext context) throws JWTValidationException {
+        final List<String> values;
+        try {
+            values = claims.getStringListClaim("crit");
+        } catch (final ParseException e) {
+            throw new JWTValidationException("Could not parse crit into a list of strings");
+        }
+        if (values != null) {
+            if (values.isEmpty()) {
+                throw new JWTValidationException("Empty array is not allowed for crit");
+            }
+            for (final String value : values) {
+                if (values.indexOf(value) != values.lastIndexOf(value)) {
+                    throw new JWTValidationException(
+                            "Claim " + value + " is included more than once in crit");
+                }
+                if (STANDARD_CLAIMS.contains(value)) {
+                    throw new JWTValidationException(
+                            "Claim " + value + " is standard claim name and is not allowed in crit");
+                }
+                if (!recognizedClaims.contains(value)) {
+                    throw new JWTValidationException( "Claim " + value + " is not recognized for crit");
+                }
+            }
+        }
+    }
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/DefaultMetadataPolicyOperatorsLookupStrategy.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/DefaultMetadataPolicyOperatorsLookupStrategy.java
new file mode 100644
index 0000000..f483259
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/DefaultMetadataPolicyOperatorsLookupStrategy.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.oidfed.security.jwt.claims.impl;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.oidfed.metadata.policy.FederationMetadataPolicyOperator;
+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 lookup strategy to resolve list of supported metadata policy operators.
+ */
+public class DefaultMetadataPolicyOperatorsLookupStrategy extends AbstractIdentifiableInitializableComponent 
+    implements Function<ProfileRequestContext, List<FederationMetadataPolicyOperator>> {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(DefaultMetadataPolicyOperatorsLookupStrategy.class);
+
+    /** Map of supported operators by inbound message class name. */
+    @NonnullAfterInit private Map<String, List<FederationMetadataPolicyOperator>> supportedOperators;
+
+    /** List of default supported operators if inbound message class name was not mapped. */
+    @NonnullAfterInit private List<FederationMetadataPolicyOperator> defaultSupportedOperators;
+
+    /**
+     * Set the map of supported operators by inbound message class name.
+     * 
+     * @param operators map of supported operators
+     */
+    public void setSupportedOperators(@Nonnull final Map<String, List<FederationMetadataPolicyOperator>> operators) {
+        checkSetterPreconditions();
+        supportedOperators = Constraint.isNotNull(operators, "Map of supported operators cannot be null");
+    }
+
+    /**
+     * Set the list of default supported operators if inbound message class name was not mapped.
+     * 
+     * @param operators default list of supported operators
+     */
+    public void setDefaultSupportedOperators(@Nonnull List<FederationMetadataPolicyOperator> operators) {
+        checkSetterPreconditions();
+        defaultSupportedOperators =
+                Constraint.isNotNull(operators, "Default list of supported operators cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        if (supportedOperators == null) {
+            throw new ComponentInitializationException("Map of supported operators cannot be null");
+        }
+        if (defaultSupportedOperators == null) {
+            throw new ComponentInitializationException("Default list of supported operators cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public List<FederationMetadataPolicyOperator> apply(@Nullable final ProfileRequestContext profileRequestContext) {
+        checkComponentActive();
+        if (profileRequestContext == null || profileRequestContext.getInboundMessageContext() == null
+                || profileRequestContext.ensureInboundMessageContext().getMessage() == null)  {
+            log.warn("No inbound message resolved, returning default list of supported operators");
+        } else {
+            final Object message = profileRequestContext.ensureInboundMessageContext().getMessage();
+            assert message != null;
+            final String messageClassName = message.getClass().getCanonicalName();
+            if (supportedOperators.containsKey(messageClassName)) {
+                log.debug("Found message {} specific list of supported operators {}", messageClassName,
+                        supportedOperators.get(messageClassName));
+                return supportedOperators.get(messageClassName);
+            }
+            log.trace("No messsage class {} specific list found, returning default list of supported operators",
+                    messageClassName);
+        }
+        return defaultSupportedOperators;
+    }
+
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/MetadataPolicyCritClaimsValidator.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/MetadataPolicyCritClaimsValidator.java
new file mode 100644
index 0000000..3fe6b59
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/MetadataPolicyCritClaimsValidator.java
@@ -0,0 +1,105 @@
+/*
+ * 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.oidfed.security.jwt.claims.impl;
+
+import java.text.ParseException;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+
+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.oidfed.metadata.policy.FederationMetadataPolicyOperator;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A {@link ClaimsValidator} for validating that the metadata_policy_crit is not an empty array, does not contain
+ * standard operators and all the values are recognized by the resolved list of supported metadata policy operators.
+ */
+ at ThreadSafeAfterInit
+public class MetadataPolicyCritClaimsValidator extends AbstractClaimsValidator {
+
+    /** The list of standard operators that cannot be included in the metadata_policy_crit array. */
+    public static final List<String> STANDARD_OPERATORS = CollectionSupport.listOf(
+            "value", "add", "default", "one_of", "subset_of", "superset_of", "essential");
+
+    /** The lookup strategy for the list of supported metadata policy operators . */
+    @NonnullAfterInit private
+    Function<ProfileRequestContext, List<FederationMetadataPolicyOperator>> supportedOperatorsLookupStrategy;
+
+    /**
+     * Set the lookup strategy for the list of supported metadata policy operators.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setSupportedOperatorsLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, List<FederationMetadataPolicyOperator>> strategy) {
+        checkSetterPreconditions();
+        supportedOperatorsLookupStrategy =
+                Constraint.isNotNull(strategy, "Supported metadata policy operators lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        if (supportedOperatorsLookupStrategy == null) {
+            throw new ComponentInitializationException(
+                    "Supported metadata policy operators lookup strategy cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    protected void doValidate(@Nonnull final JWTClaimsSet claims, 
+            @Nonnull final ProfileRequestContext context) throws JWTValidationException {
+        final List<String> values;
+        try {
+            values = claims.getStringListClaim("metadata_policy_crit");
+        } catch (final ParseException e) {
+            throw new JWTValidationException("Could not parse metadata_policy_crit into a list of strings");
+        }
+        final List<String> supportedValues = Optional.ofNullable(supportedOperatorsLookupStrategy.apply(context))
+                .orElse(CollectionSupport.emptyList())
+                .stream().map(operator -> operator.getOperatorName()).toList();
+        if (values != null) {
+            if (values.isEmpty()) {
+                throw new JWTValidationException("Empty array is not allowed for metadata_policy_crit");
+            }
+            for (final String value : values) {
+                if (values.indexOf(value) != values.lastIndexOf(value)) {
+                    throw new JWTValidationException(
+                            "Claim " + value + " is included more than once in metadata_policy_crit");
+                }
+                if (STANDARD_OPERATORS.contains(value)) {
+                    throw new JWTValidationException(
+                            "Claim " + value + " is standard operator and is not allowed in metadata_policy_crit");
+                }
+                if (!supportedValues.contains(value)) {
+                    throw new JWTValidationException( "Claim " + value + " is not recognized in metadata_policy_crit");
+                }
+            }
+        }
+    }
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/NonEmptyStringArrayClaimsValidator.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/NonEmptyStringArrayClaimsValidator.java
new file mode 100644
index 0000000..e7e3ff9
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/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.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/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/TrustMarkOwnersClaimsValidator.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/TrustMarkOwnersClaimsValidator.java
new file mode 100644
index 0000000..51ab58c
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/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.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/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/TrustMarksClaimsValidator.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/TrustMarksClaimsValidator.java
new file mode 100644
index 0000000..89f1251
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/TrustMarksClaimsValidator.java
@@ -0,0 +1,109 @@
+/*
+ * 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.oidfed.security.jwt.claims.impl;
+
+import java.text.ParseException;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+
+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_marks claim. Each item ie the array must
+ * contain a match between the trust_mark_type claim and the corresponding claim inside the trust_mark JWT payload.
+ */
+ at ThreadSafeAfterInit
+public class TrustMarksClaimsValidator extends AbstractClaimsValidator {
+
+    /** {@inheritDoc} */
+    protected void doValidate(@Nonnull final JWTClaimsSet claims, 
+            @Nonnull final ProfileRequestContext context) throws JWTValidationException {
+        try {
+            final List<Object> trustMarks = claims.getListClaim("trust_marks");
+            if (trustMarks != null) {
+                for (final Object raw : trustMarks) {
+                    if (raw instanceof Map<?, ?> map) {
+                        final Map<String, String> trustMark = map.keySet().stream()
+                                .filter(String.class::isInstance)
+                                .map(String.class::cast)
+                                .filter(key -> map.get(key) instanceof String)
+                                .collect(Collectors.toMap(key -> key, key -> (String) map.get(key)));
+                        if (trustMark.isEmpty()) {
+                            throw new JWTValidationException("Unexpected contents for trust_marks array item: "
+                                    + "Could not parse a map of Strings");
+                        }
+                        validateContent(trustMark);
+                    } else {
+                        throw new JWTValidationException("Unexpected contents for trust_marks array item: "
+                                + "Could not parse a map of strings");
+                    }
+                }
+            }
+        } catch (final ParseException e) {
+            throw new JWTValidationException("Unexpected contents for trust_marks: could not parse an array", e);
+        }
+    }
+
+    /**
+     * Verifies that the given trust mark item meets the syntax requirements: the trust_mark content must be a signed
+     * JWT with a matching value for the trust_mark_type claim.
+     * 
+     * @param trustMark trust mark JSON object as a map
+     * @throws JWTValidationException if the syntax validation fails
+     */
+    protected void validateContent(@Nonnull final Map<String, String> trustMark) throws JWTValidationException {
+        final String trustMarkType = trustMark.get("trust_mark_type");
+        if (trustMarkType == null) {
+            throw new JWTValidationException("Unexpected contents for trust_marks array item: "
+                    + "trust_mark_type is null");
+        }
+        final SignedJWT trustMarkJwt = Optional.ofNullable(trustMark.get("trust_mark"))
+                .map(value -> {
+                    try {
+                        return SignedJWT.parse(value);
+                    } catch (final ParseException e) {
+                        return null;
+                    }
+                })
+                .orElse(null);
+        if (trustMarkJwt == null) {
+            throw new JWTValidationException("Unexpected contents for trust_marks array item: "
+                    + "trust_mark cannot be parsed into JWT");
+        }
+        try {
+            final String trustMarkTypeJwt = trustMarkJwt.getJWTClaimsSet().getStringClaim("trust_mark_type");
+            if (!trustMarkType.equals(trustMarkTypeJwt)) {
+                throw new JWTValidationException("Unexpected contents for trust_marks array item: "
+                        + "trust_mark_type " + trustMarkType + " does not match the JWT claim " + trustMarkTypeJwt);
+            }
+        } catch (final ParseException e) {
+            throw new JWTValidationException("Unexpected contents for trust_marks array item: "
+                    + "Could not parse trust_mark_type from JWT for trust_mark_type " +  trustMarkType, e);
+        }
+      
+    }
+}

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


More information about the commits mailing list