[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