[java-idp-plugin-vci] 01/07: Finally verify the key proof signature

Codeberg noreply at shibboleth.net
Thu Sep 24 13:16:26 UTC 2026


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

codeberg pushed a commit to branch main
in repository java-idp-plugin-vci.

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

commit ac862ea3765aec763f83b6c46174e5bcc386b353
Author: Janne Lauros <janne.lauros at csc.fi>
AuthorDate: Thu Sep 24 16:04:00 2026 +0300

    Finally verify the key proof signature
---
 .../impl/ExtendedJOSEObjectCredentialResolver.java |   2 +-
 .../security/impl/TokenKeyTrustEngine.java         |  19 +++-
 .../security/impl/TokenKeyTrustEngineTest.java     | 116 +++++++++++++++++++++
 3 files changed, 131 insertions(+), 6 deletions(-)

diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/security/credential/impl/ExtendedJOSEObjectCredentialResolver.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/security/credential/impl/ExtendedJOSEObjectCredentialResolver.java
index ca3c090..dc3b2f0 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/security/credential/impl/ExtendedJOSEObjectCredentialResolver.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/security/credential/impl/ExtendedJOSEObjectCredentialResolver.java
@@ -88,7 +88,7 @@ public class ExtendedJOSEObjectCredentialResolver extends BasicJOSEObjectCredent
             try {
                 final JWK didJwk = JWK
                         .parse(new String(Base64.getUrlDecoder().decode(encoded), StandardCharsets.UTF_8));
-                final Credential cred = buildJWKCredential(didJwk, kid);
+                final Credential cred = buildJWKCredential(didJwk, didJwk.getKeyID());
                 if (cred != null) {
                     credentials.add(cred);
                 }
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/security/impl/TokenKeyTrustEngine.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/security/impl/TokenKeyTrustEngine.java
index e1ca4e3..9476d4c 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/security/impl/TokenKeyTrustEngine.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/security/impl/TokenKeyTrustEngine.java
@@ -36,10 +36,10 @@ import net.shibboleth.shared.resolver.CriteriaSet;
 /**
  * A {@link TrustEngine} implementation for Signed JSON Web Tokens. The token
  * derived public key is used as the trust basis.
- * 
- * Note, trust engine does not validate the jwk in any way but lets the wired
- * resolver to do the work.
- * 
+ *
+ * <p>The signature must verify with a key the token itself carries, the wired resolver deciding
+ * which keys those are. Whether such a key is one to trust is established outside this engine, by
+ * the key attestation of a key proof.</p>
  */
 public class TokenKeyTrustEngine extends BaseSignedJWTTrustEngine<Credential> {
 
@@ -62,7 +62,16 @@ public class TokenKeyTrustEngine extends BaseSignedJWTTrustEngine<Credential> {
     @Override
     protected boolean doValidate(@Nonnull final SignedJWT signedJWT, @Nonnull final CriteriaSet trustBasisCriteria)
             throws SecurityException {
-        return true;
+
+        for (final Credential credential : resolveTokenCredentials(signedJWT)) {
+            assert credential != null;
+            if (verifySignature(signedJWT, credential)) {
+                return true;
+            }
+        }
+
+        log.warn("Signature of the token is verified by none of the keys the token carries");
+        return false;
     }
 
     /** {@inheritDoc} */
diff --git a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/security/impl/TokenKeyTrustEngineTest.java b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/security/impl/TokenKeyTrustEngineTest.java
new file mode 100644
index 0000000..59fd739
--- /dev/null
+++ b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/security/impl/TokenKeyTrustEngineTest.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) 2025, GÉANT
+ *
+ * Licensed under the Apache License, Version 2.0 (the “License”); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an “AS IS” BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.geant.shibboleth.plugin.openidvci.security.impl;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+import org.geant.shibboleth.plugin.openidvci.security.credential.impl.ExtendedJOSEObjectCredentialResolver;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.crypto.ECDSASigner;
+import com.nimbusds.jose.jwk.Curve;
+import com.nimbusds.jose.jwk.ECKey;
+import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.oidc.security.jose.SignatureValidationParameters;
+import net.shibboleth.oidc.security.jose.criterion.SignatureValidationParametersCriterion;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/** Unit tests for {@link TokenKeyTrustEngine}. */
+public class TokenKeyTrustEngineTest {
+
+    private TokenKeyTrustEngine engine;
+
+    private ECKey key;
+
+    private CriteriaSet criteria;
+
+    @BeforeMethod
+    protected void setUp() throws Exception {
+        engine = new TokenKeyTrustEngine(new ExtendedJOSEObjectCredentialResolver());
+        key = new ECKeyGenerator(Curve.P_256).algorithm(JWSAlgorithm.ES256).keyID("holder").generate();
+        criteria = new CriteriaSet(new SignatureValidationParametersCriterion(new SignatureValidationParameters()));
+    }
+
+    private SignedJWT proofWithKeyInHeader() throws Exception {
+        final SignedJWT proof = new SignedJWT(
+                new JWSHeader.Builder(JWSAlgorithm.ES256).type(new JOSEObjectType("openid4vci-proof+jwt"))
+                        .jwk(key.toPublicJWK()).build(),
+                new JWTClaimsSet.Builder().issuer("client").audience("https://issuer.example.org").build());
+        proof.sign(new ECDSASigner(key));
+
+        return proof;
+    }
+
+    private SignedJWT proofWithKeyInDidJwkKid() throws Exception {
+        final String did = "did:jwk:" + Base64.getUrlEncoder().withoutPadding()
+                .encodeToString(key.toPublicJWK().toJSONString().getBytes(StandardCharsets.UTF_8)) + "#0";
+        final SignedJWT proof = new SignedJWT(
+                new JWSHeader.Builder(JWSAlgorithm.ES256).type(new JOSEObjectType("openid4vci-proof+jwt"))
+                        .keyID(did).build(),
+                new JWTClaimsSet.Builder().issuer("client").audience("https://issuer.example.org").build());
+        proof.sign(new ECDSASigner(key));
+
+        return proof;
+    }
+
+    private SignedJWT withSignatureOf(final SignedJWT proof, final SignedJWT other) throws Exception {
+        final String[] parts = proof.serialize().split("\\.");
+
+        return SignedJWT.parse(parts[0] + "." + parts[1] + "." + other.serialize().split("\\.")[2]);
+    }
+
+    @Test
+    public void testSignatureOfTheKeyInTheHeader() throws Exception {
+        Assert.assertTrue(engine.validate(proofWithKeyInHeader(), criteria));
+    }
+
+    @Test
+    public void testSignatureOfTheKeyTheDidJwkKidNames() throws Exception {
+        Assert.assertTrue(engine.validate(proofWithKeyInDidJwkKid(), criteria));
+    }
+
+    @Test
+    public void testTamperedSignatureIsRefused() throws Exception {
+        final SignedJWT signedByAnotherKey = new SignedJWT(
+                new JWSHeader.Builder(JWSAlgorithm.ES256).build(),
+                new JWTClaimsSet.Builder().issuer("someone else").build());
+        signedByAnotherKey.sign(new ECDSASigner(new ECKeyGenerator(Curve.P_256).generate()));
+
+        Assert.assertFalse(
+                engine.validate(withSignatureOf(proofWithKeyInHeader(), signedByAnotherKey), criteria));
+    }
+
+    @Test
+    public void testProofCarryingNoKeyIsRefused() throws Exception {
+        final SignedJWT proof = new SignedJWT(
+                new JWSHeader.Builder(JWSAlgorithm.ES256).type(new JOSEObjectType("openid4vci-proof+jwt")).build(),
+                new JWTClaimsSet.Builder().issuer("client").build());
+        proof.sign(new ECDSASigner(key));
+
+        Assert.assertFalse(engine.validate(proof, criteria));
+    }
+
+}

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list