1+ /*
2+ * Copyright (c) 2026 Metaform Systems Inc.
3+ *
4+ * This program and the accompanying materials are made available under the
5+ * terms of the Apache License, Version 2.0 which is available at
6+ * https://www.apache.org/licenses/LICENSE-2.0
7+ *
8+ * SPDX-License-Identifier: Apache-2.0
9+ *
10+ * Contributors:
11+ * Metaform Systems Inc. - initial API and implementation
12+ *
13+ */
14+
15+ package org .eclipse .edc .issuerservice .issuance .generator ;
16+
17+ import org .eclipse .edc .iam .verifiablecredentials .spi .model .CredentialFormat ;
18+ import org .eclipse .edc .iam .verifiablecredentials .spi .model .CredentialStatus ;
19+ import org .eclipse .edc .iam .verifiablecredentials .spi .model .CredentialSubject ;
20+ import org .eclipse .edc .iam .verifiablecredentials .spi .model .DataModelVersion ;
21+ import org .eclipse .edc .iam .verifiablecredentials .spi .model .Issuer ;
22+ import org .eclipse .edc .iam .verifiablecredentials .spi .model .VerifiableCredential ;
23+ import org .eclipse .edc .iam .verifiablecredentials .spi .model .VerifiableCredentialContainer ;
24+ import org .eclipse .edc .issuerservice .spi .issuance .generator .CredentialGenerator ;
25+ import org .eclipse .edc .issuerservice .spi .issuance .model .CredentialDefinition ;
26+ import org .eclipse .edc .jsonld .spi .JsonLdKeywords ;
27+ import org .eclipse .edc .spi .iam .TokenRepresentation ;
28+ import org .eclipse .edc .spi .result .Result ;
29+ import org .eclipse .edc .token .spi .KeyIdDecorator ;
30+ import org .eclipse .edc .token .spi .TokenDecorator ;
31+ import org .eclipse .edc .token .spi .TokenGenerationService ;
32+
33+ import java .time .Clock ;
34+ import java .time .Instant ;
35+ import java .util .ArrayList ;
36+ import java .util .Arrays ;
37+ import java .util .Date ;
38+ import java .util .HashMap ;
39+ import java .util .List ;
40+ import java .util .Map ;
41+ import java .util .UUID ;
42+
43+ import static org .eclipse .edc .issuerservice .issuance .generator .Constants .CREDENTIAL_STATUS ;
44+ import static org .eclipse .edc .issuerservice .issuance .generator .Constants .CREDENTIAL_SUBJECT ;
45+ import static org .eclipse .edc .issuerservice .issuance .generator .Constants .ID ;
46+ import static org .eclipse .edc .issuerservice .issuance .generator .Constants .ISSUER ;
47+ import static org .eclipse .edc .issuerservice .issuance .generator .Constants .TYPE ;
48+ import static org .eclipse .edc .issuerservice .issuance .generator .Constants .VALID_FROM ;
49+ import static org .eclipse .edc .issuerservice .issuance .generator .Constants .VERIFIABLE_CREDENTIAL ;
50+ import static org .eclipse .edc .issuerservice .issuance .generator .Constants .W3C_CREDENTIALS_URL_V2 ;
51+ import static org .eclipse .edc .jwt .spi .JwtRegisteredClaimNames .EXPIRATION_TIME ;
52+
53+ public class JoseVcdm20CredentialGenerator implements CredentialGenerator {
54+
55+ private final TokenGenerationService tokenGenerationService ;
56+ private final Clock clock ;
57+
58+ public JoseVcdm20CredentialGenerator (TokenGenerationService tokenGenerationService , Clock clock ) {
59+ this .tokenGenerationService = tokenGenerationService ;
60+ this .clock = clock ;
61+ }
62+
63+ @ Override
64+ public Result <VerifiableCredentialContainer > generateCredential (String participantContextId , CredentialDefinition definition ,
65+ String privateKeyAlias , String publicKeyId , String issuerId ,
66+ String holderDid , Map <String , Object > claims ) {
67+
68+ var subjectResult = extractCredentialSubject (claims );
69+ if (subjectResult .failed ()) {
70+ return subjectResult .mapFailure ();
71+ }
72+
73+ var statusResult = createCredentialStatus (claims );
74+
75+ var ctx = new ArrayList <>();
76+ ctx .add (W3C_CREDENTIALS_URL_V2 );
77+ ctx .addAll (definition .getAdditionalContext ());
78+ //noinspection unchecked
79+ var builder = VerifiableCredential .Builder .newInstance ()
80+ .id (UUID .randomUUID ().toString ())
81+ .issuer (new Issuer (issuerId ))
82+ .contexts (ctx )
83+ .dataModelVersion (DataModelVersion .V_2_0 )
84+ .issuanceDate (Instant .now (clock ))
85+ .expirationDate (Instant .now (clock ).plusSeconds (definition .getValidity ()))
86+ .types (List .of (VERIFIABLE_CREDENTIAL , definition .getCredentialType ()))
87+ .credentialSubject (CredentialSubject .Builder .newInstance ()
88+ .id (holderDid )
89+ .claims (subjectResult .getContent ())
90+ .build ());
91+
92+ statusResult .onSuccess (builder ::credentialStatus );
93+
94+ var credential = builder .build ();
95+
96+ return signCredentialInternal (participantContextId , credential , privateKeyAlias , publicKeyId , issuerId , VERIFIABLE_CREDENTIAL , definition .getCredentialType ())
97+ .map (token -> new VerifiableCredentialContainer (token , CredentialFormat .VC2_0_JOSE , credential ));
98+ }
99+
100+ @ Override
101+ public Result <String > signCredential (String participantContextId , VerifiableCredential credential , String privateKeyAlias ,
102+ String publicKeyId ) {
103+
104+ var issuerId = credential .getIssuer ().id ();
105+ var type = credential .getType ().toArray (new String [0 ]);
106+
107+ return signCredentialInternal (participantContextId , credential , privateKeyAlias , publicKeyId , issuerId , type );
108+ }
109+
110+ private Result <String > signCredentialInternal (String participantContextId , VerifiableCredential credential , String privateKeyAlias , String publicKeyId , String issuerId , String ... types ) {
111+ var composedKeyId = publicKeyId .startsWith (issuerId )
112+ ? publicKeyId
113+ : issuerId + "#" + publicKeyId ;
114+
115+ TokenDecorator decorator = tokenBuilder -> {
116+ var vcClaim = createVcClaim (credential , types );
117+ vcClaim .forEach (tokenBuilder ::claims );
118+
119+ if (credential .getExpirationDate () != null ) {
120+ tokenBuilder .claims (EXPIRATION_TIME , Date .from (credential .getExpirationDate ()));
121+ }
122+
123+ return tokenBuilder ;
124+ };
125+
126+ return tokenGenerationService
127+ .generate (participantContextId , privateKeyAlias , decorator , new KeyIdDecorator (composedKeyId ))
128+ .map (TokenRepresentation ::getToken );
129+ }
130+
131+ private Map <String , Object > createVcClaim (VerifiableCredential credential , String ... types ) {
132+
133+ var claims = new HashMap <>(
134+ Map .of (JsonLdKeywords .CONTEXT , List .of ("https://www.w3.org/ns/credentials/v2" ),
135+ TYPE , Arrays .asList (types ),
136+ ID , credential .getId (),
137+ VALID_FROM , credential .getIssuanceDate ().toString (),
138+ ISSUER , credential .getIssuer ().id (),
139+ CREDENTIAL_SUBJECT , credentialSubjectClaims (credential )
140+ ));
141+ if (credential .getExpirationDate () != null ) {
142+ claims .put ("validUntil" , credential .getExpirationDate ().toString ());
143+ }
144+ if (credential .getDescription () != null ) {
145+ claims .put ("description" , credential .getDescription ());
146+ }
147+ if (credential .getName () != null ) {
148+ claims .put ("name" , credential .getName ());
149+ }
150+
151+ var status = credentialStatusClaims (credential );
152+ if (!status .isEmpty ()) {
153+ claims .put (CREDENTIAL_STATUS , status );
154+ }
155+ return claims ;
156+ }
157+
158+ private List <Map <String , Object >> credentialSubjectClaims (VerifiableCredential verifiableCredential ) {
159+ return verifiableCredential .getCredentialSubject ().stream ().map (CredentialSubject ::getClaims ).toList ();
160+ }
161+
162+ @ SuppressWarnings ("unchecked" )
163+ private Result <Map <String , Object >> extractCredentialSubject (Map <String , Object > claims ) {
164+ if (!claims .containsKey (CREDENTIAL_SUBJECT )) {
165+ return Result .failure ("Missing credentialSubject in claims" );
166+ }
167+ return Result .success ((Map <String , Object >) claims .get (CREDENTIAL_SUBJECT ));
168+ }
169+
170+ private Map <String , Object > credentialStatusClaims (VerifiableCredential verifiableCredential ) {
171+ if (verifiableCredential .getCredentialStatus ().isEmpty ()) {
172+ return Map .of ();
173+ }
174+ var status = verifiableCredential .getCredentialStatus ().get (0 );
175+ var statusMap = new HashMap <String , Object >(Map .of (ID , status .id (),
176+ "type" , status .type ()));
177+ statusMap .putAll (status .additionalProperties ());
178+ return statusMap ;
179+ }
180+
181+ @ SuppressWarnings ("unchecked" )
182+ private Result <CredentialStatus > createCredentialStatus (Map <String , Object > claims ) {
183+ if (!claims .containsKey (CREDENTIAL_STATUS )) {
184+ return Result .failure ("no credentialStatus in claims" );
185+ }
186+ var statusClaims = (Map <String , Object >) claims .get (CREDENTIAL_STATUS );
187+
188+ return Result .success (new CredentialStatus ((String ) statusClaims .get (ID ),
189+ (String ) statusClaims .get ("type" ),
190+ statusClaims ));
191+ }
192+ }
0 commit comments