[java-oidfed-common] 02/03: Improved handling and validation of delegated trust marks

Codeberg noreply at shibboleth.net
Thu Sep 24 07:34:37 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/72131b220a162c16e97131d798d42a2879477cc3

commit 72131b220a162c16e97131d798d42a2879477cc3
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Thu Sep 24 10:33:08 2026 +0300

    Improved handling and validation of delegated trust marks
    
    - Cover all the steps mandated by the spec
    - Included happy case tests
---
 .../oidfed/flow/AbstractFederationFlowTest.java    | 25 +++++++++
 .../flow/TestTrustChainResolutionFlowTest.java     | 65 ++++++++++++++++++++++
 .../oidfed/profile/impl/ResolveTrustMarks.java     | 51 +++++++++++++----
 .../oidfed/testing/FederationJwtSupport.java       |  5 ++
 .../oidfed/testing/TestFederationCredentials.java  | 11 +++-
 .../oidfed/testing/fed-delegated-owner.jwk         | 13 +++++
 6 files changed, 158 insertions(+), 12 deletions(-)

diff --git a/oidfed-common-conf-impl/src/test/java/net/shibboleth/oidfed/flow/AbstractFederationFlowTest.java b/oidfed-common-conf-impl/src/test/java/net/shibboleth/oidfed/flow/AbstractFederationFlowTest.java
index 3cf3410..873ba6a 100644
--- a/oidfed-common-conf-impl/src/test/java/net/shibboleth/oidfed/flow/AbstractFederationFlowTest.java
+++ b/oidfed-common-conf-impl/src/test/java/net/shibboleth/oidfed/flow/AbstractFederationFlowTest.java
@@ -115,6 +115,8 @@ public class AbstractFederationFlowTest extends AbstractFlowTest {
     protected final String trustMarkEndpoint = "https://trust-mark-issuer.federation.local/issue";
     protected final String trustMarkStatusEndpoint = "https://trust-mark-issuer.federation.local/status";
     protected final String issuer = "https://op.example.org";
+    protected final String delegatedTrustMarkIssuerId = "https://delegated-issuer.federation.local";
+    protected final String delegatedTrustMarkType = "https://trust-mark-example.federation.local/delegated";
 
     protected static JWK rpKey;
     protected static JWK leafKey;
@@ -257,6 +259,12 @@ public class AbstractFederationFlowTest extends AbstractFlowTest {
             .thenReturn(classicResponse);
     }
 
+    protected void mapResponses(final String requestUri, final ClassicHttpResponse classicResponse,
+            final ClassicHttpResponse... classicResponses) throws IOException {
+        when(federationHttpClient.executeOpen(any(), argThat(new RequestUriMatcher(requestUri)), any()))
+            .thenReturn(classicResponse, classicResponses);
+    }
+
     protected ClassicHttpResponse mockResponse(final String contents)
             throws UnsupportedOperationException, IOException {
         return mockResponse(200, "application/entity-statement+jwt", contents);
@@ -393,6 +401,12 @@ public class AbstractFederationFlowTest extends AbstractFlowTest {
 
     protected String trustedAnchorConfiguration(final Map<String, Object> constraints,
             final Map<String,List<String>> trustMarkIssuers, final JWK signerKey, final JWKSet jwks) {
+        return trustedAnchorConfiguration(constraints, trustMarkIssuers, signerKey, jwks, defaultTrustMarkOwners());
+    }
+
+    protected String trustedAnchorConfiguration(final Map<String, Object> constraints,
+            final Map<String,List<String>> trustMarkIssuers, final JWK signerKey, final JWKSet jwks,
+            final Map<String,Map<String,Object>> trustMarkOwners) {
 
         final String anchorId = "https://trust-anchor.federation.local";
         final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(anchorId).subject(anchorId)
@@ -407,6 +421,9 @@ public class AbstractFederationFlowTest extends AbstractFlowTest {
         if (trustMarkIssuers != null) {
             builder.claim("trust_mark_issuers", trustMarkIssuers);
         }
+        if (trustMarkOwners != null) {
+            builder.claim("trust_mark_owners", trustMarkOwners);
+        }
         return FederationJwtSupport.entityStatement(JWSAlgorithm.RS256, signerKey, builder.build()).serialize();
     }
 
@@ -721,6 +738,14 @@ public class AbstractFederationFlowTest extends AbstractFlowTest {
         }
     }
 
+    protected Map<String, Map<String, Object>> defaultTrustMarkOwners() {
+        final Map<String, Object> owner = Map.of(
+                "sub", delegatedTrustMarkIssuerId,
+                "jwks", new JWKSet(TestFederationCredentials.delegatedTrustMarkOwnerKey().toPublicJWK())
+                    .toJSONObject());
+        return Map.of(delegatedTrustMarkType, owner);
+    }
+
     protected ErrorResponse parseErrorResponse(final FlowExecutionResult result, final String message) {
         final Response response = parseResponse(result);
         Assert.assertTrue(response instanceof ErrorResponse, message);
diff --git a/oidfed-common-conf-impl/src/test/java/net/shibboleth/oidfed/flow/TestTrustChainResolutionFlowTest.java b/oidfed-common-conf-impl/src/test/java/net/shibboleth/oidfed/flow/TestTrustChainResolutionFlowTest.java
index 1d3f9ad..6df45eb 100644
--- a/oidfed-common-conf-impl/src/test/java/net/shibboleth/oidfed/flow/TestTrustChainResolutionFlowTest.java
+++ b/oidfed-common-conf-impl/src/test/java/net/shibboleth/oidfed/flow/TestTrustChainResolutionFlowTest.java
@@ -20,6 +20,7 @@ import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.time.Instant;
+import java.util.Date;
 import java.util.List;
 import java.util.Map;
 
@@ -34,9 +35,11 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.nimbusds.jose.JWSAlgorithm;
 import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
 
 import net.shibboleth.oidfed.testing.FederationJwtSupport;
+import net.shibboleth.oidfed.testing.TestFederationCredentials;
 import net.shibboleth.shared.collection.CollectionSupport;
 
 /**
@@ -238,6 +241,68 @@ public class TestTrustChainResolutionFlowTest extends AbstractFederationFlowTest
         assertTrustMarkDetails(1);
     }
 
+    @Test
+    public void testWithTwoTrustMarkDetailsParameterValidStatusRemoteValidationRequired() throws Exception {
+        request.setMethod("GET");
+        final String entityId = super.uniqueClientId();
+        request.addParameter("entityID", entityId);
+        request.addParameter("includeTrustMark", "true");
+        request.addParameter("remoteTrustMarkValidation", "true");
+
+        final String trustMark = FederationJwtSupport.trustMark(JWSAlgorithm.RS256, trustMarkIssuerKey, trustMarkIssuerId,
+                entityId, "https://example.org/email-allowing-trust-mark", Instant.now().plusSeconds(300)).serialize();
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setRedirectionURI(new URI(redirectUri));
+        metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
+
+        final JWTClaimsSet.Builder delegationBuilder = new JWTClaimsSet.Builder()
+                .issuer(delegatedTrustMarkIssuerId)
+                .subject(trustMarkIssuerId)
+                .claim("trust_mark_type", delegatedTrustMarkType)
+                .issueTime(new Date())
+                .expirationTime(Date.from(Instant.now().plusSeconds(300)));
+
+        final JWTClaimsSet.Builder trustMarkBuilder = new JWTClaimsSet.Builder()
+                .subject(entityId)
+                .issuer(trustMarkIssuerId)
+                .claim("trust_mark_type", delegatedTrustMarkType)
+                .claim("delegation", FederationJwtSupport.trustMarkDelegation(
+                        JWSAlgorithm.RS256, TestFederationCredentials.delegatedTrustMarkOwnerKey(),
+                        delegationBuilder.build()).serialize())
+                .issueTime(new Date())
+                .expirationTime(Date.from(Instant.now().plusSeconds(300)));
+        final String trustMark2 = FederationJwtSupport.trustMark(
+                JWSAlgorithm.RS256, trustMarkIssuerKey, trustMarkBuilder.build()).serialize();
+
+        final String rpEntityConfiguration = rpEntityConfiguration(entityId, metadata, List.of(
+                Map.of("trust_mark_type", "https://example.org/email-allowing-trust-mark",
+                        "trust_mark", trustMark),
+                Map.of("trust_mark_type", delegatedTrustMarkType,
+                        "trust_mark", trustMark2)), leafKey);
+        rpConfigureMockHttpClient(entityId, rpEntityConfiguration);
+        try {
+            mapResponse(entityConfigurationUrl(trustMarkIssuerId),
+                    mockResponse(trustMarkIssuerConfiguration(trustMarkIssuerId)));
+            mapResponse(subordinateStatementUrl(anchorFetchEndpoint, trustMarkIssuerId),
+                    mockResponse(subordinateStatement(trustMarkIssuerId,
+                            Map.of("federation_entity", CollectionSupport.emptyMap()), trustMarkIssuerKey)));
+            mapResponses(trustMarkStatusEndpoint,
+                    mockResponse(200, "application/trust-mark-status-response+jwt",
+                            trustMarkStatusResponse(trustMarkIssuerId, trustMark, "active", trustMarkIssuerKey)),
+                    mockResponse(200, "application/trust-mark-status-response+jwt",
+                            trustMarkStatusResponse(trustMarkIssuerId, trustMark2, "active", trustMarkIssuerKey)));
+        } catch (UnsupportedOperationException | IOException e) {
+            Assert.fail("Could not initialize mock HTTP client", e);
+        }
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+
+        final FlowExecutionOutcome outcome = result.getOutcome();
+        assertEquals(outcome.getId(), "ResponseView");
+        assertResponseArraySize(1);
+        assertTrustMarkDetails(2);
+    }
+
     protected void assertResponseArraySize(final int expected) {
         final ObjectMapper objectMapper = new ObjectMapper();
         try {
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/ResolveTrustMarks.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/ResolveTrustMarks.java
index 9c5e38d..ab35a52 100644
--- a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/ResolveTrustMarks.java
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/impl/ResolveTrustMarks.java
@@ -341,7 +341,7 @@ public class ResolveTrustMarks extends AbstractProfileAction {
                             entry -> entry.getValue().stream()
                                 .filter(trustMark ->
                                         validateClaims(trustMarkClaimsValidator, trustMark.getJwt(),
-                                                profileRequestContext))
+                                                profileRequestContext, "trust-mark+jwt"))
                             .toList()));
         if (chainTrustMarks == null || chainTrustMarks.isEmpty()) {
             log.debug("{} No valid trust marks found from the selected trust chain", getLogPrefix());
@@ -390,13 +390,22 @@ public class ResolveTrustMarks extends AbstractProfileAction {
      * @param claimsValidator the claims validator (chain)
      * @param jwt the trust mark
      * @param profileRequestContext the profile request context
+     * @param expectedType the expected type header value
      * @return true if validation succeeded, false otherwise
      */
     protected boolean validateClaims(@Nullable final ClaimsValidator claimsValidator, @Nullable final SignedJWT jwt,
-            @Nonnull final ProfileRequestContext profileRequestContext) {
+            @Nonnull final ProfileRequestContext profileRequestContext, @Nonnull final String expectedType) {
         if (claimsValidator == null || jwt == null) {
             return false;
         }
+        final String actualType = Optional.ofNullable(jwt.getHeader().getType())
+                .map(type -> type.getType())
+                .orElse(null);
+        if (!expectedType.equals(actualType)) {
+            log.debug("{} Type {} is not matching the expected type {}", getLogPrefix(), actualType, expectedType);
+            return false;
+        }
+
         try {
             final JWTClaimsSet claimsSet = jwt.getJWTClaimsSet();
             assert claimsSet != null;
@@ -506,22 +515,42 @@ public class ResolveTrustMarks extends AbstractProfileAction {
             @Nonnull final ProfileRequestContext profileRequestContext) {
         log.debug("{} Validating delegated trust mark {}", getLogPrefix(), id);
         try {
-            final SignedJWT delegationJwt = SignedJWT.parse(trustMarkClaims.getDelegation());
+            final String delegation = trustMarkClaims.getDelegation();
+            if (delegation == null) {
+                log.debug("{} No delegation included in the claims, rejecting the trust mark {}", getLogPrefix(), id);
+                return false;
+            }
+            final SignedJWT delegationJwt = SignedJWT.parse(delegation);
             final CriteriaSet delegationCriteria = new CriteriaSet(
                     new TrustMarkOwnersCriterion(trustedOwners),
                     new SubjectEntityIDCriterion(id));
-            if (validateClaims(delegatedTrustMarkClaimsValidator, delegationJwt, profileRequestContext)) {
+            if (validateClaims(delegatedTrustMarkClaimsValidator, delegationJwt, profileRequestContext,
+                    "trust-mark-delegation+jwt")) {
                 assert delegationJwt != null;
+                final String subject = delegationJwt.getJWTClaimsSet().getSubject();
+                if (subject == null || !subject.equals(trustMarkClaims.getIssuer())) {
+                    log.debug("{} The subject of the delegation {} does not match with the trust mark issuer {}",
+                            getLogPrefix(), subject, trustMarkClaims.getIssuer());
+                    return false;
+                }
+                final String issuer = delegationJwt.getJWTClaimsSet().getIssuer();
+                final String owner = Optional.ofNullable(trustedOwners.get(id))
+                        .map(item -> item.getSub()).orElse(null);
+                if (issuer == null || !issuer.equals(owner)) {
+                    log.debug("{} The issuer of the delegation {} does not match with the trusted owner {}",
+                            getLogPrefix(), issuer, owner);
+                    return false;
+                }
+                final String delegationType = delegationJwt.getJWTClaimsSet().getStringClaim("trust_mark_type");
+                if (!id.equals(delegationType)) {
+                    log.debug("{} The trust mark type of the delegation {} is not expected {}", getLogPrefix(),
+                            delegationType, id);
+                    return false;
+                }
                 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());
-                    }
+                    return true;
                 }
             }
         } catch (final SecurityException  e) {
diff --git a/oidfed-common-testing/src/main/java/net/shibboleth/oidfed/testing/FederationJwtSupport.java b/oidfed-common-testing/src/main/java/net/shibboleth/oidfed/testing/FederationJwtSupport.java
index 6959e6d..0e9c9b5 100644
--- a/oidfed-common-testing/src/main/java/net/shibboleth/oidfed/testing/FederationJwtSupport.java
+++ b/oidfed-common-testing/src/main/java/net/shibboleth/oidfed/testing/FederationJwtSupport.java
@@ -49,6 +49,11 @@ public class FederationJwtSupport {
         return signedJwt(algorithm, jwk, "trust-mark+jwt", claimsSet);
     }
 
+    public static SignedJWT trustMarkDelegation(final JWSAlgorithm algorithm, final JWK jwk,
+            final JWTClaimsSet claimsSet) {
+        return signedJwt(algorithm, jwk, "trust-mark-delegation+jwt", claimsSet);
+    }
+
     public static SignedJWT entityStatement(final JWSAlgorithm algorithm, final JWK jwk, final JWTClaimsSet claimsSet) {
         return signedJwt(algorithm, jwk, new JWSHeader.Builder(algorithm)
                     .type(new JOSEObjectType("entity-statement+jwt"))
diff --git a/oidfed-common-testing/src/main/java/net/shibboleth/oidfed/testing/TestFederationCredentials.java b/oidfed-common-testing/src/main/java/net/shibboleth/oidfed/testing/TestFederationCredentials.java
index d1afbba..a089e7b 100644
--- a/oidfed-common-testing/src/main/java/net/shibboleth/oidfed/testing/TestFederationCredentials.java
+++ b/oidfed-common-testing/src/main/java/net/shibboleth/oidfed/testing/TestFederationCredentials.java
@@ -56,7 +56,16 @@ public class TestFederationCredentials {
                 .keyID("signingRsKey")
                 .build();
     }
-    
+
+    public static JWK delegatedTrustMarkOwnerKey() {
+        final BasicJWKCredential signingRsKey =
+                loadJWKCredential("/net/shibboleth/oidfed/testing/fed-delegated-owner.jwk");
+        return new RSAKey.Builder((RSAPublicKey) signingRsKey.getPublicKey())
+                .privateKey(signingRsKey.getPrivateKey())
+                .keyID("delegatedTrustMarkOwnerKey")
+                .build();
+    }
+
     public static BasicJWKCredential loadJWKCredential(final String classPathLocation) {
         final BasicJWKCredentialFactoryBean factory = new BasicJWKCredentialFactoryBean();
         factory.setResource(new ClassPathResource(classPathLocation));
diff --git a/oidfed-common-testing/src/main/resources/net/shibboleth/oidfed/testing/fed-delegated-owner.jwk b/oidfed-common-testing/src/main/resources/net/shibboleth/oidfed/testing/fed-delegated-owner.jwk
new file mode 100644
index 0000000..a8aa432
--- /dev/null
+++ b/oidfed-common-testing/src/main/resources/net/shibboleth/oidfed/testing/fed-delegated-owner.jwk
@@ -0,0 +1,13 @@
+{
+  "p": "yKPc01OqNvU8gIcwGoYAx8ysvofsxyZTigAbPE87BL5jDZqqAngYo9k7FiXGaZd3g5HfPjtfjNYXAkrKS8ieo5UVKm06pfOiJEpFj_aec-9iGJonR4GFsCERLcdntT2QTYJvI3JI9h-NnikKuKb9xK04b0URII97PSNY3B2UPvk",
+  "kty": "RSA",
+  "q": "936MEcBD4HZVbA7-1PWWz8aKcYPU9SIs3BCViTfZoidyrujd3FBvILQMkh1n5mdTGOxd5-GFZ4pMgKBJdSlI7hYjA4mE2E-tzA9-7lVAfbuf3ey32v91lfuO-XK8KRsZRE45P6euymuRJ0ANv_E-QsEYH3Bi8VMqE7nXmK9N21U",
+  "d": "FQxfXOvf0uJ5uFSaUVrs80pfjsLK5KFw4GlbM6gb0x_7i0jefPvmfKPHYew2bRoAXFtZ6XGoHEEzmbUeat2E1MAH7YhmIdCaFBY0ZJeugpcC9mm_Tun8r-_wz7JcbJTQ0VBVz94Fgwo3pDDRBPYRDktl3749o763CCmKFDX5mfLBOnVytI7n3hVwckeTL8rHPYpwwHQ5LyV6ppw7fFJOKv0_V5QyLvqigqY7d0ybpeHXdgm3FA6Bdp-O4wP5K6Lh6DtQmwbU1OwJlTUErs7KOZCQj39Gs0nCHPMMvO9WPDc6XAUveRsq5slk7nfqpaz8kr6B1lGBra3BhiLX1ypKUQ",
+  "e": "AQAB",
+  "use": "sig",
+  "kid": "delegatedTrustMarkOwner",
+  "qi": "ckZr2U3UkXOmMfj_-IEbl4-25aoYL45Lu1jNNDoRWARZwJ3ibBQyfXrbeSGhbJOEZ-oYJ6aBGlynO9ks4NWVb9-kaPyD-9PwJED-Ep3Ls84hHMbpFLT2pRuUhrxRkhHweVaPTIxso8OzkD-BLKbETyFWU2pv1nT_iqFvaemMOG0",
+  "dp": "BLVjv0ndWsDSlKmXhWfTqNyyyR3jfqD1oO1ztiyd0_Sr2mvgxYIQAfhdHJ3yVBQL5_iKZql6CpITpCghKzogDvK2tnItyCs5iOR9UW2WNl5NzQoBFfqCKqn7_zkIxAYX1tnSxeAuuifvaODJsZ-poO33vxS93CfB2qNzuGaxTfE",
+  "dq": "U0IoHsSeUQGeBQcaZUvlGyrAYiWmW3zOuI1_sLSwKyO4Ke0-zaHlcIwBepoX3OO_ia_ie2JbvZRB3jeN3rMvzKB30PXPUfGkFvTTj5vwYRvgUQkUZMDxrq380B_v-qgaHOdvzOE9JO2A3EJpHbwrZMuOsOze7C49BnvLcpruxy0",
+  "n": "wflIfhwRvQS4MeenFVZsnSSfXjzJyMFKrC_dC99d-zSQLoY6fdO9R3KZvKITYNUEWcTK_Etvkpz7cWbVNzyXfUlDLLQM0q_YtwYw4MY-rq5k9CoJIZ1jFPSY-oQ5oXpRUv-8CwJoUxLelywWT48J7jKC5Nf3Sbf-856GqN5jmpPD0FvMJkHNXzKe1rqEQgMVWH3FuNEA7UCe8f98F_cY4M9lCEZc_6w7wGflzVytEV22LGK8KZYxDkvFZCtkocZcxhmX5n4dngpkDqATztSq8dODFViQMTC-0hj4KLzdxbL3wJ9MII977fakJLY8kj7hv9EubQBxrsnKB43ey_zrrQ"
+}

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


More information about the commits mailing list