[java-oidfed-common] 02/02: Improve trust mark parsing strategy for controlling the subject

Codeberg noreply at shibboleth.net
Fri Sep 18 06:59:25 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/483e826845e436b452bdfc3043d1c484960c62fe

commit 483e826845e436b452bdfc3043d1c484960c62fe
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri Sep 18 09:58:46 2026 +0300

    Improve trust mark parsing strategy for controlling the subject
    
    - Also included initial unit tests
---
 ...DefaultTrustChainTrustMarksParsingStrategy.java |  48 +++--
 ...ultTrustChainTrustMarksParsingStrategyTest.java | 230 +++++++++++++++++++++
 .../oidfed/testing/TestFederationCredentials.java  |   9 +
 .../net/shibboleth/oidfed/testing/signing-rs.jwk   |   8 +
 4 files changed, 280 insertions(+), 15 deletions(-)

diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultTrustChainTrustMarksParsingStrategy.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultTrustChainTrustMarksParsingStrategy.java
index f5e2d08..15eda80 100644
--- a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultTrustChainTrustMarksParsingStrategy.java
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/profile/navigate/DefaultTrustChainTrustMarksParsingStrategy.java
@@ -30,6 +30,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.nimbusds.jwt.SignedJWT;
 
+import net.shibboleth.oidfed.metadata.EntityConfiguration;
 import net.shibboleth.oidfed.metadata.EntityStatement;
 import net.shibboleth.oidfed.metadata.TrustMark;
 import net.shibboleth.oidfed.metadata.impl.TrustMarkImpl;
@@ -44,7 +45,8 @@ import net.shibboleth.shared.primitive.StringSupport;
 
 /**
  * Default strategy for parsing map of trust marks for the given trust chain. The keys in the map refer to the entity
- * ID for which the trust mark has been issued to.
+ * ID for which the trust mark has been issued to. They are solely parsed from the {@link EntityConfiguration}, i.e.
+ * from the trust anchor and leaf configurations in the trust chain.
  */
 @ThreadSafeAfterInit
 public class DefaultTrustChainTrustMarksParsingStrategy extends AbstractIdentifiableInitializableComponent
@@ -88,7 +90,10 @@ public class DefaultTrustChainTrustMarksParsingStrategy extends AbstractIdentifi
         
         final Map<String, List<TrustMark>> result = new HashMap<>();
         for (final EntityStatement<?> statement : trustChain) {
-            assert statement != null;
+            if (statement == null) {
+                log.error("Ignoring a null entity statement element from the trust chain");
+                continue;
+            }
             if (statement.getParsedPayload() instanceof EntityConfigurationPayload ecp) {
                 final SignedJWT statementJwt = statement.getJwt();
                 final List<Map<String, String>> rawTrustMarks = ecp.getTrustMarks();
@@ -99,12 +104,16 @@ public class DefaultTrustChainTrustMarksParsingStrategy extends AbstractIdentifi
                     log.trace("Transforming the trust mark into a JWT");
                     final List<TrustMark> trustMarks = rawTrustMarks
                             .stream()
-                            .map(entry -> parseTrustMark(entry))
+                            .map(entry -> parseTrustMark(entry, statement.getSubject()))
                             .filter(Objects::nonNull)
                             .toList();
-                    log.debug("Returning {} trust marks for entity {}", trustMarks.size(),
-                            statement.getSubject());
-                    result.put(statement.getSubject(), trustMarks);
+                    if (!trustMarks.isEmpty()) {
+                        log.debug("Returning {} trust marks for entity {}", trustMarks.size(),
+                                statement.getSubject());
+                        result.put(statement.getSubject(), trustMarks);
+                    } else {
+                        log.debug("No verified trust marks found for entity {}", statement.getSubject());
+                    }
                 }
             }
         }
@@ -115,24 +124,28 @@ public class DefaultTrustChainTrustMarksParsingStrategy extends AbstractIdentifi
     /**
      * Parses and validates JWT from the trust mark entry.
      * 
-     * @param trustMarkEntry trust mark entry as Strign to be parsed into a JWT
+     * @param trustMarkEntry trust mark entry as String to be parsed into a JWT
+     * @param subject subject of the trust mark entry
      * @return trust mark JWT if valid, null otherwise
      */
-    @Nullable private TrustMark parseTrustMark(@Nullable final Map<String, String> trustMarkEntry) {
+    @Nullable private TrustMark parseTrustMark(@Nullable final Map<String, String> trustMarkEntry,
+            @Nonnull final String subject) {
         if (trustMarkEntry == null) {
             return null;
         }
-        return verifyTrustMark(trustMarkEntry.get("trust_mark"), trustMarkEntry.get("trust_mark_type"));
+        return verifyTrustMark(trustMarkEntry.get("trust_mark"), trustMarkEntry.get("trust_mark_type"), subject);
     }
 
     /**
-     * Verifies the trust mark id and issuer claims.
+     * Verifies the trust mark id, issuer and subject claims.
      * 
      * @param trustMark trust mark to be verified
      * @param id the id to be verified from the JWT claims set
+     * @param subject subject of the trust mark
      * @return trust mark if valid, null otherwise
      */
-    @Nullable private TrustMark verifyTrustMark(@Nullable final String trustMark, @Nullable final String id) {
+    @Nullable private TrustMark verifyTrustMark(@Nullable final String trustMark, @Nullable final String id,
+            @Nonnull final String subject) {
         if (trustMark == null || id == null) {
             log.trace("Could not parse trust mark {} with trust_mark_type {}", trustMark, id);
             return null;
@@ -141,16 +154,21 @@ public class DefaultTrustChainTrustMarksParsingStrategy extends AbstractIdentifi
             final SignedJWT jwt = SignedJWT.parse((String) trustMark);
             assert jwt != null; assert objectMapper != null;
             final TrustMarkImpl result = TrustMarkImpl.parse(jwt, objectMapper);
+            final String trustMarkType = result.getParsedPayload().getTrustMarkType();
             if (StringSupport.trimOrNull(result.getIssuer()) == null) {
-                log.error("Trust Mark {} is missing mandatory issuer",
-                        result.getParsedPayload().getTrustMarkType());
+                log.warn("Trust Mark {} is missing mandatory issuer", trustMarkType);
+                return null;
+            }
+            final String parsedSubject = result.getParsedPayload().getSubject();
+            if (!subject.equals(parsedSubject)) {
+                log.warn("Trust Mark {} subject {} is not matching with the expected one: {}", trustMarkType,
+                        parsedSubject, subject);
                 return null;
             }
             if (id.equals(result.getParsedPayload().getTrustMarkType())) {
                 return result;
             }
-            log.error("The id {} is not matching with the trust_mark_type-claim {}", id,
-                    result.getParsedPayload().getTrustMarkType());
+            log.warn("The id {} is not matching with the trust_mark_type-claim {}", id, trustMarkType);
         } catch (final ParseException e) {
             log.error("Could not parse id-claim from the trust mark", e);
         } catch (final JsonProcessingException e) {
diff --git a/oidfed-common-impl/src/test/java/net/shibboleth/oidfed/profile/navigate/DefaultTrustChainTrustMarksParsingStrategyTest.java b/oidfed-common-impl/src/test/java/net/shibboleth/oidfed/profile/navigate/DefaultTrustChainTrustMarksParsingStrategyTest.java
new file mode 100644
index 0000000..2d4ab55
--- /dev/null
+++ b/oidfed-common-impl/src/test/java/net/shibboleth/oidfed/profile/navigate/DefaultTrustChainTrustMarksParsingStrategyTest.java
@@ -0,0 +1,230 @@
+/*
+ * 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.profile.navigate;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.time.Instant;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.crypto.RSASSASigner;
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.oidfed.metadata.EntityConfiguration;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.SubordinateStatement;
+import net.shibboleth.oidfed.metadata.TrustMark;
+import net.shibboleth.oidfed.metadata.jackson.JacksonTestingSupport;
+import net.shibboleth.oidfed.metadata.payload.impl.EntityConfigurationPayloadImpl;
+import net.shibboleth.oidfed.metadata.payload.impl.SubordinateStatementPayloadImpl;
+import net.shibboleth.oidfed.testing.TestFederationCredentials;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.component.UninitializedComponentException;
+
+/**
+ * Unit tests for {@link DefaultTrustChainTrustMarksParsingStrategy}.
+ */
+public class DefaultTrustChainTrustMarksParsingStrategyTest {
+
+    DefaultTrustChainTrustMarksParsingStrategy function;
+
+    final String leaf = "https://entity.example.org";
+    final String anchor = "https://anchor.example.org";
+
+    @BeforeMethod
+    public void init() throws ComponentInitializationException {
+        function = new DefaultTrustChainTrustMarksParsingStrategy();
+        function.setObjectMapper(JacksonTestingSupport.payloadObjectMapper());
+        function.setId("mockFunction");
+        function.initialize();
+    }
+
+    @Test(expectedExceptions = UninitializedComponentException.class)
+    public void uninitializedThrowsException() throws ComponentInitializationException {
+        function = new DefaultTrustChainTrustMarksParsingStrategy();
+        function.setObjectMapper(new ObjectMapper());
+        function.setId("mockFunction");
+        function.apply(CollectionSupport.emptyList());
+    }
+
+    @Test(expectedExceptions = ComponentInitializationException.class)
+    public void noObjectMapperThrowsException() throws ComponentInitializationException {
+        function = new DefaultTrustChainTrustMarksParsingStrategy();
+        function.setId("mockFunction");
+        function.initialize();
+    }
+
+    @Test
+    public void apply_nullTrustChain_returnsNullMap() {
+        Assert.assertEquals(function.apply(null), null);
+    }
+
+    @Test
+    public void apply_emptyTrustChain_returnsNullMap() {
+        Assert.assertEquals(function.apply(CollectionSupport.emptyList()), null);
+    }
+
+    @Test
+    public void apply_soleEntityConfiguration_returnsNullMap() {
+        Assert.assertEquals(function.apply(
+                CollectionSupport.listOf(mockEntityConfiguration(leaf))), null);
+    }
+
+    @Test
+    public void apply_tooShortTrustChain_returnsNullMap() {
+        Assert.assertEquals(function.apply(
+                CollectionSupport.listOf(mockEntityConfiguration(leaf),
+                        mockEntityConfiguration(anchor))), null);
+    }
+
+    @Test
+    public void apply_noTrustMarks_returnsEmptyMap() {
+        Assert.assertEquals(
+                function.apply(mockTrustChain(leaf, anchor)),
+                CollectionSupport.emptyMap());
+    }
+
+    @Test
+    public void apply_wrongSubTrustMarks_returnsEmptyMap() {
+        final String trustMarkType = "https://example.org/trust_mark";
+        final Map<String,List<TrustMark>> result =
+                function.apply(CollectionSupport.listOf(
+                        mockEntityConfiguration(leaf,
+                                List.of(trustMark("https://wrong.example.org", anchor, trustMarkType))), 
+                        mockSubordinateStatement(leaf, anchor),
+                        mockEntityConfiguration(anchor)));
+        assert result != null;
+        Assert.assertEquals(result.size(), 0);
+    }
+
+    @Test
+    public void apply_wrongTypeTrustMarks_returnsEmptyMap() {
+        final String trustMarkType = "https://example.org/trust_mark";
+        final var trustMarkMap = new HashMap<>(trustMark(leaf, anchor, trustMarkType));
+        trustMarkMap.put("trust_mark_type", "https://wrong.example.org/trust_mark");
+        final Map<String,List<TrustMark>> result =
+                function.apply(CollectionSupport.listOf(
+                        mockEntityConfiguration(leaf, List.of(trustMarkMap)), 
+                        mockSubordinateStatement(leaf, anchor),
+                        mockEntityConfiguration(anchor)));
+        assert result != null;
+        Assert.assertEquals(result.size(), 0);
+    }
+
+    @Test
+    public void apply_correctSubTrustMarks_returnsTrustMarks() {
+        final String trustMarkType = "https://example.org/trust_mark";
+        final var leafTrustMarkMap = trustMark(leaf, anchor, trustMarkType);
+        final var anchorTrustMarkMap = trustMark(anchor, anchor, trustMarkType);
+        final Map<String,List<TrustMark>> result =
+                function.apply(CollectionSupport.listOf(
+                        mockEntityConfiguration(leaf, List.of(leafTrustMarkMap)), 
+                        mockSubordinateStatement(leaf, anchor),
+                        mockEntityConfiguration(anchor, List.of(anchorTrustMarkMap))));
+        assert result != null;
+        Assert.assertEquals(result.size(), 2);
+        final List<TrustMark> leafTrustMarks = result.get(leaf);
+        assert leafTrustMarks != null;
+        Assert.assertEquals(leafTrustMarks.size(), 1);
+        Assert.assertEquals(leafTrustMarks.get(0).getParsedPayload().getSubject(), leaf);
+        Assert.assertEquals(leafTrustMarks.get(0).getParsedPayload().getIssuer(), anchor);
+        Assert.assertEquals(leafTrustMarks.get(0).getJwt().serialize(), leafTrustMarkMap.get("trust_mark"));
+        final List<TrustMark> anchorTrustMarks = result.get(anchor);
+        assert anchorTrustMarks != null;
+        Assert.assertEquals(anchorTrustMarks.size(), 1);
+        Assert.assertEquals(anchorTrustMarks.get(0).getParsedPayload().getSubject(), anchor);
+        Assert.assertEquals(anchorTrustMarks.get(0).getParsedPayload().getIssuer(), anchor);
+        Assert.assertEquals(anchorTrustMarks.get(0).getJwt().serialize(), anchorTrustMarkMap.get("trust_mark"));
+    }
+
+    @Nonnull protected EntityConfiguration mockEntityConfiguration(final String sub) {
+        return mockEntityConfiguration(sub, null);
+    }
+
+    @Nonnull protected EntityConfiguration mockEntityConfiguration(final String sub,
+            final List<Map<String,String>> trustMarks) {
+        final EntityConfiguration entityConfiguration = mock(EntityConfiguration.class);
+        final EntityConfigurationPayloadImpl payload = new EntityConfigurationPayloadImpl();
+        payload.setSubject(sub);
+        if (trustMarks != null) {
+            payload.setTrustMarks(trustMarks);
+        }
+        when(entityConfiguration.getParsedPayload()).thenReturn(payload);
+        final SignedJWT jwt = mock(SignedJWT.class);
+        when(jwt.serialize()).thenReturn("mockContent");
+        when(entityConfiguration.getJwt()).thenReturn(jwt);
+        when(entityConfiguration.getSubject()).thenReturn(sub);
+        assert entityConfiguration != null;
+        return entityConfiguration;
+    }
+
+    @Nonnull protected SubordinateStatement mockSubordinateStatement(final String sub, final String iss) {
+        final SubordinateStatement statement = mock(SubordinateStatement.class);
+        final SubordinateStatementPayloadImpl payload = new SubordinateStatementPayloadImpl();
+        payload.setSubject(sub);
+        payload.setIssuer(iss);
+        when(statement.getParsedPayload()).thenReturn(payload);
+        when(statement.getSubject()).thenReturn(sub);
+        when(statement.getIssuer()).thenReturn(iss);
+        assert statement != null;
+        return statement;
+    }
+
+    @Nonnull protected List<EntityStatement<?>> mockTrustChain(final String anchor, final String sub) {
+        return CollectionSupport.listOf(
+                mockEntityConfiguration(anchor),
+                mockSubordinateStatement(sub, anchor),
+                mockEntityConfiguration(sub));
+    }
+
+    protected Map<String, String> trustMark(final String sub, final String iss, final String trustMarkType) {
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder()
+                .subject(sub)
+                .issuer(iss)
+                .claim("trust_mark_type", trustMarkType)
+                .issueTime(new Date())
+                .expirationTime(Date.from(Instant.now().plusSeconds(300)));
+        final JWK jwk = TestFederationCredentials.signingRSKey();
+        final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256)
+                .type(new JOSEObjectType("trust-mark+jwt"))
+                .keyID(jwk.getKeyID()).build();
+        final SignedJWT signedJwt = new SignedJWT(header, builder.build());
+        try {
+            signedJwt.sign(new RSASSASigner(jwk.toRSAKey()));
+        } catch (final JOSEException e) {
+            Assert.fail("Could not construct signed JWT", e);
+        }
+        return Map.of("trust_mark_type", trustMarkType, "trust_mark", signedJwt.serialize());
+    }
+
+}
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 0ac6a75..d1afbba 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
@@ -47,6 +47,15 @@ public class TestFederationCredentials {
                 .keyID("locallyTrustedIntermediateKey")
                 .build();
     }
+
+    public static JWK signingRSKey() {
+        final BasicJWKCredential signingRsKey =
+                loadJWKCredential("/net/shibboleth/oidfed/testing/signing-rs.jwk");
+        return new RSAKey.Builder((RSAPublicKey) signingRsKey.getPublicKey())
+                .privateKey(signingRsKey.getPrivateKey())
+                .keyID("signingRsKey")
+                .build();
+    }
     
     public static BasicJWKCredential loadJWKCredential(final String classPathLocation) {
         final BasicJWKCredentialFactoryBean factory = new BasicJWKCredentialFactoryBean();
diff --git a/oidfed-common-testing/src/main/resources/net/shibboleth/oidfed/testing/signing-rs.jwk b/oidfed-common-testing/src/main/resources/net/shibboleth/oidfed/testing/signing-rs.jwk
new file mode 100644
index 0000000..a6297ee
--- /dev/null
+++ b/oidfed-common-testing/src/main/resources/net/shibboleth/oidfed/testing/signing-rs.jwk
@@ -0,0 +1,8 @@
+{
+  "kty": "RSA",
+  "d": "gv7aqFcXV86jDcCn6-JCqEEIRcv1Rh1AEv4dKziFzQal1nROliDdtkJjELpOYlFY9CgI-xAXt8ivwJ4q1eA_G9WTId7qLxPdcQW4QjfRl8VVEPUhka6Gc8y95WUO4VONEwzZnZ4V7KobE0QGADXvXUw3MtIZdGgvRCS-6avQXITjhTnlkUONxeqpy2BE6l0cI8GSM1vlLy66vjsQ06aAizMB-g3yMMpbKNd73oYgrdpEjAtddH3-sLhv_TG7pMlbB_etnPGkWKdIbpvTKr2P2oZN_8Qvq7G4ETIe9nIv7i8T7GXZfTxWspYkszbrpRACM9Ic8fSctvil2j013JeSgQ",
+  "e": "AQAB",
+  "use": "sig",
+  "kid": "fedtestkeyRS",
+  "n": "pNf03ghVzMAw5sWrwDAMAZdSYNY2q7OVlxMInljMgz8XB5mf8XKH3EtP7AKrb8IAf7rGhfuH3T1N1C7F-jwIeYjXxMm2nIAZ0hXApgbccvBpf4n2H7IZflMjt4A3tt587QQSxQ069drCP4sYevxhTcLplJy6RWA0cLj-5CHyWy94zPeeA4GRd6xgHFLz0RNiSF0pF0kE4rmRgQVZ-b4_BmD9SsWnIpwhms5Ihciw36WyAGQUeZqULGsfwAMwlNLIaTCBLAoRgv370p-XsLrgz86pTkNBJqXP5GwI-ZfgiLmJuHjQ9l85KqHM87f-QdsqiV8KoRcslgXPqb6VOTJBVw"
+}

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


More information about the commits mailing list