[java-idp-oidc] branch dev/JOIDC-222 updated: JOIDC-222 - Support for OpenID Federation

Henri Mikkonen henri.mikkonen at iki.fi
Fri Apr 25 13:24:24 UTC 2025


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

hjmikkon pushed a commit to branch dev/JOIDC-222
in repository java-idp-oidc.

View the commit online:
http://git.shibboleth.net/view/?p=java-idp-oidc.git;a=commit;h=e8a6e6d4b656cb925d141fef744557ca0c689d17

The following commit(s) were added to refs/heads/dev/JOIDC-222 by this push:
     new e8a6e6d4 JOIDC-222 - Support for OpenID Federation
e8a6e6d4 is described below

commit e8a6e6d4b656cb925d141fef744557ca0c689d17
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri Apr 25 16:24:07 2025 +0300

    JOIDC-222 - Support for OpenID Federation
    
    https://shibboleth.atlassian.net/browse/JOIDC-222
    
    Improved trust mark validation
    - Now capable of validating delegated trust marks
    - Exploit claim validators for iss, sub, iat, exp and trust_mark_id
      - May be customized via shibboleth.oidfed.TrustMarkClaimsValidationLookupStrategy and shibboleth.oidfed.DelegatedTrustMarkClaimsValidationLookupStrategy
---
 ...tChainTrustedTrustMarkOwnersLookupStrategy.java | 103 ++++++++
 .../DefaultTrustMarkOwnerCredentialResolver.java   |  97 ++++++++
 .../oidfed/metadata/TrustMarkOwnersCriterion.java  |  80 ++++++
 .../op/oidfed/profile/impl/ResolveTrustMarks.java  | 268 ++++++++++++++++++---
 .../flows/oidc/abstract/oidc-abstract-beans.xml    |  23 ++
 .../oidc/metadata-lookup/metadata-lookup-beans.xml |  15 +-
 .../idp/flows/oidfed/register/register-beans.xml   |  15 +-
 .../oidfed/resolve-entity/resolve-entity-beans.xml |  15 +-
 8 files changed, 573 insertions(+), 43 deletions(-)

diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainTrustedTrustMarkOwnersLookupStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainTrustedTrustMarkOwnersLookupStrategy.java
new file mode 100644
index 00000000..d1c7e030
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustChainTrustedTrustMarkOwnersLookupStrategy.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.type.MapType;
+import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
+
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Default function for fetching trusted trust mark owners from a trust chain: they are read from the trust anchor's
+ * entity configuration.
+ */
+public class DefaultTrustChainTrustedTrustMarkOwnersLookupStrategy extends AbstractIdentifiableInitializableComponent
+    implements Function<List<EntityStatement>, Map<String, Map<String, Object>>> {
+
+    /** Class logger. */
+    @Nonnull private final Logger log =
+            LoggerFactory.getLogger(DefaultTrustChainTrustedTrustMarkOwnersLookupStrategy.class);
+
+    /** JSON object mapper used for decoding JSON into Map. */
+    @NonnullAfterInit private ObjectMapper objectMapper;
+
+    /**
+     * Set the JSON {@link ObjectMapper} used for decoding JSON into Map.
+     * 
+     * @param mapper object mapper
+     */
+    public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+        checkSetterPreconditions();
+
+        objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        if (objectMapper == null) {
+            throw new ComponentInitializationException("Object mapper cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Nullable @Override
+    public Map<String, Map<String, Object>> apply(@Nullable final List<EntityStatement> trustChain) {
+        checkComponentActive();
+        if (trustChain == null || trustChain.size() < 3) {
+            return null;
+        }
+        final Object ownersClaim =
+                trustChain.get(trustChain.size() - 1).getClaimsSet().getClaim("trust_mark_owners");
+        log.debug("Raw trust_mark_owners claim {}", ownersClaim);
+        if (ownersClaim != null) {
+            final JavaType objectType = objectMapper.constructType(Object.class);
+            final JavaType stringType = objectMapper.constructType(String.class);
+            final MapType objectMapType =
+                    objectMapper.getTypeFactory().constructMapType(Map.class, stringType, objectType);
+            final MapType rootMapType =
+                    objectMapper.getTypeFactory().constructMapType(Map.class, stringType, objectMapType);
+            try {
+                final Map<String, Map<String, Object>> result =
+                        objectMapper.readValue(ownersClaim.toString(), rootMapType);
+                if (result != null) {
+                    log.debug("Parsed trust_mark_owners map {}", result);
+                    return result;
+                }
+            } catch (final JsonProcessingException e) {
+                log.warn("Could not parse trust mark issuers from the trust chain", e);
+            }
+        }
+        log.debug("Returning empty map");
+        return CollectionSupport.emptyMap();
+    }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustMarkOwnerCredentialResolver.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustMarkOwnerCredentialResolver.java
new file mode 100644
index 00000000..14c9d9c5
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/DefaultTrustMarkOwnerCredentialResolver.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
+
+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;
+
+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.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, Map<String, Object>> 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 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 parse jwks from {}", ownerConfiguration.get("jwks"));
+        return CollectionSupport.emptyList();        
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/TrustMarkOwnersCriterion.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/TrustMarkOwnersCriterion.java
new file mode 100644
index 00000000..56a19bbb
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/metadata/TrustMarkOwnersCriterion.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oidfed.metadata;
+
+import java.util.Map;
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.resolver.Criterion;
+
+/**
+ * A {@link Criterion} representing trust mark owners.
+ */
+public class TrustMarkOwnersCriterion implements Criterion {
+
+    /** The trust mark owners. */
+    @Nonnull private final Map<String, Map<String, Object>> owners;
+
+    /**
+     * Constructor.
+     *
+     * @param trustMarkOwners the truts mark owners, must not be null
+     */
+    public TrustMarkOwnersCriterion(@Nonnull final Map<String, Map<String, Object>> trustMarkOwners) {
+        owners = Constraint.isNotNull(trustMarkOwners, "Trust Mark owners cannot be null");
+    }
+
+    /**
+     * Get the trust mark owners value.
+     * 
+     * @return the trust mark owners value
+     */
+    @Nonnull
+    public Map<String, Map<String, Object>> getValue() {
+        return owners;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        return "TrustMarkOwnersCriterion [owners=" + owners + "]";
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int hashCode() {
+        return Objects.hash(owners);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final TrustMarkOwnersCriterion other = (TrustMarkOwnersCriterion) obj;
+        return owners.equals(other.owners);
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ResolveTrustMarks.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ResolveTrustMarks.java
index 34d729fb..0b194d61 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ResolveTrustMarks.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/profile/impl/ResolveTrustMarks.java
@@ -44,7 +44,10 @@ import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainTrustMarksParsingStrategy;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityIDCriterion;
 import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.SubjectEntityStatementCriterion;
+import net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.TrustMarkOwnersCriterion;
 import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
 import net.shibboleth.oidc.metadata.cache.MetadataCache;
 import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
@@ -82,6 +85,10 @@ public class ResolveTrustMarks extends AbstractProfileAction {
     @NonnullAfterInit
     private Function<List<EntityStatement>, Map<String, List<String>>> trustedTrustMarkIssuersLookupStrategy;
 
+    /** Strategy used to lookup trusted trust mark owners for the trust chain. */
+    @NonnullAfterInit
+    private Function<List<EntityStatement>, Map<String, Map<String, Object>>> trustedTrustMarkOwnersLookupStrategy;
+
     /** Condition to solely take trusted trust mark issuers into account. */
     @Nonnull private Predicate<ProfileRequestContext> trustedTrustMarkIssuersOnlyCondition;
 
@@ -91,18 +98,28 @@ public class ResolveTrustMarks extends AbstractProfileAction {
     /** Trust engine used to validate a trust mark signature. */
     @NonnullAfterInit private TrustEngine<SignedJWT> trustEngine;
 
+    /** Trust engine used to validate a delegated trust mark signature. */
+    @NonnullAfterInit private TrustEngine<SignedJWT> delegationTrustEngine;
+
+    /** Strategy used to lookup trust mark claims validator. */
+    @NonnullAfterInit private Function<ProfileRequestContext,ClaimsValidator> trustMarkClaimsValidationLookupStrategy;
+
+    /** Strategy used to lookup delegated trust mark claims validator. */
+    @NonnullAfterInit
+    private Function<ProfileRequestContext,ClaimsValidator> delegatedTrustMarkClaimsValidationLookupStrategy;
+
     /** Trust chain context to operate on. */
     @NonnullBeforeExec private RelyingPartyTrustChainContext trustChainContext;
 
     /** The selected trust chain to resolve trust marks from. */
     @NonnullBeforeExec private List<EntityStatement> selectedTrustChain;
 
-    /** Flag indicating that only trusted trust mark issuers are taken into account. */
-    private boolean onlyTrustedIssuers;
-
-    /** Map of trusted trust mark issuers. */
-    @NonnullBeforeExec private Map<String, List<String>> trustedIssuers;
+    /** Trust mark claims validator. */
+    @NonnullBeforeExec private ClaimsValidator trustMarkClaimsValidator;
 
+    /** Delegated trust mark claims validator. */
+    @NonnullBeforeExec private ClaimsValidator delegatedTrustMarkClaimsValidator;
+    
     /**
      * Constructor.
      */
@@ -140,6 +157,18 @@ public class ResolveTrustMarks extends AbstractProfileAction {
                 Constraint.isNotNull(strategy, "trustedTrustMarkIssuersLookupStrategy cannot be null");
     }
 
+    /**
+     * Set the strategy used to lookup trusted trust mark issuers for the trust chain.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setTrustedTrustMarkOwnersLookupStrategy(
+            @Nonnull final Function<List<EntityStatement>, Map<String, Map<String, Object>>> strategy) {
+        checkSetterPreconditions();
+        trustedTrustMarkOwnersLookupStrategy =
+                Constraint.isNotNull(strategy, "trustedTrustMarkOwnersLookupStrategy cannot be null");
+    }
+
     /**
      * Set the condition to solely take trusted trust mark issuers into account.
      * 
@@ -171,6 +200,40 @@ public class ResolveTrustMarks extends AbstractProfileAction {
         trustEngine = Constraint.isNotNull(engine, "Trust Engine cannot be null");
     }
 
+    /**
+     * Set trust engine used to validate a delegated trust mark signature.
+     * 
+     * @param engine trust engine
+     */
+    public void setDelegationTrustEngine(@Nonnull final TrustEngine<SignedJWT> engine) {
+        checkSetterPreconditions();
+        delegationTrustEngine = Constraint.isNotNull(engine, "Delegation Trust Engine cannot be null");
+    }
+
+    /**
+     * Set the strategy used to lookup trust mark claims validator.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setTrustMarkClaimsValidationLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, ClaimsValidator> strategy) {
+        checkSetterPreconditions();
+        trustMarkClaimsValidationLookupStrategy =
+                Constraint.isNotNull(strategy, "TrustMarkClaimsValidationLookupStrategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to lookup delegated trust mark claims validator.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setDelegatedTrustMarkClaimsValidationLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, ClaimsValidator> strategy) {
+        checkSetterPreconditions();
+        delegatedTrustMarkClaimsValidationLookupStrategy =
+                Constraint.isNotNull(strategy, "DelegatedTrustMarkClaimsValidationLookupStrategy cannot be null");
+    }
+
     /** {@inheritDoc} */
     @Override
     protected void doInitialize() throws ComponentInitializationException {
@@ -182,9 +245,22 @@ public class ResolveTrustMarks extends AbstractProfileAction {
         if (trustEngine == null) {
             throw new ComponentInitializationException("Trust Engine cannot be null");
         }
+        if (delegationTrustEngine == null) {
+            throw new ComponentInitializationException("Delegation Trust Engine cannot be null");
+        }
         if (trustedTrustMarkIssuersLookupStrategy == null) {
             throw new ComponentInitializationException("Trusted trust mark issuers lookup strategy cannot be null");
         }
+        if (trustedTrustMarkOwnersLookupStrategy == null) {
+            throw new ComponentInitializationException("Trusted trust mark owners lookup strategy cannot be null");
+        }
+        if (trustMarkClaimsValidationLookupStrategy == null) {
+            throw new ComponentInitializationException("TrustMarkClaimsValidationLookupStrategy cannot be null");
+        }
+        if (delegatedTrustMarkClaimsValidationLookupStrategy == null) {
+            throw new ComponentInitializationException(
+                    "DelegatedTrustMarkClaimsValidationLookupStrategy cannot be null");
+        }
     }
 
     /** {@inheritDoc} */
@@ -219,31 +295,55 @@ public class ResolveTrustMarks extends AbstractProfileAction {
             return false;
         }
 
-        onlyTrustedIssuers = trustedTrustMarkIssuersOnlyCondition.test(profileRequestContext);
-        trustedIssuers = Optional.ofNullable(trustedTrustMarkIssuersLookupStrategy.apply(selectedTrustChain))
-                .orElseGet(NonnullSupplier.of(CollectionSupport.emptyMap()));
+        trustMarkClaimsValidator = trustMarkClaimsValidationLookupStrategy.apply(profileRequestContext);
+        if (trustMarkClaimsValidator == null) {
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            log.error("{} Unable to locate trust mark claims validator", getLogPrefix());
+            return false;
+        }
 
-        if (trustedIssuers.isEmpty()) {
-            if (onlyTrustedIssuers) {
-                log.debug("{} No trusted issuers set in the selected trust anchor, trust marks won't be resolved",
-                        getLogPrefix());
-                return false;
-            }
+        delegatedTrustMarkClaimsValidator =
+                delegatedTrustMarkClaimsValidationLookupStrategy.apply(profileRequestContext);
+        if (delegatedTrustMarkClaimsValidator == null) {
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            log.error("{} Unable to locate delegated trust mark claims validator", getLogPrefix());
+            return false;
         }
+
         return true;
     }
 
     /** {@inheritDoc} */
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
-        log.debug("{} Trusted trust mark issuers {}", getLogPrefix(), trustedIssuers);
+        final boolean onlyTrustedIssuers = trustedTrustMarkIssuersOnlyCondition.test(profileRequestContext);
+
         final Map<String, List<SignedJWT>> chainTrustMarks =
-                trustChainTrustMarksParsingStrategy.apply(selectedTrustChain);
+                Optional.ofNullable(trustChainTrustMarksParsingStrategy.apply(selectedTrustChain))
+                    .orElseGet(NonnullSupplier.of(CollectionSupport.emptyMap()))
+                    .entrySet().stream()
+                    .collect(Collectors.toMap(entry -> entry.getKey(),
+                            entry -> entry.getValue().stream()
+                                .filter(trustMark ->
+                                        validateClaims(trustMarkClaimsValidator, trustMark, profileRequestContext))
+                            .toList()));
         if (chainTrustMarks == null || chainTrustMarks.isEmpty()) {
-            log.debug("{} No trust marks found from the selected trust chain", getLogPrefix());
+            log.debug("{} No valid trust marks found from the selected trust chain", getLogPrefix());
             return;
         }
 
+        final Map<String, List<String>> trustedIssuers =
+                Optional.ofNullable(trustedTrustMarkIssuersLookupStrategy.apply(selectedTrustChain))
+                    .orElseGet(NonnullSupplier.of(CollectionSupport.emptyMap()));
+        log.debug("{} Trusted trust mark issuers {}", getLogPrefix(), trustedIssuers);
+        assert trustedIssuers != null;
+
+        final Map<String, Map<String, Object>> trustedOwners =
+                Optional.ofNullable(trustedTrustMarkOwnersLookupStrategy.apply(selectedTrustChain))
+                    .orElseGet(NonnullSupplier.of(CollectionSupport.emptyMap()));
+        log.debug("{} Trusted trust mark owners {}", getLogPrefix(), trustedOwners);
+        assert trustedOwners != null;
+        
         final Map<String, List<SignedJWT>> verifiedTrustMarks = new HashMap<>();
         for (final EntityStatement statement : selectedTrustChain) {
             final List<SignedJWT> trustMarks = chainTrustMarks.get(statement.getEntityID().getValue());
@@ -253,8 +353,9 @@ public class ResolveTrustMarks extends AbstractProfileAction {
             verifiedTrustMarks.put(
                     statement.getEntityID().getValue(),
                     trustMarks.stream()
-                        .filter(entry -> checkTrustedIssuer(entry))
-                        .filter(entry -> verifyTrustMark(entry))
+                        .filter(entry -> onlyTrustedIssuers ?
+                                checkTrustedIssuer(entry, trustedIssuers, trustedOwners) : true)
+                        .filter(entry -> verifyTrustMark(entry, trustedOwners, profileRequestContext))
                         .filter(Objects::nonNull)
                         .toList());
         }
@@ -267,36 +368,71 @@ public class ResolveTrustMarks extends AbstractProfileAction {
         trustChainContext.setVerifiedTrustMarkIds(verifiedTrustMarkIds);
     }
 
+    /**
+     * Validates the given trust mark JWT against the given claims validator.
+     * 
+     * @param claimsValidator the claims validator (chain)
+     * @param jwt the trust mark
+     * @param profileRequestContext the profile request context
+     * @return true if validation succeeded, false otherwise
+     */
+    protected boolean validateClaims(@Nullable final ClaimsValidator claimsValidator, @Nullable final SignedJWT jwt,
+            @Nonnull final ProfileRequestContext profileRequestContext) {
+        if (claimsValidator == null || jwt == null) {
+            return false;
+        }
+        try {
+            final JWTClaimsSet claimsSet = jwt.getJWTClaimsSet();
+            assert claimsSet != null;
+            claimsValidator.validate(claimsSet, profileRequestContext);
+            return true;
+        } catch (final JWTValidationException | ParseException e) {
+            log.debug("{} Claims validation failed", getLogPrefix(), e);
+        }
+        return false;
+    }
+
     /**
      * Verifies the given trust mark meets configuration for trusted trust mark issuers.
      * 
      * @param jwt the trust mark to be verified
+     * @param trustedIssuers the trusted trust mark issuers
+     * @param trustedOwners the trusted trust mark owners
      * @return true if the trust mark meets configuration, false otherwise
      */
-    protected boolean checkTrustedIssuer(@Nullable final SignedJWT jwt) {
+    protected boolean checkTrustedIssuer(@Nullable final SignedJWT jwt,
+            @Nonnull final Map<String, List<String>> trustedIssuers,
+            @Nonnull final Map<String, Map<String, Object>> trustedOwners) {
         if (jwt == null) {
             return false;
         }
-        if (onlyTrustedIssuers) {
-            try {
-                final JWTClaimsSet claimsSet = jwt.getJWTClaimsSet();
-                final String id = StringSupport.trimOrNull(getTrustMarkId(jwt));
-                if (id == null) {
+        try {
+            final JWTClaimsSet claimsSet = jwt.getJWTClaimsSet();
+            final String id = StringSupport.trimOrNull(getTrustMarkId(jwt));
+            if (id == null) {
+                return false;
+            }
+            if (trustedIssuers.containsKey(id)) {
+                final String issuer = claimsSet.getIssuer();
+                assert issuer != null;
+                final List<String> validIssuers = trustedIssuers.get(id);
+                if (validIssuers == null || !validIssuers.contains(issuer)) {
+                    log.debug("{} Issuer {} is not valid trust mark issuer", getLogPrefix(), issuer);
                     return false;
                 }
-                if (trustedIssuers.containsKey(id)) {
-                    final String issuer = claimsSet.getIssuer();
-                    assert issuer != null;
-                    final List<String> validIssuers = trustedIssuers.get(id);
-                    if (onlyTrustedIssuers && (validIssuers == null || !validIssuers.contains(issuer)) ) {
-                        log.debug("{} Issuer {} is not valid trust mark issuer", getLogPrefix(), issuer);
-                        return false;
-                    }
+            } else if (trustedOwners.containsKey(id)) {
+                log.debug("{} Trust mark ID {} is included in trusted owners", getLogPrefix(), id);
+                if (claimsSet.getStringClaim("delegation") == null) {
+                    log.debug("(} Trust mark ID {} does not contain a delegation claim", getLogPrefix(), id);
+                    return false;
                 }
-            } catch (final ParseException e) {
-                log.error("Could not parse TrustMark JWT contents", e);
+            } else {
+                log.debug("{} Trust mark ID {} is not included in the trusted issuers", getLogPrefix(), id);
                 return false;
             }
+        } catch (final ParseException e) {
+            log.error("{} Could not parse TrustMark JWT contents", getLogPrefix(), e);
+            return false;
         }
         return true;
     }
@@ -306,9 +442,12 @@ public class ResolveTrustMarks extends AbstractProfileAction {
      * entity configuration and (2) the trust engine for validating the trust mark signature.
      * 
      * @param jwt the trust mark to be verified
+     * @param profileRequestContext the profile request context
      * @return true if trust mark verification was successful, false otherwise
      */
-    protected boolean verifyTrustMark(@Nullable final SignedJWT jwt) {
+    protected boolean verifyTrustMark(@Nullable final SignedJWT jwt,
+            @Nonnull final Map<String, Map<String, Object>> trustedOwners,
+            @Nonnull final ProfileRequestContext profileRequestContext) {
         if (jwt == null) {
             return false;
         }
@@ -321,7 +460,7 @@ public class ResolveTrustMarks extends AbstractProfileAction {
         }
         final String issuer = trustMarkClaims.getIssuer();
         assert issuer != null;
-        log.debug("Resolving trust chain for {}", issuer);
+        log.debug("{} Resolving trust chain for {}", getLogPrefix(), issuer);
         final List<List<List<EntityStatement>>> cacheResult;
         try {
             cacheResult = trustChainCache.get(new CriteriaSet(new SubjectEntityIDCriterion(issuer)));
@@ -339,11 +478,60 @@ public class ResolveTrustMarks extends AbstractProfileAction {
         final CriteriaSet criteria = new CriteriaSet(new SubjectEntityStatementCriterion(trustMarkIssuer));
         try {
             if (trustEngine.validate(jwt, criteria)) {
-                log.debug("Successfully validated trust mark issued by {}", issuer);
-                return true;
+                final String id = getTrustMarkId(jwt);
+                assert id != null;
+                log.debug("{} Successfully validated trust mark {} issued by {}", getLogPrefix(), id, issuer);
+                if (trustedOwners.containsKey(id)) {
+                    return validateDelegatedTrustMark(trustMarkClaims, id, trustedOwners, profileRequestContext);
+                } else {
+                    return true;
+                }
             }
         } catch (final SecurityException e) {
-            log.debug("Security exception while validating trust mark signature for {}", issuer, e);
+            log.debug("{} Security exception while validating trust mark signature for {}", getLogPrefix(), issuer, e);
+        }
+        return false;
+    }
+
+    /**
+     * Validates a delegated trust mark.
+     * 
+     * @param trustMarkClaims the claims set containing delegation claim
+     * @param id the trust mark identifier
+     * @param trustedOwners the trusted trust mark owners
+     * @param profileRequestContext the profile request context
+     * @return true if delegation JWT was valid, false otherwise
+     */
+    protected boolean validateDelegatedTrustMark(@Nonnull final JWTClaimsSet trustMarkClaims,
+            @Nonnull final String id,
+            @Nonnull final Map<String, Map<String, Object>> trustedOwners,
+            @Nonnull final ProfileRequestContext profileRequestContext) {
+        log.debug("{} Validating delegated trust mark {}", getLogPrefix(), id);
+        try {
+            final SignedJWT delegationJwt = SignedJWT.parse(trustMarkClaims.getStringClaim("delegation"));
+            final CriteriaSet delegationCriteria = new CriteriaSet(
+                    new TrustMarkOwnersCriterion(trustedOwners),
+                    new SubjectEntityIDCriterion(id));
+            if (validateClaims(delegatedTrustMarkClaimsValidator, delegationJwt, profileRequestContext)) {
+                assert delegationJwt != null;
+                if (delegationTrustEngine.validate(delegationJwt, delegationCriteria)) {
+                    final String issuer = delegationJwt.getJWTClaimsSet().getIssuer();
+                    log.debug("{} Successfully validated delegated {} signature issued by {}", getLogPrefix(), id,
+                            issuer);
+                    if (issuer != null && issuer.equals(trustMarkClaims.getSubject())) {
+                        return true;
+                    } else {
+                        log.debug("{} The issuer of the delegation {} does not match with the subject {}",
+                                getLogPrefix(), issuer, trustMarkClaims.getSubject());
+                    }
+                }
+            }
+        } catch (final SecurityException  e) {
+            log.debug("{} Security exception while validating trust mark signature for {}", getLogPrefix(),
+                    trustMarkClaims.getIssuer(), e);
+        } catch (final ParseException e) {
+            log.debug("{} Parsing exception while processing delegated trust mark from {}", getLogPrefix(),
+                    trustMarkClaims.getIssuer(), e);
         }
         return false;
     }
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract/oidc-abstract-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract/oidc-abstract-beans.xml
index 79b9ba1c..2b6a33a3 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract/oidc-abstract-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract/oidc-abstract-beans.xml
@@ -146,4 +146,27 @@
         p:activationCondition-ref="shibboleth.Conditions.FALSE">
     </bean>
 
+    <bean id="DefaultTrustMarkClaimsValidationLookupStrategy" parent="shibboleth.Functions.Constant">
+        <constructor-arg name="target">
+            <bean class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator">
+                <property name="claimValidators">
+                    <util:list value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
+                        <bean class="net.shibboleth.oidc.security.jwt.claims.impl.IssuedAtClaimsValidator"
+                            p:clockSkew="%{idp.policy.clockSkew:PT1M}"
+                            p:messageLifetime="%{idp.oidfed.maxTrustMarkifetime:P365D}"
+                            p:requiredRule="true" />
+                        <bean class="net.shibboleth.oidc.security.jwt.claims.impl.RequiredClaimsValidator"
+                            p:requiredClaims="iss" />
+                        <bean class="net.shibboleth.oidc.security.jwt.claims.impl.RequiredClaimsValidator"
+                            p:requiredClaims="sub" />
+                        <bean class="net.shibboleth.oidc.security.jwt.claims.impl.RequiredClaimsValidator"
+                            p:requiredClaims="trust_mark_id" />
+                        <bean class="net.shibboleth.oidc.security.jwt.claims.impl.ExpiryClaimsValidator"
+                            p:clockSkew="%{idp.policy.clockSkew:PT1M}" />
+                    </util:list>
+                </property>
+            </bean>
+        </constructor-arg>
+    </bean>
+
 </beans>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup/metadata-lookup-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup/metadata-lookup-beans.xml
index 80935f1b..29e7166a 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup/metadata-lookup-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup/metadata-lookup-beans.xml
@@ -138,7 +138,9 @@
 
     <bean id="ResolveTrustMarks" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ResolveTrustMarks"
         scope="prototype"
-        p:trustChainCache-ref="#{'%{idp.oidfed.authorize.TrustChainMetadataCache:shibboleth.oidfed.TrustChainMetadataCache}'.trim()}">
+        p:trustChainCache-ref="#{'%{idp.oidfed.authorize.TrustChainMetadataCache:shibboleth.oidfed.TrustChainMetadataCache}'.trim()}"
+        p:trustMarkClaimsValidationLookupStrategy="#{getObject('shibboleth.oidfed.TrustMarkClaimsValidationLookupStrategy') ?: getObject('DefaultTrustMarkClaimsValidationLookupStrategy')}"
+        p:delegatedTrustMarkClaimsValidationLookupStrategy="#{getObject('shibboleth.oidfed.DelegatedTrustMarkClaimsValidationLookupStrategy') ?: getObject('DefaultTrustMarkClaimsValidationLookupStrategy')}">
         <property name="trustEngine">
             <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.TokenPayloadAsymmetricKeyTrustEngine">
                 <constructor-arg>
@@ -146,10 +148,21 @@
                  </constructor-arg>
             </bean>
         </property>
+        <property name="delegationTrustEngine">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.TokenPayloadAsymmetricKeyTrustEngine">
+                <constructor-arg>
+                    <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustMarkOwnerCredentialResolver" />
+                 </constructor-arg>
+            </bean>
+        </property>
         <property name="trustedTrustMarkIssuersLookupStrategy">
             <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainTrustedTrustMarkIssuersLookupStrategy"
                 p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper" />
         </property>
+        <property name="trustedTrustMarkOwnersLookupStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainTrustedTrustMarkOwnersLookupStrategy"
+                p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper" />
+        </property>
     </bean>
 
     <bean id="ValidateAutomaticRegistrationProfileConfiguration"
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-beans.xml
index cc1e301a..3cb3d50c 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/register/register-beans.xml
@@ -112,7 +112,9 @@
 
     <bean id="ResolveTrustMarks" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ResolveTrustMarks"
         scope="prototype"
-        p:trustChainCache-ref="#{'%{idp.oidfed.authorize.TrustChainMetadataCache:shibboleth.oidfed.TrustChainMetadataCache}'.trim()}">
+        p:trustChainCache-ref="#{'%{idp.oidfed.authorize.TrustChainMetadataCache:shibboleth.oidfed.TrustChainMetadataCache}'.trim()}"
+        p:trustMarkClaimsValidationLookupStrategy="#{getObject('shibboleth.oidfed.TrustMarkClaimsValidationLookupStrategy') ?: getObject('DefaultTrustMarkClaimsValidationLookupStrategy')}"
+        p:delegatedTrustMarkClaimsValidationLookupStrategy="#{getObject('shibboleth.oidfed.DelegatedTrustMarkClaimsValidationLookupStrategy') ?: getObject('DefaultTrustMarkClaimsValidationLookupStrategy')}">
         <property name="trustEngine">
             <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.TokenPayloadAsymmetricKeyTrustEngine">
                 <constructor-arg>
@@ -120,10 +122,21 @@
                  </constructor-arg>
             </bean>
         </property>
+        <property name="delegationTrustEngine">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.TokenPayloadAsymmetricKeyTrustEngine">
+                <constructor-arg>
+                    <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustMarkOwnerCredentialResolver" />
+                 </constructor-arg>
+            </bean>
+        </property>
         <property name="trustedTrustMarkIssuersLookupStrategy">
             <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainTrustedTrustMarkIssuersLookupStrategy"
                 p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper" />
         </property>
+        <property name="trustedTrustMarkOwnersLookupStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainTrustedTrustMarkOwnersLookupStrategy"
+                p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper" />
+        </property>
     </bean>
 
     <bean id="RelyingPartyTrustChainContextLookupStrategy" parent="shibboleth.Functions.Expression"
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-beans.xml
index 57377fad..ae78b670 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidfed/resolve-entity/resolve-entity-beans.xml
@@ -109,7 +109,9 @@
 
     <bean id="ResolveTrustMarks" class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.ResolveTrustMarks"
         scope="prototype"
-        p:trustChainCache-ref="#{'%{idp.oidfed.authorize.TrustChainMetadataCache:shibboleth.oidfed.TrustChainMetadataCache}'.trim()}">
+        p:trustChainCache-ref="#{'%{idp.oidfed.authorize.TrustChainMetadataCache:shibboleth.oidfed.TrustChainMetadataCache}'.trim()}"
+        p:trustMarkClaimsValidationLookupStrategy="#{getObject('shibboleth.oidfed.TrustMarkClaimsValidationLookupStrategy') ?: getObject('DefaultTrustMarkClaimsValidationLookupStrategy')}"
+        p:delegatedTrustMarkClaimsValidationLookupStrategy="#{getObject('shibboleth.oidfed.DelegatedTrustMarkClaimsValidationLookupStrategy') ?: getObject('DefaultTrustMarkClaimsValidationLookupStrategy')}">
         <property name="trustEngine">
             <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.TokenPayloadAsymmetricKeyTrustEngine">
                 <constructor-arg>
@@ -117,10 +119,21 @@
                  </constructor-arg>
             </bean>
         </property>
+        <property name="delegationTrustEngine">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.TokenPayloadAsymmetricKeyTrustEngine">
+                <constructor-arg>
+                    <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustMarkOwnerCredentialResolver" />
+                 </constructor-arg>
+            </bean>
+        </property>
         <property name="trustedTrustMarkIssuersLookupStrategy">
             <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainTrustedTrustMarkIssuersLookupStrategy"
                 p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper" />
         </property>
+        <property name="trustedTrustMarkOwnersLookupStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oidfed.metadata.DefaultTrustChainTrustedTrustMarkOwnersLookupStrategy"
+                p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper" />
+        </property>
     </bean>
 
     <bean id="PopulateEntityStatementSignatureSigningParameters"

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


More information about the commits mailing list