[java-idp-plugin-vci] 02/02: Initial support for forming 'jwt_vc_json-ld' credential

Codeberg noreply at shibboleth.net
Fri Jan 9 12:05:51 UTC 2026


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

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

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

commit f7d4e0177cadf4c2b71d2d49a62d985608526757
Author: jlauros <janne.lauros at csc.fi>
AuthorDate: Fri Jan 9 14:05:28 2026 +0200

    Initial support for forming 'jwt_vc_json-ld' credential
---
 ...FormJsonLdSelectiveDisclosureJWTCredential.java | 144 +++++++++++++++++++++
 .../openidvci/profile/impl/SignJWTCredential.java  |  22 +++-
 .../openid/vci/credentials/credentials-beans.xml   |   3 +
 .../openid/vci/credentials/credentials-flow.xml    |   1 +
 4 files changed, 168 insertions(+), 2 deletions(-)

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
new file mode 100644
index 0000000..a8efc22
--- /dev/null
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormJsonLdSelectiveDisclosureJWTCredential.java
@@ -0,0 +1,144 @@
+/*
+ * 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.profile.impl;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+import org.geant.shibboleth.plugin.openidvci.messaging.context.CredentialsContext;
+import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialOfferRequestedClaim;
+import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialOfferRequestedCredential;
+import org.geant.shibboleth.plugin.openidvci.profile.OpenIDVCIEventIds;
+import org.geant.shibboleth.plugin.openidvci.util.SelectiveDisclosureClaimSetUtil;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.oauth2.sdk.ParseException;
+
+/**
+ * Action that forms
+ * {@link https://datatracker.ietf.org/doc/draft-ietf-oauth-sd-jwt-vc/13/}.
+ * 
+ * 
+ * Action verifies first that we are indeed forming 'jwt_vc_json-ld' 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
+ * {@link CredentialsContext#setJWTCredentials}. The disclosure data is stored
+ * to {@link CredentialsContext#setDisclosures}
+ * 
+ * TODO, only partial support. Missing still numerous fields not yet parsed by
+ * {@link CredentialConfiguration}. Also id parameter is never set.
+ * 
+ */
+public class FormJsonLdSelectiveDisclosureJWTCredential extends AbstractProfileAction {
+
+    /** Class logger. */
+    @Nonnull
+    private Logger log = LoggerFactory.getLogger(FormJsonLdSelectiveDisclosureJWTCredential.class);
+
+    @NonnullBeforeExec
+    private CredentialsContext ctx;
+
+    @NonnullBeforeExec
+    CredentialOfferRequestedCredential credential;
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        ctx = profileRequestContext.getInboundMessageContext().getSubcontext(CredentialsContext.class);
+        if (ctx == null) {
+            log.error("{} No credentials context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            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());
+            return false;
+        }
+        credential = ctx.getCredentialContent();
+        if (credential == null) {
+            log.warn("{} No credential content in context, nothingh to do", getLogPrefix());
+            return false;
+        }
+        if (ctx.getCredentialShells() == null || ctx.getCredentialShells().isEmpty()) {
+            log.warn("{} No shells stored to context, nothing to do", getLogPrefix());
+            return false;
+        }
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @SuppressWarnings("unchecked")
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        String disclosures = null;
+        Map<String, Object> credentialSubject = new HashMap<>();
+        for (CredentialOfferRequestedClaim claim : credential.getRequestedCredential()) {
+            Map<String, Object> map = credentialSubject;
+            for (int i = 1; i < claim.getPath().size() - 1; i++) {
+                if (!map.containsKey(claim.getPath().get(i))) {
+                    map.put(claim.getPath().get(i), new HashMap<String, Object>());
+                }
+                map = ((Map<String, Object>) map.get(claim.getPath().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());
+            item.put("name", "get from configurarion");
+            item.put("description", "get from configurarion");
+            map.put(claim.getPath().get(claim.getPath().size() - 1), item);
+            disclosures = (disclosures == null ? sdActClaims.getFormattedDisclosures()
+                    : disclosures + "~" + sdActClaims.getFormattedDisclosures());
+        }
+        List<JWTClaimsSet> credentials = new ArrayList<>();
+        ctx.getCredentialShells().forEach(cred -> {
+            try {
+                credentials.add(new JWTClaimsSet.Builder(cred.toJWTClaimsSet())
+                        .claim("@context", ctx.getCredentialConfiguration().getCredentialDefinition().getContext())
+                        .claim("type", ctx.getCredentialConfiguration().getCredentialDefinition().getType())
+                        .claim("credentialSubject", credentialSubject).claim("_sd_alg", "sha-256").build());
+            } catch (ParseException e) {
+                log.error("{} Parsing credential failed", getLogPrefix(), e);
+                ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.INVALID_CREDENTIAL);
+                return;
+            }
+        });
+        ctx.setJWTCredentials(credentials);
+        ctx.setDisclosures(disclosures);
+    }
+}
\ No newline at end of file
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/SignJWTCredential.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/SignJWTCredential.java
index 9dac333..381a1ab 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/SignJWTCredential.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/SignJWTCredential.java
@@ -188,8 +188,9 @@ public class SignJWTCredential extends AbstractOIDCResponseAction {
                 SignedJWT signedJWT = new SignedJWT(
                         new JWSHeader.Builder(new JWSAlgorithm(signatureSigningParameters.getSignatureAlgorithm()))
                                 .x509CertChain(certificateChainLookupStrategy.apply(signatureSigningParameters))
-                                .type(new JOSEObjectType(ctx.getCredentialConfiguration().getFormat())).keyID("signing")
-                                .build(),
+                                .type(new JOSEObjectType(
+                                        CredentialFormatToJoseType(ctx.getCredentialConfiguration().getFormat())))
+                                .keyID("signing").build(),
                         credential);
                 try {
                     signedJWT.sign(signer);
@@ -207,4 +208,21 @@ public class SignJWTCredential extends AbstractOIDCResponseAction {
             ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
         }
     }
+
+    /**
+     * Get JOSE type for credential format. For
+     * https://datatracker.ietf.org/doc/html/draft-ietf-oauth-sd-jwt-vc-11 type is
+     * "dc+sd-jwt" and for https://www.w3.org/TR/vc-jose-cose/#securing-with-sd-jwt
+     * type is "vc+sd-jwt". Propably only mismatching versions but for now followed.
+     * 
+     * @param format jwt_vc_json-ld for W3C sd jwt, otherwise sd jwt assumed.
+     * @return JOSE Type
+     */
+    private String CredentialFormatToJoseType(String format) {
+        switch (format) {
+        case "jwt_vc_json-ld":
+            return "vc+sd-jwt";
+        }
+        return "dc+sd-jwt";
+    }
 }
\ No newline at end of file
diff --git a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/credentials/credentials-beans.xml b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/credentials/credentials-beans.xml
index 88263c3..15ecc2f 100644
--- a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/credentials/credentials-beans.xml
+++ b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/credentials/credentials-beans.xml
@@ -76,6 +76,9 @@
         
   <bean id="OptionallyFormSelectiveDisclosureJWTCredential" class="org.geant.shibboleth.plugin.openidvci.profile.impl.FormSelectiveDisclosureJWTCredential"
         scope="prototype" />
+        
+  <bean id="OptionallyFormJsonLdSelectiveDisclosureJWTCredential" class="org.geant.shibboleth.plugin.openidvci.profile.impl.FormJsonLdSelectiveDisclosureJWTCredential"
+        scope="prototype" />
   
   <bean id="SignJWTCredential" class="org.geant.shibboleth.plugin.openidvci.profile.impl.SignJWTCredential"
         scope="prototype" p:issuerLookupStrategy-ref="shibboleth.ResponderIdLookup.Simple" >
diff --git a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/credentials/credentials-flow.xml b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/credentials/credentials-flow.xml
index 14a41d3..8528777 100644
--- a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/credentials/credentials-flow.xml
+++ b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/credentials/credentials-flow.xml
@@ -38,6 +38,7 @@
   <action-state id="BuildResponse">
     <evaluate expression="AddCredentialShell" />
     <evaluate expression="OptionallyFormSelectiveDisclosureJWTCredential" />
+    <evaluate expression="OptionallyFormJsonLdSelectiveDisclosureJWTCredential" />
     <evaluate expression="SignJWTCredential" />
     <evaluate expression="'proceed'"/>
     <transition on="proceed" to="BuildResponseMessage"/>

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


More information about the commits mailing list