[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