[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