[java-idp-plugin-vci] 01/01: Enforce key binding in credentials endpoint if published configuration expectes that
Codeberg
noreply at shibboleth.net
Tue Sep 22 10:18:14 UTC 2026
This is an automated email from the git hooks/post-receive script.
codeberg pushed a commit to branch bugs/StrictProof
in repository java-idp-plugin-vci.
View the commit online:
https://codeberg.org/Shibboleth/java-idp-plugin-vci/commit/4f72203a8189cbf5d50877e2e866239050d2040d
commit 4f72203a8189cbf5d50877e2e866239050d2040d
Author: Janne Lauros <janne.lauros at csc.fi>
AuthorDate: Tue Sep 22 13:17:56 2026 +0300
Enforce key binding in credentials endpoint if published configuration expectes that
---
.../openidvci/profile/impl/AddCredentialShell.java | 10 ++-
.../plugin/openidvci/profile/impl/ParseProof.java | 74 +++++++++++++++-------
.../openidvci/profile/impl/ParseProofTest.java | 35 ++++++++++
3 files changed, 94 insertions(+), 25 deletions(-)
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/AddCredentialShell.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/AddCredentialShell.java
index 08d269d..f5d5e8d 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/AddCredentialShell.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/AddCredentialShell.java
@@ -40,6 +40,7 @@ import net.shibboleth.shared.resolver.ResolverException;
import org.geant.shibboleth.plugin.openidvci.credential.CredentialIssuanceConfiguration;
import org.geant.shibboleth.plugin.openidvci.metadata.CredentialIssuerMetadata;
import org.geant.shibboleth.plugin.openidvci.metadata.resolver.CredentialIssuerMetadataResolver;
+import org.geant.shibboleth.plugin.openidvci.profile.OpenIDVCIEventIds;
import org.geant.shibboleth.plugin.openidvci.profile.logic.CredentialIssuerLookupFunction;
import org.geant.shibboleth.plugin.openidvci.statuslist.context.StatusListContext;
import org.geant.shibboleth.plugin.openidvci.profile.config.OpenIDVCIConfiguration;
@@ -282,12 +283,15 @@ public class AddCredentialShell extends AbstractProfileAction {
final ClaimsSet shell = createShell(ordinal);
try {
final Map<String, Object> cnf = buildCnf(proofs.get(ordinal));
- if (cnf != null) {
- shell.setClaim("cnf", cnf);
+ if (cnf == null) {
+ log.error("{} Proof carries no holder key this issuer binds to", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.INVALID_PROOF);
+ return;
}
+ shell.setClaim("cnf", cnf);
} catch (final Exception e) {
log.error("{} Failed to build cnf", getLogPrefix(), e);
- ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+ ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.INVALID_PROOF);
return;
}
shells.add(shell);
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ParseProof.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ParseProof.java
index 9d2edea..e7d97f6 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ParseProof.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ParseProof.java
@@ -25,6 +25,7 @@ import java.util.function.Function;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
+import org.geant.shibboleth.plugin.openidvci.credential.CredentialConfiguration;
import org.geant.shibboleth.plugin.openidvci.profile.config.OpenIDVCIConfiguration;
import org.geant.shibboleth.plugin.openidvci.messaging.context.CredentialsContext;
import org.geant.shibboleth.plugin.openidvci.messaging.impl.OpenIDVCICredentialsRequest;
@@ -143,7 +144,13 @@ public class ParseProof extends AbstractProfileAction {
proof = request.getProofs();
}
if (proof == null) {
- log.debug("{} Request does not contain proof or proofs, nothing to do");
+ if (isProofRequired(profileRequestContext)) {
+ log.error("{} Requested credential configuration requires a key proof, request carries none",
+ getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.INVALID_PROOF);
+ return false;
+ }
+ log.debug("{} Request does not contain proofs, nothing to do", getLogPrefix());
return false;
}
return true;
@@ -158,31 +165,37 @@ public class ParseProof extends AbstractProfileAction {
ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.PROOF_TYPE_UNSUPPORTED);
return;
}
+ if (!(proofToken instanceof List<?> tokens) || tokens.isEmpty()) {
+ log.error("{} Key proofs of type 'jwt' are not a non-empty array", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.INVALID_PROOF);
+ return;
+ }
+ if (tokens.size() > batchSize) {
+ log.error("{} Request carries {} key proofs, at most {} are accepted", getLogPrefix(), tokens.size(),
+ batchSize);
+ ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.INVALID_PROOF);
+ return;
+ }
final List<SignedJWT> proofs = new ArrayList<SignedJWT>();
- if (proofToken instanceof List<?> tokens) {
- if (tokens.size() > batchSize) {
- log.error("{} Request carries {} key proofs, at most {} are accepted", getLogPrefix(), tokens.size(),
- batchSize);
+ for (final Object token : tokens) {
+ if (!(token instanceof String strToken)) {
+ log.error("{} Key proof is not a string", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.INVALID_PROOF);
+ return;
+ }
+ try {
+ final SignedJWT singleProof = SignedJWT.parse(strToken);
+ validateJWTProof(singleProof, profileRequestContext);
+ proofs.add(singleProof);
+ } catch (final DPoPProofNonceJWTValidationException e) {
+ log.error("{} proof carries an invalid nonce.", getLogPrefix(), e);
+ ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.INVALID_NONCE);
+ return;
+ } catch (final Exception e) {
+ log.error("{} proof parsing failed.", getLogPrefix(), e);
ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.INVALID_PROOF);
return;
}
- tokens.forEach(token -> {
- if (token instanceof String strToken) {
- try {
- final SignedJWT singleProof = SignedJWT.parse(strToken);
- validateJWTProof(singleProof, profileRequestContext);
- proofs.add(singleProof);
- } catch (final DPoPProofNonceJWTValidationException e) {
- log.error("{} proof {} carries an invalid nonce.", getLogPrefix(), proof, e);
- ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.INVALID_NONCE);
- return;
- } catch (final Exception e) {
- log.error("{} proof {} parsing failed.", getLogPrefix(), proof, e);
- ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.INVALID_PROOF);
- return;
- }
- }
- });
}
final CredentialsContext ctx = profileRequestContext.getInboundMessageContext()
.ensureSubcontext(CredentialsContext.class);
@@ -225,6 +238,23 @@ public class ParseProof extends AbstractProfileAction {
}
}
+ /**
+ * Whether the requested credential configuration declares key proofs.
+ *
+ * @param profileRequestContext profile request context
+ *
+ * @return true if the request is required to carry key proofs
+ */
+ private boolean isProofRequired(@Nonnull final ProfileRequestContext profileRequestContext) {
+ final CredentialsContext credentialsContext = profileRequestContext.getInboundMessageContext()
+ .getSubcontext(CredentialsContext.class);
+ final CredentialConfiguration configuration = credentialsContext != null
+ ? credentialsContext.getCredentialConfiguration()
+ : null;
+ return configuration != null && configuration.getProofTypesSupported() != null
+ && !configuration.getProofTypesSupported().isEmpty();
+ }
+
/**
* Resolve the maximum number of key proofs to accept, the advertised batch size where the Credential Issuer
* metadata has one and the configured batch size otherwise.
diff --git a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ParseProofTest.java b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ParseProofTest.java
index 796518f..814ec03 100644
--- a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ParseProofTest.java
+++ b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ParseProofTest.java
@@ -18,6 +18,7 @@ package org.geant.shibboleth.plugin.openidvci.profile.impl;
import java.net.URI;
+import org.geant.shibboleth.plugin.openidvci.credential.CredentialConfiguration;
import org.geant.shibboleth.plugin.openidvci.messaging.context.CredentialsContext;
import org.geant.shibboleth.plugin.openidvci.messaging.impl.OpenIDVCICredentialsRequest;
import org.geant.shibboleth.plugin.openidvci.profile.OpenIDVCIEventIds;
@@ -29,6 +30,7 @@ import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
+import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.http.HTTPRequest;
@@ -131,6 +133,39 @@ public class ParseProofTest {
ActionTestingSupport.assertEvent(action.execute(requestCtx), OpenIDVCIEventIds.INVALID_PROOF);
}
+ @Test
+ public void testMissingProofsWhenConfigurationRequiresThem() throws Exception {
+ httpRequest.setQuery("{\n" + " \"credential_configuration_id\": \"org.iso.18013.5.1.mDL\"\n" + "}\n" + "");
+ profileRequestCtx.getInboundMessageContext().setMessage(OpenIDVCICredentialsRequest.parse(httpRequest));
+ profileRequestCtx.getInboundMessageContext().ensureSubcontext(CredentialsContext.class)
+ .setCredentialConfiguration(new ObjectMapper()
+ .readValue("{\"format\":\"dc+sd-jwt\",\"vct\":\"TestCredential\","
+ + "\"proof_types_supported\":{\"jwt\":{\"proof_signing_alg_values_supported\":[\"ES256\"]}}}",
+ CredentialConfiguration.class));
+ ActionTestingSupport.assertEvent(action.execute(requestCtx), OpenIDVCIEventIds.INVALID_PROOF);
+ }
+
+ @Test
+ public void testMissingProofsWhenConfigurationDoesNotRequireThem() throws Exception {
+ httpRequest.setQuery("{\n" + " \"credential_configuration_id\": \"org.iso.18013.5.1.mDL\"\n" + "}\n" + "");
+ profileRequestCtx.getInboundMessageContext().setMessage(OpenIDVCICredentialsRequest.parse(httpRequest));
+ profileRequestCtx.getInboundMessageContext().ensureSubcontext(CredentialsContext.class)
+ .setCredentialConfiguration(
+ new ObjectMapper().readValue("{\"format\":\"dc+sd-jwt\",\"vct\":\"TestCredential\"}",
+ CredentialConfiguration.class));
+ ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
+ Assert.assertNull(
+ profileRequestCtx.getInboundMessageContext().getSubcontext(CredentialsContext.class).getProofs());
+ }
+
+ @Test
+ public void testEmptyProofs() throws ParseException {
+ httpRequest.setQuery("{\n" + " \"credential_configuration_id\": \"org.iso.18013.5.1.mDL\",\n"
+ + " \"proofs\": {\n" + " \"jwt\": []\n" + " }\n" + "}\n" + "");
+ profileRequestCtx.getInboundMessageContext().setMessage(OpenIDVCICredentialsRequest.parse(httpRequest));
+ ActionTestingSupport.assertEvent(action.execute(requestCtx), OpenIDVCIEventIds.INVALID_PROOF);
+ }
+
@Test
public void testNoInboundMsgCtx() {
profileRequestCtx.setInboundMessageContext(null);
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list