[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