[java-idp-plugin-vci] 02/02: Initial work for proof validation for jwt type of proofs

Codeberg noreply at shibboleth.net
Fri Nov 21 11:59:13 UTC 2025


This is an automated email from the git hooks/post-receive script.

codeberg pushed a commit to branch dev/PROOFVERIFY
in repository java-idp-plugin-vci.

View the commit online:
https://codeberg.org/Shibboleth/java-idp-plugin-vci/commit/0049edb6df62d9b58f6baabcbd259ba8dbd6c3a4

commit 0049edb6df62d9b58f6baabcbd259ba8dbd6c3a4
Author: jlauros <janne.lauros at csc.fi>
AuthorDate: Fri Nov 21 13:59:00 2025 +0200

    Initial work for proof validation for jwt type of proofs
---
 .../plugin/openidvci/profile/impl/ParseProof.java  | 96 ++++++++++++++++++----
 .../idp/service/relying-party/postconfig.xml       | 21 ++++-
 .../openidvci/profile/impl/ParseProofTest.java     | 34 +++++++-
 3 files changed, 131 insertions(+), 20 deletions(-)

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 e4041a5..60344bc 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
@@ -16,29 +16,35 @@
 
 package org.geant.shibboleth.plugin.openidvci.profile.impl;
 
-import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Function;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 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;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
 import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jose.JWSAlgorithm;
 import com.nimbusds.jose.JWSObject;
+import com.nimbusds.jwt.JWTClaimsSet;
 
 import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
+import net.shibboleth.oidc.profile.oauth2.config.OAuth2DPoPProofValidatingProfileConfiguration;
+import net.shibboleth.profile.context.RelyingPartyContext;
 import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.logic.Constraint;
 
-/**
- *  
- */
 public class ParseProof extends AbstractProfileAction {
 
     /** Class logger. */
@@ -48,12 +54,54 @@ public class ParseProof extends AbstractProfileAction {
     @NonnullBeforeExec
     private Map<String, Object> proof;
 
+    /**
+     * Strategy used to locate the {@link RelyingPartyContext} associated with a
+     * given {@link ProfileRequestContext}.
+     */
+    @Nonnull
+    private Function<ProfileRequestContext, RelyingPartyContext> relyingPartyContextLookupStrategy;
+
+    /** Relying party context. */
+    @Nullable
+    private RelyingPartyContext rpCtx;
+
+    /** Validator for nonce. */
+    @Nullable
+    private ClaimsValidator validator;
+
+    /**
+     * Constructor.
+     */
+    public ParseProof() {
+        relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
+    }
+
+    /**
+     * Set the strategy used to locate the {@link RelyingPartyContext} associated
+     * with a given {@link ProfileRequestContext}.
+     * 
+     * @param strategy strategy used to locate the {@link RelyingPartyContext}
+     *                 associated with a given {@link ProfileRequestContext}
+     */
+    public void setRelyingPartyContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, RelyingPartyContext> strategy) {
+        checkSetterPreconditions();
+
+        relyingPartyContextLookupStrategy = Constraint.isNotNull(strategy,
+                "RelyingPartyContext lookup strategy cannot be null");
+    }
+
     /** {@inheritDoc} */
     @Override
     protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
         if (!super.doPreExecute(profileRequestContext)) {
             return false;
         }
+        rpCtx = relyingPartyContextLookupStrategy.apply(profileRequestContext);
+        if (rpCtx != null
+                && rpCtx.getProfileConfig() instanceof OAuth2DPoPProofValidatingProfileConfiguration configuration) {
+            validator = configuration.getDpopProofClaimsValidator(profileRequestContext);
+        }
         if (profileRequestContext.getInboundMessageContext() == null || !(profileRequestContext
                 .getInboundMessageContext().getMessage() instanceof OpenIDVCICredentialsRequest)) {
             log.error("{} No OpenIDVCICredentialsRequest as inbound message", getLogPrefix());
@@ -81,20 +129,14 @@ public class ParseProof extends AbstractProfileAction {
             return;
         }
         List<JWSObject> proofs = new ArrayList<JWSObject>();
-        if (proofToken instanceof String token) {
-            try {
-                proofs.add(JWSObject.parse(token));
-            } catch (ParseException e) {
-                log.error("{} proof {} parsing failed.", getLogPrefix(), proof, e);
-                ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.INVALID_PROOF);
-                return;
-            }
-        } else if (proofToken instanceof List<?> tokens) {
+        if (proofToken instanceof List<?> tokens) {
             tokens.forEach(token -> {
                 if (token instanceof String strToken) {
                     try {
-                        proofs.add(JWSObject.parse(strToken));
-                    } catch (ParseException e) {
+                        JWSObject singleProof = JWSObject.parse(strToken);
+                        validateJWTProof(singleProof, profileRequestContext);
+                        proofs.add(singleProof);
+                    } catch (Exception e) {
                         log.error("{} proof {} parsing failed.", getLogPrefix(), proof, e);
                         ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.INVALID_PROOF);
                         return;
@@ -107,4 +149,28 @@ public class ParseProof extends AbstractProfileAction {
         ctx.setProofs(proofs);
 
     }
+
+    /**
+     * Validate jwt proof per
+     * https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-jwt-proof-type
+     * 
+     * @param proof proof to validate
+     * @throws Exception thrown if validation fails
+     */
+    private void validateJWTProof(@Nonnull JWSObject proof, @Nonnull final ProfileRequestContext profileRequestContext)
+            throws Exception {
+        assert proof != null;
+        if (JWSAlgorithm.Family.HMAC_SHA.contains(proof.getHeader().getAlgorithm())) {
+            throw new Exception("HMAC algorithm used: " + proof.getHeader().getAlgorithm().getName());
+        }
+        if (!proof.getHeader().getType().equals(new JOSEObjectType("openid4vci-proof+jwt"))) {
+            throw new Exception("typ should be openid4vci-proof+jwt: " + proof.getHeader().getType());
+        }
+        // TBD validate header parameters kid, jwk, x5c, key_attestation and trust_chain
+        // TBD validate signature
+        if (validator != null) {
+            // TBD validate body parameters iss, aud, iat and nonce
+            validator.validate(JWTClaimsSet.parse(proof.getPayload().toJSONObject()), profileRequestContext);
+        }
+    }
 }
\ No newline at end of file
diff --git a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
index 7e77d6c..9e370b9 100644
--- a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
+++ b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
@@ -15,7 +15,6 @@
     <bean id="shibboleth.PropertySourcesPlaceholderConfigurer"
         class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer"
         p:placeholderPrefix="%{" p:placeholderSuffix="}" />
-
     
     <bean id="AbstractVCIProfile" abstract="true"
         p:issuer="#{getObject('shibboleth.oidc.issuer')}"
@@ -40,12 +39,13 @@
     <bean id="OpenID.VCI.Token" parent="AbstractVCIProfile" lazy-init="true"
         class="org.geant.shibboleth.plugin.openidvci.config.impl.DefaultOpenIDVCITokenConfiguration" />
 
-    <bean id="OpenID.VCI.Credentials" parent="AbstractVCIProfile" lazy-init="true"
-        class="org.geant.shibboleth.plugin.openidvci.config.impl.DefaultOpenIDVCICredentialsConfiguration" />
-
     <bean id="OpenID.VCI.Nonce" parent="AbstractVCIProfile" lazy-init="true"
         class="org.geant.shibboleth.plugin.openidvci.config.impl.DefaultOpenIDVCINonceConfiguration"
         p:dpopProofNonceGenerator="#{getObject('DefaultOpenIDVCINonceGenerator')}" />
+        
+    <bean id="OpenID.VCI.Credentials" parent="AbstractVCIProfile" lazy-init="true"
+        class="org.geant.shibboleth.plugin.openidvci.config.impl.DefaultOpenIDVCICredentialsConfiguration"
+        p:dpopProofClaimsValidator="#{getObject('DefaultProofBodyClaimsValidator')}" />
     
     
     <!-- We use special DPoP Nonce generator for Nonce endpoint. -->
@@ -60,5 +60,18 @@
             <bean class="java.lang.String" c:_0="mockRelyingParty" />
          </constructor-arg>
     </bean>
+    
+    <bean id="DefaultProofBodyClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator"
+        p:claimValidators-ref="ProofBodyClaimsValidator" />
+ 
+    <util:list id="ProofBodyClaimsValidator" value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
+        <bean id="DPoPProofNonceClaimsValidator"
+            class="net.shibboleth.idp.plugin.oidc.op.security.jwt.claims.impl.DPoPProofNonceClaimsValidator"
+            p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper"
+            c:sealer-ref="DefaultDPoPNonceSealer"
+            p:relyingPartyIdLookupStrategy-ref="openidvci.RelyingPartyForNonce">
+        </bean>
+    </util:list>
 
 </beans>
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 a70b2d2..117d0f0 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
@@ -85,7 +85,39 @@ public class ParseProofTest {
         profileRequestCtx.getInboundMessageContext().setMessage(OpenIDVCICredentialsRequest.parse(httpRequest));
         ActionTestingSupport.assertEvent(action.execute(requestCtx), OpenIDVCIEventIds.PROOF_TYPE_UNSUPPORTED);
     }
-
+    
+    @Test
+    public void testAlgNone() throws ParseException {
+        httpRequest.setQuery("{\n" + "  \"credential_configuration_id\": \"org.iso.18013.5.1.mDL\",\n"
+                + "  \"proofs\": {\n"
+                + "    \"jwt\": [\"eyJ0eXAiOiJvcGVuaWQ0dmNpLXByb29mK2p3dCIsImFsZyI6Im5vbmUiLCJraWQiOiJkaWQ6andrOmV5SmhiR2NpT2lKRlV6STFOaUlzSW5WelpTSTZJbk5wWnlJc0ltdDBlU0k2SWtWRElpd2lZM0oySWpvaVVDMHlOVFlpTENKNElqb2lVa0ZDZFVoc1NHTkhiR3AyVVZWSmRVOXJhM0pCUkRoclgzZFRUVlJoY25Cck5HSnlOelpCV1dRMmF5SXNJbmtpT2lKV1gwRk1UREpqYURoQlYxOUdXSEoxUzFadVlrNXRTemhSZUVSek1uUTRSalJ1TlRKUWFFbDFWMU5uSW4wIzAifQ.eyJhdWQiOiJodHRwczovL2dlYW50LXZjaS4yLnJhaHRpYXBwLmZpL29pZDR2Y2kiLCJpYXQiOjE3NDQyNzU1OTEsImV4cCI6MTc0ND [...]
+                + "  }\n" + "}\n" + "");
+        profileRequestCtx.getInboundMessageContext().setMessage(OpenIDVCICredentialsRequest.parse(httpRequest));
+        ActionTestingSupport.assertEvent(action.execute(requestCtx), OpenIDVCIEventIds.INVALID_PROOF);
+    }
+    
+    @Test
+    public void testAlgHMAC() throws ParseException {
+        
+        httpRequest.setQuery("{\n" + "  \"credential_configuration_id\": \"org.iso.18013.5.1.mDL\",\n"
+                + "  \"proofs\": {\n"
+                + "    \"jwt\": [\"eyJ0eXAiOiJvcGVuaWQ0dmNpLXByb29mK2p3dCIsImFsZyI6IkhTMjU2Iiwia2lkIjoiZGlkOmp3azpleUpoYkdjaU9pSkZVekkxTmlJc0luVnpaU0k2SW5OcFp5SXNJbXQwZVNJNklrVkRJaXdpWTNKMklqb2lVQzB5TlRZaUxDSjRJam9pVWtGQ2RVaHNTR05IYkdwMlVWVkpkVTlyYTNKQlJEaHJYM2RUVFZSaGNuQnJOR0p5TnpaQldXUTJheUlzSW5raU9pSldYMEZNVERKamFEaEJWMTlHV0hKMVMxWnVZazV0U3poUmVFUnpNblE0UmpSdU5USlFhRWwxVjFObkluMCMwIn0.eyJhdWQiOiJodHRwczovL2dlYW50LXZjaS4yLnJhaHRpYXBwLmZpL29pZDR2Y2kiLCJpYXQiOjE3NDQyNzU1OTEsImV4cCI6MTc0N [...]
+                + "  }\n" + "}\n" + "");
+        profileRequestCtx.getInboundMessageContext().setMessage(OpenIDVCICredentialsRequest.parse(httpRequest));
+        ActionTestingSupport.assertEvent(action.execute(requestCtx), OpenIDVCIEventIds.INVALID_PROOF);
+    }
+    
+    @Test
+    public void testWrongType() throws ParseException {
+        
+        httpRequest.setQuery("{\n" + "  \"credential_configuration_id\": \"org.iso.18013.5.1.mDL\",\n"
+                + "  \"proofs\": {\n"
+                + "    \"jwt\": [\"eyJ0eXAiOiJzb21lZDR2Y2ktcHJvb2Yrand0IiwiYWxnIjoiRVMyNTYiLCJraWQiOiJkaWQ6andrOmV5SmhiR2NpT2lKRlV6STFOaUlzSW5WelpTSTZJbk5wWnlJc0ltdDBlU0k2SWtWRElpd2lZM0oySWpvaVVDMHlOVFlpTENKNElqb2lVa0ZDZFVoc1NHTkhiR3AyVVZWSmRVOXJhM0pCUkRoclgzZFRUVlJoY25Cck5HSnlOelpCV1dRMmF5SXNJbmtpT2lKV1gwRk1UREpqYURoQlYxOUdXSEoxUzFadVlrNXRTemhSZUVSek1uUTRSalJ1TlRKUWFFbDFWMU5uSW4wIzAifQ.eyJhdWQiOiJodHRwczovL2dlYW50LXZjaS4yLnJhaHRpYXBwLmZpL29pZDR2Y2kiLCJpYXQiOjE3NDQyNzU1OTEsImV4cCI6MTc0ND [...]
+                + "  }\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