[java-idp-plugin-vci] 01/01: W3C VCDM SD JWT credential support improved

Codeberg noreply at shibboleth.net
Mon Apr 27 16:07:55 UTC 2026


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

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

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

commit 46418683ffffd40f8444596e69e398a3f6d1753d
Author: Janne Lauros <janne.lauros at csc.fi>
AuthorDate: Mon Apr 27 19:07:18 2026 +0300

    W3C VCDM SD JWT credential support improved
---
 .../openidvci/profile/impl/AddCredentialShell.java | 40 ++++++++++++++++++----
 ...FormJsonLdSelectiveDisclosureJWTCredential.java | 38 +++++++++++---------
 2 files changed, 55 insertions(+), 23 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 867f040..5d9a082 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
@@ -15,10 +15,13 @@
  */
 package org.geant.shibboleth.plugin.openidvci.profile.impl;
 
+import java.nio.charset.StandardCharsets;
+import java.text.ParseException;
 import java.time.Duration;
 import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
+import java.util.Base64;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -46,6 +49,7 @@ import org.slf4j.Logger;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jose.jwk.JWK;
 import com.nimbusds.oauth2.sdk.id.Audience;
 import com.nimbusds.oauth2.sdk.id.Issuer;
 import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
@@ -186,8 +190,25 @@ public class AddCredentialShell extends AbstractProfileAction {
                 ClaimsSet shell = createShell();
                 Map<String, Object> cnfKid = new HashMap<>();
                 try {
-                    cnfKid.put("jwk",
-                            new ObjectMapper().readValue(proof.getHeader().getJWK().toJSONString(), Object.class));
+                    // This is actually work that has already been done by credential resolver.
+                    // TODO common util for parsing for this here and in resolver!
+                    if (proof.getHeader().getJWK() != null) {
+                        cnfKid.put("jwk",
+                                new ObjectMapper().readValue(proof.getHeader().getJWK().toJSONString(), Object.class));
+                    } else if (proof.getHeader().getKeyID() != null) {
+                        String kid = proof.getHeader().getKeyID();
+                        if (kid != null && kid.startsWith("did:jwk:") && kid.endsWith("#0")) {
+                            String encoded = kid.substring("did:jwk:".length(), kid.indexOf('#'));
+                            try {
+                                final JWK didJwk = JWK.parse(
+                                        new String(Base64.getUrlDecoder().decode(encoded), StandardCharsets.UTF_8));
+                                cnfKid.put("jwk", new ObjectMapper().readValue(didJwk.toJSONString(), Object.class));
+                                ((Map) cnfKid.get("jwk")).put("kid", kid);
+                            } catch (ParseException e) {
+                                log.warn("Failed parsing kid {} as did:jwk", kid, e);
+                            }
+                        }
+                    }
                     shell.setClaim("cnf", cnfKid);
                 } catch (JsonProcessingException e1) {
                     log.error("{} Parsing failed", getLogPrefix(), e1);
@@ -213,11 +234,16 @@ public class AddCredentialShell extends AbstractProfileAction {
         shell.setIssuer(new Issuer(issuerId));
         shell.setAudience(new Audience(rpCtx.getRelyingPartyId()));
         ZonedDateTime now = ZonedDateTime.now();
-        shell.setClaim("iat", now.toEpochSecond());
-        shell.setClaim("exp", now.toEpochSecond() + expiration.toSeconds());
-        shell.setClaim("validFrom", DateTimeFormatter.ISO_DATE_TIME.format(now));
-        shell.setClaim("validUntil", DateTimeFormatter.ISO_DATE_TIME.format(now.plusSeconds(expiration.getSeconds())));
-        shell.setClaim("vct", ctx.getCredentialIdentifier());
+        if ("dc+sd-jwt".equals(ctx.getCredentialConfiguration().getFormat())) {
+            shell.setClaim("iat", now.toEpochSecond());
+            shell.setClaim("exp", now.toEpochSecond() + expiration.toSeconds());
+            shell.setClaim("vct", ctx.getCredentialIdentifier());
+        }
+        if ("vc+sd-jwt".equals(ctx.getCredentialConfiguration().getFormat())) {
+            shell.setClaim("validFrom", DateTimeFormatter.ISO_DATE_TIME.format(now));
+            shell.setClaim("validUntil",
+                    DateTimeFormatter.ISO_DATE_TIME.format(now.plusSeconds(expiration.getSeconds())));
+        }
         return shell;
     }
 
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormJsonLdSelectiveDisclosureJWTCredential.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormJsonLdSelectiveDisclosureJWTCredential.java
index 7be8479..1c8f217 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormJsonLdSelectiveDisclosureJWTCredential.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormJsonLdSelectiveDisclosureJWTCredential.java
@@ -42,11 +42,11 @@ import com.nimbusds.oauth2.sdk.ParseException;
 
 /**
  * Action that forms
- * {@link https://datatracker.ietf.org/doc/draft-ietf-oauth-sd-jwt-vc/13/}.
+ * {@link hhttps://www.w3.org/TR/2025/REC-vc-jose-cose-20250515/#with-sd-jwt}.
  * 
  * 
- * Action verifies first that we are indeed forming 'jwt_vc_json-ld' type
- * credential and then combines content for disclosure
+ * Action verifies first that we are indeed forming 'vc+sd-jwt' type credential
+ * and then combines content for disclosure
  * {@link CredentialsContext#getCredentialContent()} with credential shells
  * {@link CredentialsContext#getCredentialShells()}. The outcome is a list of
  * JWTs ready to be signed. The list is stored to
@@ -83,8 +83,8 @@ public class FormJsonLdSelectiveDisclosureJWTCredential extends AbstractProfileA
             return false;
         }
         if (ctx.getCredentialConfiguration() == null
-                || !"jwt_vc_json-ld".equals(ctx.getCredentialConfiguration().getFormat())) {
-            log.debug("{} No reason to continue. Credential configuration is not 'jwt_vc_json-ld'", getLogPrefix());
+                || !"vc+sd-jwt".equals(ctx.getCredentialConfiguration().getFormat())) {
+            log.debug("{} No reason to continue. Credential configuration is not 'vc+sd-jwt'", getLogPrefix());
             return false;
         }
         credential = ctx.getCredentialContent();
@@ -104,22 +104,28 @@ public class FormJsonLdSelectiveDisclosureJWTCredential extends AbstractProfileA
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
 
+        Map<List<String>, Map<String, Object>> sdMaps = new HashMap<>();
+        for (CredentialOfferRequestedClaim claim : credential.getRequestedCredential()) {
+            List<String> path = new ArrayList<>(claim.getPath());
+            path.remove(path.size() - 1);
+            if (sdMaps.get(path) == null) {
+                sdMaps.put(path, new HashMap<>());
+            }
+            sdMaps.get(path).put(claim.getPath().get(claim.getPath().size() - 1), claim.getValue());
+        }
         String disclosures = null;
         Map<String, Object> credentialSubject = new HashMap<>();
-        for (CredentialOfferRequestedClaim claim : credential.getRequestedCredential()) {
+        for (List<String> path : sdMaps.keySet()) {
             Map<String, Object> map = credentialSubject;
-            for (int i = 0; i < claim.getPath().size() - 1; i++) {
-                if (!map.containsKey(claim.getPath().get(i))) {
-                    map.put(claim.getPath().get(i), new HashMap<String, Object>());
+            for (int i = 1; i < path.size() - 1; i++) {
+                if (!map.containsKey(path.get(i))) {
+                    map.put(path.get(i), new HashMap<String, Object>());
                 }
-                map = ((Map<String, Object>) map.get(claim.getPath().get(i)));
+                map = ((Map<String, Object>) map.get(path.get(i)));
+                log.info("map is the value of {}", path.get(i));
             }
-            Map<String, Object> mapClaim = new HashMap<>();
-            mapClaim.put(claim.getPath().get(claim.getPath().size() - 1), claim.getValue());
-            SelectiveDisclosureClaimSetUtil sdActClaims = new SelectiveDisclosureClaimSetUtil(mapClaim);
-            Map<String, Object> item = new HashMap<>();
-            item.put("_sd", sdActClaims.get_sd());
-            map.put(claim.getPath().get(claim.getPath().size() - 1), item);
+            SelectiveDisclosureClaimSetUtil sdActClaims = new SelectiveDisclosureClaimSetUtil(sdMaps.get(path));
+            map.put("_sd", sdActClaims.get_sd());
             disclosures = (disclosures == null ? sdActClaims.getFormattedDisclosures()
                     : disclosures + "~" + sdActClaims.getFormattedDisclosures());
         }

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


More information about the commits mailing list