[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