@sphereon/ssi-sdk.oid4vci-holder 0.33.1-feature.vcdm2.4 → 0.33.1-feature.vcdm2.tsup.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +25401 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +786 -0
- package/dist/index.d.ts +785 -11
- package/dist/index.js +25355 -11
- package/dist/index.js.map +1 -1
- package/package.json +44 -34
- package/src/agent/OID4VCIHolder.ts +1 -1
- package/dist/agent/OID4VCIHolder.d.ts +0 -59
- package/dist/agent/OID4VCIHolder.d.ts.map +0 -1
- package/dist/agent/OID4VCIHolder.js +0 -882
- package/dist/agent/OID4VCIHolder.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/link-handler/index.d.ts +0 -31
- package/dist/link-handler/index.d.ts.map +0 -1
- package/dist/link-handler/index.js +0 -71
- package/dist/link-handler/index.js.map +0 -1
- package/dist/listeners/headlessStateNavListener.d.ts +0 -3
- package/dist/listeners/headlessStateNavListener.d.ts.map +0 -1
- package/dist/listeners/headlessStateNavListener.js +0 -32
- package/dist/listeners/headlessStateNavListener.js.map +0 -1
- package/dist/localization/Localization.d.ts +0 -9
- package/dist/localization/Localization.d.ts.map +0 -1
- package/dist/localization/Localization.js +0 -41
- package/dist/localization/Localization.js.map +0 -1
- package/dist/localization/translations/en.json +0 -19
- package/dist/localization/translations/nl.json +0 -18
- package/dist/machines/firstPartyMachine.d.ts +0 -15
- package/dist/machines/firstPartyMachine.d.ts.map +0 -1
- package/dist/machines/firstPartyMachine.js +0 -223
- package/dist/machines/firstPartyMachine.js.map +0 -1
- package/dist/machines/oid4vciMachine.d.ts +0 -7
- package/dist/machines/oid4vciMachine.d.ts.map +0 -1
- package/dist/machines/oid4vciMachine.js +0 -708
- package/dist/machines/oid4vciMachine.js.map +0 -1
- package/dist/mappers/OIDC4VCIBrandingMapper.d.ts +0 -16
- package/dist/mappers/OIDC4VCIBrandingMapper.d.ts.map +0 -1
- package/dist/mappers/OIDC4VCIBrandingMapper.js +0 -229
- package/dist/mappers/OIDC4VCIBrandingMapper.js.map +0 -1
- package/dist/services/FirstPartyMachineServices.d.ts +0 -9
- package/dist/services/FirstPartyMachineServices.d.ts.map +0 -1
- package/dist/services/FirstPartyMachineServices.js +0 -42
- package/dist/services/FirstPartyMachineServices.js.map +0 -1
- package/dist/services/OID4VCIHolderService.d.ts +0 -28
- package/dist/services/OID4VCIHolderService.d.ts.map +0 -1
- package/dist/services/OID4VCIHolderService.js +0 -516
- package/dist/services/OID4VCIHolderService.js.map +0 -1
- package/dist/types/FirstPartyMachine.d.ts +0 -112
- package/dist/types/FirstPartyMachine.d.ts.map +0 -1
- package/dist/types/FirstPartyMachine.js +0 -27
- package/dist/types/FirstPartyMachine.js.map +0 -1
- package/dist/types/IOID4VCIHolder.d.ts +0 -558
- package/dist/types/IOID4VCIHolder.d.ts.map +0 -1
- package/dist/types/IOID4VCIHolder.js +0 -111
- package/dist/types/IOID4VCIHolder.js.map +0 -1
|
@@ -1,882 +0,0 @@
|
|
|
1
|
-
import { CredentialOfferClient, MetadataClient, OpenID4VCIClient } from '@sphereon/oid4vci-client';
|
|
2
|
-
import { DefaultURISchemes, getTypesFromAuthorizationDetails, getTypesFromCredentialOffer, getTypesFromObject, } from '@sphereon/oid4vci-common';
|
|
3
|
-
import { SupportedDidMethodEnum } from '@sphereon/ssi-sdk-ext.did-utils';
|
|
4
|
-
import { isManagedIdentifierDidOpts, isManagedIdentifierDidResult, isManagedIdentifierJwkResult, isManagedIdentifierKidResult, isManagedIdentifierResult, isManagedIdentifierX5cOpts, isManagedIdentifierX5cResult, } from '@sphereon/ssi-sdk-ext.identifier-resolution';
|
|
5
|
-
import { signatureAlgorithmFromKey } from '@sphereon/ssi-sdk-ext.key-utils';
|
|
6
|
-
import { ConnectionType, CorrelationIdentifierType, CredentialCorrelationType, CredentialRole, ensureRawDocument, IdentityOrigin, } from '@sphereon/ssi-sdk.data-store';
|
|
7
|
-
import { CredentialMapper, JoseSignatureAlgorithm, Loggers, parseDid, } from '@sphereon/ssi-types';
|
|
8
|
-
import { asArray, computeEntryHash } from '@veramo/utils';
|
|
9
|
-
import { decodeJWT } from 'did-jwt';
|
|
10
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
11
|
-
import { OID4VCIMachine } from '../machines/oid4vciMachine';
|
|
12
|
-
import { OID4VCIHolderEvent, OID4VCIMachineServices, RequestType, } from '../types/IOID4VCIHolder';
|
|
13
|
-
import { getBasicIssuerLocaleBranding, getCredentialBranding, getCredentialConfigsSupportedMerged, getIdentifierOpts, getIssuanceOpts, mapCredentialToAccept, selectCredentialLocaleBranding, startFirstPartApplicationMachine, verifyCredentialToAccept, } from '../services/OID4VCIHolderService';
|
|
14
|
-
import 'cross-fetch/polyfill';
|
|
15
|
-
import { defaultHasher } from '@sphereon/ssi-sdk.core';
|
|
16
|
-
/**
|
|
17
|
-
* {@inheritDoc IOID4VCIHolder}
|
|
18
|
-
*/
|
|
19
|
-
// Exposing the methods here for any REST implementation
|
|
20
|
-
export const oid4vciHolderContextMethods = [
|
|
21
|
-
'cmGetContacts',
|
|
22
|
-
'cmGetContact',
|
|
23
|
-
'cmAddContact',
|
|
24
|
-
'cmAddIdentity',
|
|
25
|
-
'ibCredentialLocaleBrandingFrom',
|
|
26
|
-
'ibAddCredentialBranding',
|
|
27
|
-
'dataStoreSaveVerifiableCredential',
|
|
28
|
-
'didManagerFind',
|
|
29
|
-
'didManagerGet',
|
|
30
|
-
'keyManagerSign',
|
|
31
|
-
'verifyCredential',
|
|
32
|
-
];
|
|
33
|
-
const logger = Loggers.DEFAULT.get('sphereon:oid4vci:holder');
|
|
34
|
-
export function signCallback(identifier, context, nonce) {
|
|
35
|
-
return async (jwt, kid) => {
|
|
36
|
-
let resolution = await context.agent.identifierManagedGet(identifier);
|
|
37
|
-
const jwk = jwt.header.jwk ?? (resolution.method === 'jwk' ? resolution.jwk : undefined);
|
|
38
|
-
if (!resolution.issuer && !jwt.payload.iss) {
|
|
39
|
-
return Promise.reject(Error(`No issuer could be determined from the JWT ${JSON.stringify(jwt)} or identifier resolution`));
|
|
40
|
-
}
|
|
41
|
-
const header = jwt.header;
|
|
42
|
-
const payload = jwt.payload;
|
|
43
|
-
if (nonce) {
|
|
44
|
-
payload.nonce = nonce;
|
|
45
|
-
}
|
|
46
|
-
if (jwk && header.kid) {
|
|
47
|
-
console.log(`Deleting kid, as we are using a jwk and the oid4vci spec does not allow both to be present (which is not the case in the JOSE spec)`);
|
|
48
|
-
delete header.kid; // The OID4VCI spec does not allow a JWK with kid present although the JWS spec does
|
|
49
|
-
}
|
|
50
|
-
return (await context.agent.jwtCreateJwsCompactSignature({
|
|
51
|
-
issuer: { ...resolution, noIssPayloadUpdate: false },
|
|
52
|
-
protectedHeader: header,
|
|
53
|
-
payload,
|
|
54
|
-
})).jwt;
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
export async function verifyEBSICredentialIssuer(args) {
|
|
58
|
-
const { wrappedVc, issuerType = ['TI'] } = args;
|
|
59
|
-
const issuer = wrappedVc.decoded?.iss ??
|
|
60
|
-
(typeof wrappedVc.decoded?.vc?.issuer === 'string' ? wrappedVc.decoded?.vc?.issuer : wrappedVc.decoded?.vc?.issuer?.existingInstanceId);
|
|
61
|
-
if (!issuer) {
|
|
62
|
-
throw Error('The issuer of the VC is required to be present');
|
|
63
|
-
}
|
|
64
|
-
const url = `https://api-conformance.ebsi.eu/trusted-issuers-registry/v4/issuers/${issuer}`;
|
|
65
|
-
const response = await fetch(url);
|
|
66
|
-
if (response.status !== 200) {
|
|
67
|
-
throw Error('The issuer of the VC cannot be trusted');
|
|
68
|
-
}
|
|
69
|
-
const payload = await response.json();
|
|
70
|
-
if (!payload.attributes.some((a) => issuerType.includes(a.issuerType))) {
|
|
71
|
-
throw Error(`The issuer type is required to be one of: ${issuerType.join(', ')}`);
|
|
72
|
-
}
|
|
73
|
-
return payload;
|
|
74
|
-
}
|
|
75
|
-
export class OID4VCIHolder {
|
|
76
|
-
hasher;
|
|
77
|
-
eventTypes = [
|
|
78
|
-
OID4VCIHolderEvent.CONTACT_IDENTITY_CREATED,
|
|
79
|
-
OID4VCIHolderEvent.CREDENTIAL_STORED,
|
|
80
|
-
OID4VCIHolderEvent.IDENTIFIER_CREATED,
|
|
81
|
-
];
|
|
82
|
-
methods = {
|
|
83
|
-
oid4vciHolderStart: this.oid4vciHolderStart.bind(this),
|
|
84
|
-
oid4vciHolderGetIssuerMetadata: this.oid4vciHolderGetIssuerMetadata.bind(this),
|
|
85
|
-
oid4vciHolderGetMachineInterpreter: this.oid4vciHolderGetMachineInterpreter.bind(this),
|
|
86
|
-
oid4vciHolderCreateCredentialsToSelectFrom: this.oid4vciHolderCreateCredentialsToSelectFrom.bind(this),
|
|
87
|
-
oid4vciHolderGetContact: this.oid4vciHolderGetContact.bind(this),
|
|
88
|
-
oid4vciHolderGetCredentials: this.oid4vciHolderGetCredentials.bind(this),
|
|
89
|
-
oid4vciHolderGetCredential: this.oid4vciHolderGetCredential.bind(this),
|
|
90
|
-
oid4vciHolderAddContactIdentity: this.oid4vciHolderAddContactIdentity.bind(this),
|
|
91
|
-
oid4vciHolderAssertValidCredentials: this.oid4vciHolderAssertValidCredentials.bind(this),
|
|
92
|
-
oid4vciHolderStoreCredentialBranding: this.oid4vciHolderStoreCredentialBranding.bind(this),
|
|
93
|
-
oid4vciHolderStoreCredentials: this.oid4vciHolderStoreCredentials.bind(this),
|
|
94
|
-
oid4vciHolderSendNotification: this.oid4vciHolderSendNotification.bind(this),
|
|
95
|
-
oid4vciHolderGetIssuerBranding: this.oid4vciHolderGetIssuerBranding.bind(this),
|
|
96
|
-
oid4vciHolderStoreIssuerBranding: this.oid4vciHolderStoreIssuerBranding.bind(this),
|
|
97
|
-
};
|
|
98
|
-
vcFormatPreferences = ['vc+sd-jwt', 'mso_mdoc', 'jwt_vc_json', 'jwt_vc', 'ldp_vc'];
|
|
99
|
-
jsonldCryptographicSuitePreferences = [
|
|
100
|
-
'Ed25519Signature2018',
|
|
101
|
-
'EcdsaSecp256k1Signature2019',
|
|
102
|
-
'Ed25519Signature2020',
|
|
103
|
-
'JsonWebSignature2020',
|
|
104
|
-
// "JcsEd25519Signature2020"
|
|
105
|
-
];
|
|
106
|
-
didMethodPreferences = [
|
|
107
|
-
SupportedDidMethodEnum.DID_JWK, // FIXME prefer JWK until we devise a method to detect when to use EBSI/jcs for did:key and when not
|
|
108
|
-
SupportedDidMethodEnum.DID_KEY,
|
|
109
|
-
SupportedDidMethodEnum.DID_OYD,
|
|
110
|
-
SupportedDidMethodEnum.DID_EBSI,
|
|
111
|
-
SupportedDidMethodEnum.DID_ION,
|
|
112
|
-
];
|
|
113
|
-
jwtCryptographicSuitePreferences = [
|
|
114
|
-
JoseSignatureAlgorithm.ES256,
|
|
115
|
-
JoseSignatureAlgorithm.ES256K,
|
|
116
|
-
JoseSignatureAlgorithm.EdDSA,
|
|
117
|
-
];
|
|
118
|
-
static DEFAULT_MOBILE_REDIRECT_URI = `${DefaultURISchemes.CREDENTIAL_OFFER}://`;
|
|
119
|
-
defaultAuthorizationRequestOpts = { redirectUri: OID4VCIHolder.DEFAULT_MOBILE_REDIRECT_URI };
|
|
120
|
-
onContactIdentityCreated;
|
|
121
|
-
onCredentialStored;
|
|
122
|
-
onIdentifierCreated;
|
|
123
|
-
onVerifyEBSICredentialIssuer;
|
|
124
|
-
constructor(options) {
|
|
125
|
-
const { onContactIdentityCreated, onCredentialStored, onIdentifierCreated, onVerifyEBSICredentialIssuer, vcFormatPreferences, jsonldCryptographicSuitePreferences, didMethodPreferences, jwtCryptographicSuitePreferences, defaultAuthorizationRequestOptions, hasher = defaultHasher, } = { ...options };
|
|
126
|
-
this.hasher = hasher;
|
|
127
|
-
if (vcFormatPreferences !== undefined && vcFormatPreferences.length > 0) {
|
|
128
|
-
this.vcFormatPreferences = vcFormatPreferences;
|
|
129
|
-
}
|
|
130
|
-
if (jsonldCryptographicSuitePreferences !== undefined && jsonldCryptographicSuitePreferences.length > 0) {
|
|
131
|
-
this.jsonldCryptographicSuitePreferences = jsonldCryptographicSuitePreferences;
|
|
132
|
-
}
|
|
133
|
-
if (didMethodPreferences !== undefined && didMethodPreferences.length > 0) {
|
|
134
|
-
this.didMethodPreferences = didMethodPreferences;
|
|
135
|
-
}
|
|
136
|
-
if (jwtCryptographicSuitePreferences !== undefined && jwtCryptographicSuitePreferences.length > 0) {
|
|
137
|
-
this.jwtCryptographicSuitePreferences = jwtCryptographicSuitePreferences;
|
|
138
|
-
}
|
|
139
|
-
if (defaultAuthorizationRequestOptions) {
|
|
140
|
-
this.defaultAuthorizationRequestOpts = defaultAuthorizationRequestOptions;
|
|
141
|
-
}
|
|
142
|
-
this.onContactIdentityCreated = onContactIdentityCreated;
|
|
143
|
-
this.onCredentialStored = onCredentialStored;
|
|
144
|
-
this.onIdentifierCreated = onIdentifierCreated;
|
|
145
|
-
this.onVerifyEBSICredentialIssuer = onVerifyEBSICredentialIssuer;
|
|
146
|
-
}
|
|
147
|
-
async onEvent(event, context) {
|
|
148
|
-
switch (event.type) {
|
|
149
|
-
case OID4VCIHolderEvent.CONTACT_IDENTITY_CREATED:
|
|
150
|
-
this.onContactIdentityCreated?.(event.data);
|
|
151
|
-
break;
|
|
152
|
-
case OID4VCIHolderEvent.CREDENTIAL_STORED:
|
|
153
|
-
this.onCredentialStored?.(event.data);
|
|
154
|
-
break;
|
|
155
|
-
case OID4VCIHolderEvent.IDENTIFIER_CREATED:
|
|
156
|
-
this.onIdentifierCreated?.(event.data);
|
|
157
|
-
break;
|
|
158
|
-
default:
|
|
159
|
-
return Promise.reject(Error(`Event type ${event.type} not supported`));
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* FIXME: This method can only be used locally. Creating the interpreter should be local to where the agent is running
|
|
164
|
-
*/
|
|
165
|
-
async oid4vciHolderGetMachineInterpreter(opts, context) {
|
|
166
|
-
const authorizationRequestOpts = { ...this.defaultAuthorizationRequestOpts, ...opts.authorizationRequestOpts };
|
|
167
|
-
const services = {
|
|
168
|
-
[OID4VCIMachineServices.start]: (args) => this.oid4vciHolderStart({
|
|
169
|
-
...args,
|
|
170
|
-
authorizationRequestOpts,
|
|
171
|
-
}, context),
|
|
172
|
-
[OID4VCIMachineServices.startFirstPartApplicationFlow]: (args) => startFirstPartApplicationMachine({ ...args, stateNavigationListener: opts.firstPartyStateNavigationListener }, context),
|
|
173
|
-
[OID4VCIMachineServices.createCredentialsToSelectFrom]: (args) => this.oid4vciHolderCreateCredentialsToSelectFrom(args, context),
|
|
174
|
-
[OID4VCIMachineServices.getContact]: (args) => this.oid4vciHolderGetContact(args, context),
|
|
175
|
-
[OID4VCIMachineServices.getCredentials]: (args) => this.oid4vciHolderGetCredentials({ accessTokenOpts: args.accessTokenOpts ?? opts.accessTokenOpts, ...args }, context),
|
|
176
|
-
[OID4VCIMachineServices.addContactIdentity]: (args) => this.oid4vciHolderAddContactIdentity(args, context),
|
|
177
|
-
[OID4VCIMachineServices.getIssuerBranding]: (args) => this.oid4vciHolderGetIssuerBranding(args, context),
|
|
178
|
-
[OID4VCIMachineServices.storeIssuerBranding]: (args) => this.oid4vciHolderStoreIssuerBranding(args, context),
|
|
179
|
-
[OID4VCIMachineServices.assertValidCredentials]: (args) => this.oid4vciHolderAssertValidCredentials(args, context),
|
|
180
|
-
[OID4VCIMachineServices.storeCredentialBranding]: (args) => this.oid4vciHolderStoreCredentialBranding(args, context),
|
|
181
|
-
[OID4VCIMachineServices.storeCredentials]: (args) => this.oid4vciHolderStoreCredentials(args, context),
|
|
182
|
-
[OID4VCIMachineServices.sendNotification]: (args) => this.oid4vciHolderSendNotification(args, context),
|
|
183
|
-
[OID4VCIMachineServices.getFederationTrust]: (args) => this.getFederationTrust(args, context),
|
|
184
|
-
};
|
|
185
|
-
const oid4vciMachineInstanceArgs = {
|
|
186
|
-
...opts,
|
|
187
|
-
authorizationRequestOpts,
|
|
188
|
-
services: {
|
|
189
|
-
...services,
|
|
190
|
-
...opts.services,
|
|
191
|
-
},
|
|
192
|
-
};
|
|
193
|
-
const { interpreter } = await OID4VCIMachine.newInstance(oid4vciMachineInstanceArgs, context);
|
|
194
|
-
return {
|
|
195
|
-
interpreter,
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* This method is run before the machine starts! So there is no concept of the state machine context or states yet
|
|
200
|
-
*
|
|
201
|
-
* The result of this method can be directly passed into the start method of the state machine
|
|
202
|
-
* @param args
|
|
203
|
-
* @param context
|
|
204
|
-
* @private
|
|
205
|
-
*/
|
|
206
|
-
async oid4vciHolderStart(args, context) {
|
|
207
|
-
const { requestData } = args;
|
|
208
|
-
if (!requestData) {
|
|
209
|
-
throw Error(`Cannot start the OID4VCI holder flow without request data being provided`);
|
|
210
|
-
}
|
|
211
|
-
const { uri = undefined } = requestData;
|
|
212
|
-
if (!uri) {
|
|
213
|
-
return Promise.reject(Error('Missing request URI in context'));
|
|
214
|
-
}
|
|
215
|
-
const authorizationRequestOpts = { ...this.defaultAuthorizationRequestOpts, ...args.authorizationRequestOpts };
|
|
216
|
-
// We filter the details first against our vcformat prefs
|
|
217
|
-
authorizationRequestOpts.authorizationDetails = authorizationRequestOpts?.authorizationDetails
|
|
218
|
-
? asArray(authorizationRequestOpts.authorizationDetails).filter((detail) => typeof detail === 'string' || this.vcFormatPreferences.includes(detail.format))
|
|
219
|
-
: undefined;
|
|
220
|
-
if (!authorizationRequestOpts.redirectUri) {
|
|
221
|
-
authorizationRequestOpts.redirectUri = OID4VCIHolder.DEFAULT_MOBILE_REDIRECT_URI;
|
|
222
|
-
}
|
|
223
|
-
if (authorizationRequestOpts.redirectUri.startsWith('http') && !authorizationRequestOpts.clientId) {
|
|
224
|
-
// At least set a default for a web based wallet.
|
|
225
|
-
// TODO: We really need (dynamic) client registration support
|
|
226
|
-
authorizationRequestOpts.clientId = authorizationRequestOpts.redirectUri;
|
|
227
|
-
}
|
|
228
|
-
let formats = this.vcFormatPreferences;
|
|
229
|
-
const authFormats = authorizationRequestOpts?.authorizationDetails
|
|
230
|
-
?.map((detail) => (typeof detail === 'object' && 'format' in detail && detail.format ? detail.format : undefined))
|
|
231
|
-
.filter((format) => !!format)
|
|
232
|
-
.map((format) => format);
|
|
233
|
-
if (authFormats && authFormats.length > 0) {
|
|
234
|
-
formats = Array.from(new Set(authFormats));
|
|
235
|
-
}
|
|
236
|
-
let oid4vciClient;
|
|
237
|
-
let types = undefined;
|
|
238
|
-
let offer;
|
|
239
|
-
if (requestData.existingClientState) {
|
|
240
|
-
oid4vciClient = await OpenID4VCIClient.fromState({ state: requestData.existingClientState });
|
|
241
|
-
offer = oid4vciClient.credentialOffer;
|
|
242
|
-
}
|
|
243
|
-
else {
|
|
244
|
-
offer = requestData.credentialOffer;
|
|
245
|
-
if (uri.startsWith(RequestType.OPENID_INITIATE_ISSUANCE) ||
|
|
246
|
-
uri.startsWith(RequestType.OPENID_CREDENTIAL_OFFER) ||
|
|
247
|
-
uri.match(/https?:\/\/.*credential_offer(_uri)=?.*/)) {
|
|
248
|
-
if (!offer) {
|
|
249
|
-
// Let's make sure to convert the URI to offer, as it matches the regexes. Normally this should already have happened at this point though
|
|
250
|
-
offer = await CredentialOfferClient.fromURI(uri);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
else {
|
|
254
|
-
if (!!offer) {
|
|
255
|
-
logger.warning(`Non default URI used for credential offer: ${uri}`);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
if (!offer) {
|
|
259
|
-
// else no offer, meaning we have an issuer URL
|
|
260
|
-
logger.log(`Issuer url received (no credential offer): ${uri}`);
|
|
261
|
-
oid4vciClient = await OpenID4VCIClient.fromCredentialIssuer({
|
|
262
|
-
credentialIssuer: uri,
|
|
263
|
-
authorizationRequest: authorizationRequestOpts,
|
|
264
|
-
clientId: authorizationRequestOpts.clientId,
|
|
265
|
-
createAuthorizationRequestURL: requestData.createAuthorizationRequestURL ?? true,
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
else {
|
|
269
|
-
logger.log(`Credential offer received: ${uri}`);
|
|
270
|
-
oid4vciClient = await OpenID4VCIClient.fromURI({
|
|
271
|
-
uri,
|
|
272
|
-
authorizationRequest: authorizationRequestOpts,
|
|
273
|
-
clientId: authorizationRequestOpts.clientId,
|
|
274
|
-
createAuthorizationRequestURL: requestData.createAuthorizationRequestURL ?? true,
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
if (offer) {
|
|
279
|
-
types = getTypesFromCredentialOffer(offer.original_credential_offer);
|
|
280
|
-
}
|
|
281
|
-
else {
|
|
282
|
-
types = asArray(authorizationRequestOpts.authorizationDetails)
|
|
283
|
-
.map((authReqOpts) => getTypesFromAuthorizationDetails(authReqOpts) ?? [])
|
|
284
|
-
.filter((inner) => inner.length > 0);
|
|
285
|
-
}
|
|
286
|
-
const serverMetadata = await oid4vciClient.retrieveServerMetadata();
|
|
287
|
-
const credentialsSupported = await getCredentialConfigsSupportedMerged({
|
|
288
|
-
client: oid4vciClient,
|
|
289
|
-
vcFormatPreferences: formats,
|
|
290
|
-
types,
|
|
291
|
-
});
|
|
292
|
-
const credentialBranding = await getCredentialBranding({ credentialsSupported, context });
|
|
293
|
-
const authorizationCodeURL = oid4vciClient.authorizationURL;
|
|
294
|
-
if (authorizationCodeURL) {
|
|
295
|
-
logger.log(`authorization code URL ${authorizationCodeURL}`);
|
|
296
|
-
}
|
|
297
|
-
const oid4vciClientState = JSON.parse(await oid4vciClient.exportState());
|
|
298
|
-
return {
|
|
299
|
-
authorizationCodeURL,
|
|
300
|
-
credentialBranding,
|
|
301
|
-
credentialsSupported,
|
|
302
|
-
serverMetadata,
|
|
303
|
-
oid4vciClientState,
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
async oid4vciHolderCreateCredentialsToSelectFrom(args, context) {
|
|
307
|
-
const { credentialBranding, locale, selectedCredentials /*, openID4VCIClientState*/, credentialsSupported } = args;
|
|
308
|
-
// const client = await OpenID4VCIClient.fromState({ state: openID4VCIClientState! }) // TODO see if we need the check openID4VCIClientState defined
|
|
309
|
-
/*const credentialsSupported = await getCredentialConfigsSupportedBySingleTypeOrId({
|
|
310
|
-
client,
|
|
311
|
-
vcFormatPreferences: this.vcFormatPreferences,
|
|
312
|
-
})*/
|
|
313
|
-
logger.info(`Credentials supported ${Object.keys(credentialsSupported).join(', ')}`);
|
|
314
|
-
const credentialSelection = await Promise.all(Object.entries(credentialsSupported).map(async ([id, credentialConfigSupported]) => {
|
|
315
|
-
// FIXME this allows for duplicate VerifiableCredential, which the user has no idea which ones those are and we also have a branding map with unique keys, so some branding will not match
|
|
316
|
-
// const defaultCredentialType = 'VerifiableCredential'
|
|
317
|
-
const credentialTypes = getTypesFromObject(credentialConfigSupported);
|
|
318
|
-
// const credentialType = id /*?? credentialTypes?.find((type) => type !== defaultCredentialType) ?? defaultCredentialType*/
|
|
319
|
-
const localeBranding = !credentialBranding
|
|
320
|
-
? undefined
|
|
321
|
-
: (credentialBranding?.[id] ??
|
|
322
|
-
Object.entries(credentialBranding)
|
|
323
|
-
.find(([type, _brandings]) => {
|
|
324
|
-
credentialTypes && type in credentialTypes;
|
|
325
|
-
})
|
|
326
|
-
?.map(([type, supported]) => supported));
|
|
327
|
-
const credentialAlias = (await selectCredentialLocaleBranding({
|
|
328
|
-
locale,
|
|
329
|
-
localeBranding,
|
|
330
|
-
}))?.alias;
|
|
331
|
-
return {
|
|
332
|
-
id: uuidv4(),
|
|
333
|
-
credentialId: id,
|
|
334
|
-
credentialTypes: credentialTypes ?? asArray(id),
|
|
335
|
-
credentialAlias: credentialAlias ?? id,
|
|
336
|
-
isSelected: false,
|
|
337
|
-
};
|
|
338
|
-
}));
|
|
339
|
-
// TODO find better place to do this, would be nice if the machine does this?
|
|
340
|
-
if (credentialSelection.length >= 1) {
|
|
341
|
-
credentialSelection.map((sel) => selectedCredentials.push(sel.credentialId));
|
|
342
|
-
}
|
|
343
|
-
logger.log(`Credential selection ${JSON.stringify(credentialSelection)}`);
|
|
344
|
-
return credentialSelection;
|
|
345
|
-
}
|
|
346
|
-
async oid4vciHolderGetContact(args, context) {
|
|
347
|
-
const { serverMetadata } = args;
|
|
348
|
-
if (serverMetadata === undefined) {
|
|
349
|
-
return Promise.reject(Error('Missing serverMetadata in context'));
|
|
350
|
-
}
|
|
351
|
-
const names = new Set(serverMetadata.credentialIssuerMetadata?.display
|
|
352
|
-
?.map((display) => display.name)
|
|
353
|
-
.filter((name) => name != undefined)
|
|
354
|
-
.map((name) => name) ?? []);
|
|
355
|
-
const name = names.size > 0 ? Array.from(names)[0] : undefined;
|
|
356
|
-
const correlationId = new URL(serverMetadata.issuer).hostname;
|
|
357
|
-
const filter = [
|
|
358
|
-
{
|
|
359
|
-
identities: {
|
|
360
|
-
identifier: {
|
|
361
|
-
correlationId,
|
|
362
|
-
},
|
|
363
|
-
},
|
|
364
|
-
},
|
|
365
|
-
];
|
|
366
|
-
if (name) {
|
|
367
|
-
filter.push({
|
|
368
|
-
contact: {
|
|
369
|
-
legalName: name,
|
|
370
|
-
},
|
|
371
|
-
});
|
|
372
|
-
filter.push({
|
|
373
|
-
contact: {
|
|
374
|
-
displayName: name,
|
|
375
|
-
},
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
const parties = await context.agent.cmGetContacts({
|
|
379
|
-
filter,
|
|
380
|
-
});
|
|
381
|
-
if (parties.length > 1) {
|
|
382
|
-
logger.warning(`Get contacts returned more than one result: ${parties.length}, ${parties.map((party) => party.contact.displayName).join(',')}`);
|
|
383
|
-
}
|
|
384
|
-
const party = parties.length >= 1 ? parties[0] : undefined;
|
|
385
|
-
logger.log(`Party involved: `, party);
|
|
386
|
-
return party;
|
|
387
|
-
}
|
|
388
|
-
async oid4vciHolderGetCredentials(args, context) {
|
|
389
|
-
const { verificationCode, openID4VCIClientState, didMethodPreferences = this.didMethodPreferences, issuanceOpt, accessTokenOpts } = args;
|
|
390
|
-
logger.debug(`Getting credentials`, issuanceOpt, accessTokenOpts);
|
|
391
|
-
if (!openID4VCIClientState) {
|
|
392
|
-
return Promise.reject(Error('Missing openID4VCI client state in context'));
|
|
393
|
-
}
|
|
394
|
-
const client = await OpenID4VCIClient.fromState({ state: openID4VCIClientState });
|
|
395
|
-
const credentialsSupported = await getCredentialConfigsSupportedMerged({
|
|
396
|
-
client,
|
|
397
|
-
vcFormatPreferences: this.vcFormatPreferences,
|
|
398
|
-
configurationIds: args.selectedCredentials,
|
|
399
|
-
});
|
|
400
|
-
const serverMetadata = await client.retrieveServerMetadata();
|
|
401
|
-
const issuanceOpts = await getIssuanceOpts({
|
|
402
|
-
client,
|
|
403
|
-
credentialsSupported,
|
|
404
|
-
serverMetadata,
|
|
405
|
-
context,
|
|
406
|
-
didMethodPreferences: Array.isArray(didMethodPreferences) && didMethodPreferences.length > 0 ? didMethodPreferences : this.didMethodPreferences,
|
|
407
|
-
jwtCryptographicSuitePreferences: this.jwtCryptographicSuitePreferences,
|
|
408
|
-
jsonldCryptographicSuitePreferences: this.jsonldCryptographicSuitePreferences,
|
|
409
|
-
...(issuanceOpt && { forceIssuanceOpt: issuanceOpt }),
|
|
410
|
-
});
|
|
411
|
-
const getCredentials = issuanceOpts.map(async (issuanceOpt) => await this.oid4vciHolderGetCredential({
|
|
412
|
-
issuanceOpt,
|
|
413
|
-
pin: verificationCode,
|
|
414
|
-
client,
|
|
415
|
-
accessTokenOpts,
|
|
416
|
-
}, context));
|
|
417
|
-
const allCredentials = await Promise.all(getCredentials);
|
|
418
|
-
logger.log(`Credentials received`, allCredentials);
|
|
419
|
-
return allCredentials;
|
|
420
|
-
}
|
|
421
|
-
async oid4vciHolderGetCredential(args, context) {
|
|
422
|
-
const { issuanceOpt, pin, client, accessTokenOpts } = args;
|
|
423
|
-
logger.info(`Getting credential`, issuanceOpt);
|
|
424
|
-
if (!issuanceOpt) {
|
|
425
|
-
return Promise.reject(Error(`Cannot get credential issuance options`));
|
|
426
|
-
}
|
|
427
|
-
const identifier = await getIdentifierOpts({ issuanceOpt, context });
|
|
428
|
-
issuanceOpt.identifier = identifier;
|
|
429
|
-
logger.info(`ID opts`, identifier);
|
|
430
|
-
const alg = await signatureAlgorithmFromKey({ key: identifier.key });
|
|
431
|
-
// The VCI lib either expects a jwk or a kid
|
|
432
|
-
const jwk = isManagedIdentifierJwkResult(identifier) ? identifier.jwk : undefined;
|
|
433
|
-
const callbacks = {
|
|
434
|
-
signCallback: signCallback(identifier, context),
|
|
435
|
-
};
|
|
436
|
-
try {
|
|
437
|
-
// We need to make sure we have acquired the access token
|
|
438
|
-
if (!client.clientId) {
|
|
439
|
-
client.clientId = isManagedIdentifierDidResult(identifier) ? identifier.did : identifier.issuer;
|
|
440
|
-
}
|
|
441
|
-
let asOpts = undefined;
|
|
442
|
-
let kid = accessTokenOpts?.clientOpts?.kid ?? identifier.kid;
|
|
443
|
-
if (accessTokenOpts?.clientOpts) {
|
|
444
|
-
const clientId = accessTokenOpts.clientOpts.clientId ?? client.clientId ?? identifier.issuer;
|
|
445
|
-
if (client.isEBSI() && clientId?.startsWith('http') && kid?.includes('#')) {
|
|
446
|
-
kid = kid.split('#')[1];
|
|
447
|
-
}
|
|
448
|
-
//todo: investigate if the jwk should be used here as well if present
|
|
449
|
-
const clientOpts = {
|
|
450
|
-
...accessTokenOpts.clientOpts,
|
|
451
|
-
clientId,
|
|
452
|
-
kid,
|
|
453
|
-
// @ts-ignore
|
|
454
|
-
alg: accessTokenOpts.clientOpts.alg ?? alg,
|
|
455
|
-
signCallbacks: accessTokenOpts.clientOpts.signCallbacks ?? callbacks,
|
|
456
|
-
};
|
|
457
|
-
asOpts = {
|
|
458
|
-
clientOpts,
|
|
459
|
-
};
|
|
460
|
-
}
|
|
461
|
-
await client.acquireAccessToken({
|
|
462
|
-
clientId: client.clientId,
|
|
463
|
-
pin,
|
|
464
|
-
authorizationResponse: JSON.parse(await client.exportState()).authorizationCodeResponse,
|
|
465
|
-
additionalRequestParams: accessTokenOpts?.additionalRequestParams,
|
|
466
|
-
...(asOpts && { asOpts }),
|
|
467
|
-
});
|
|
468
|
-
// FIXME: This type mapping is wrong. It should use credential_identifier in case the access token response has authorization details
|
|
469
|
-
const types = getTypesFromObject(issuanceOpt);
|
|
470
|
-
const id = 'id' in issuanceOpt && issuanceOpt.id ? issuanceOpt.id : undefined;
|
|
471
|
-
const credentialTypes = asArray(issuanceOpt.credentialConfigurationId ?? types ?? id);
|
|
472
|
-
if (!credentialTypes || credentialTypes.length === 0) {
|
|
473
|
-
return Promise.reject(Error('cannot determine credential id to request'));
|
|
474
|
-
}
|
|
475
|
-
const credentialDefinition = this.getCredentialDefinition(issuanceOpt);
|
|
476
|
-
const credentialResponse = await client.acquireCredentials({
|
|
477
|
-
...(credentialDefinition && { context: credentialDefinition['@context'] }),
|
|
478
|
-
credentialTypes,
|
|
479
|
-
proofCallbacks: callbacks,
|
|
480
|
-
format: issuanceOpt.format,
|
|
481
|
-
// TODO: We need to update the machine and add notifications support for actual deferred credentials instead of just waiting/retrying
|
|
482
|
-
deferredCredentialAwait: true,
|
|
483
|
-
...(!jwk && { kid }), // vci client either wants a jwk or kid. If we have used the jwk method do not provide the kid
|
|
484
|
-
jwk,
|
|
485
|
-
alg,
|
|
486
|
-
jti: uuidv4(),
|
|
487
|
-
});
|
|
488
|
-
const credential = {
|
|
489
|
-
id: issuanceOpt.credentialConfigurationId ?? id,
|
|
490
|
-
types: types ?? asArray(credentialTypes),
|
|
491
|
-
issuanceOpt,
|
|
492
|
-
credentialResponse,
|
|
493
|
-
};
|
|
494
|
-
return mapCredentialToAccept({ credentialToAccept: credential, hasher: this.hasher });
|
|
495
|
-
}
|
|
496
|
-
catch (error) {
|
|
497
|
-
return Promise.reject(error);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
async oid4vciHolderAddContactIdentity(args, context) {
|
|
501
|
-
const { credentialsToAccept, contact } = args;
|
|
502
|
-
if (!contact) {
|
|
503
|
-
return Promise.reject(Error('Missing contact in context'));
|
|
504
|
-
}
|
|
505
|
-
if (credentialsToAccept === undefined || credentialsToAccept.length === 0) {
|
|
506
|
-
return Promise.reject(Error('Missing credential offers in context'));
|
|
507
|
-
}
|
|
508
|
-
let correlationId = credentialsToAccept[0].correlationId;
|
|
509
|
-
let identifierType = CorrelationIdentifierType.DID;
|
|
510
|
-
if (!correlationId.toLowerCase().startsWith('did:')) {
|
|
511
|
-
identifierType = CorrelationIdentifierType.URL;
|
|
512
|
-
if (correlationId.startsWith('http')) {
|
|
513
|
-
correlationId = new URL(correlationId).hostname;
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
const identity = {
|
|
517
|
-
alias: credentialsToAccept[0].correlationId,
|
|
518
|
-
origin: IdentityOrigin.EXTERNAL,
|
|
519
|
-
roles: [CredentialRole.ISSUER],
|
|
520
|
-
identifier: {
|
|
521
|
-
type: identifierType,
|
|
522
|
-
correlationId,
|
|
523
|
-
},
|
|
524
|
-
...(identifierType === CorrelationIdentifierType.URL && {
|
|
525
|
-
connection: {
|
|
526
|
-
type: ConnectionType.OPENID_CONNECT,
|
|
527
|
-
config: {
|
|
528
|
-
clientId: '138d7bf8-c930-4c6e-b928-97d3a4928b01',
|
|
529
|
-
clientSecret: '03b3955f-d020-4f2a-8a27-4e452d4e27a0',
|
|
530
|
-
scopes: ['auth'],
|
|
531
|
-
issuer: 'https://example.com/app-test',
|
|
532
|
-
redirectUrl: 'app:/callback',
|
|
533
|
-
dangerouslyAllowInsecureHttpRequests: true,
|
|
534
|
-
clientAuthMethod: 'post',
|
|
535
|
-
},
|
|
536
|
-
},
|
|
537
|
-
}),
|
|
538
|
-
};
|
|
539
|
-
await context.agent.emit(OID4VCIHolderEvent.CONTACT_IDENTITY_CREATED, {
|
|
540
|
-
contactId: contact.id,
|
|
541
|
-
identity,
|
|
542
|
-
});
|
|
543
|
-
logger.log(`Contact added: ${correlationId}`);
|
|
544
|
-
return context.agent.cmAddIdentity({ contactId: contact.id, identity });
|
|
545
|
-
}
|
|
546
|
-
async oid4vciHolderGetIssuerBranding(args, context) {
|
|
547
|
-
const { serverMetadata, contact } = args;
|
|
548
|
-
// Here we are fetching issuer branding for a contact. If no contact is found that means we encounter this contact for the first time. This also means we do not have any branding for the contact.
|
|
549
|
-
const issuerCorrelationId = contact?.identities
|
|
550
|
-
.filter((identity) => identity.roles.includes(CredentialRole.ISSUER))
|
|
551
|
-
.map((identity) => identity.identifier.correlationId)[0];
|
|
552
|
-
if (issuerCorrelationId) {
|
|
553
|
-
const branding = await context.agent.ibGetIssuerBranding({ filter: [{ issuerCorrelationId }] });
|
|
554
|
-
if (branding.length > 0) {
|
|
555
|
-
return branding[0].localeBranding;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
// We should have serverMetadata in the context else something went wrong
|
|
559
|
-
if (!serverMetadata) {
|
|
560
|
-
return Promise.reject(Error('Missing serverMetadata in context'));
|
|
561
|
-
}
|
|
562
|
-
return getBasicIssuerLocaleBranding({
|
|
563
|
-
display: serverMetadata.credentialIssuerMetadata?.display ?? [],
|
|
564
|
-
dynamicRegistrationClientMetadata: serverMetadata.credentialIssuerMetadata,
|
|
565
|
-
context,
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
async oid4vciHolderStoreIssuerBranding(args, context) {
|
|
569
|
-
const { issuerBranding, contact } = args;
|
|
570
|
-
if (!issuerBranding || issuerBranding.length === 0 || issuerBranding[0].id) {
|
|
571
|
-
// FIXME we need better separation between a contact(issuer) we encountered before and it's branding vs a new contact and it's branding
|
|
572
|
-
return;
|
|
573
|
-
}
|
|
574
|
-
if (!contact) {
|
|
575
|
-
return Promise.reject(Error('Missing contact in context'));
|
|
576
|
-
}
|
|
577
|
-
const issuerCorrelationId = contact?.identities
|
|
578
|
-
.filter((identity) => identity.roles.includes(CredentialRole.ISSUER))
|
|
579
|
-
.map((identity) => identity.identifier.correlationId)[0];
|
|
580
|
-
// we check for issuer branding as adding an identity might also trigger storing the issuer branding
|
|
581
|
-
const branding = await context.agent.ibGetIssuerBranding({ filter: [{ issuerCorrelationId }] });
|
|
582
|
-
if (branding.length > 0) {
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
await context.agent.ibAddIssuerBranding({
|
|
586
|
-
localeBranding: issuerBranding,
|
|
587
|
-
issuerCorrelationId,
|
|
588
|
-
});
|
|
589
|
-
}
|
|
590
|
-
async oid4vciHolderAssertValidCredentials(args, context) {
|
|
591
|
-
const { credentialsToAccept, issuanceOpt } = args;
|
|
592
|
-
return await Promise.all(credentialsToAccept.map((credentialToAccept) => verifyCredentialToAccept({
|
|
593
|
-
mappedCredential: credentialToAccept,
|
|
594
|
-
onVerifyEBSICredentialIssuer: this.onVerifyEBSICredentialIssuer,
|
|
595
|
-
hasher: this.hasher,
|
|
596
|
-
schemaValidation: issuanceOpt?.schemaValidation,
|
|
597
|
-
context,
|
|
598
|
-
})));
|
|
599
|
-
}
|
|
600
|
-
async oid4vciHolderStoreCredentialBranding(args, context) {
|
|
601
|
-
const { credentialBranding, serverMetadata, selectedCredentials, credentialsToAccept } = args;
|
|
602
|
-
if (serverMetadata === undefined) {
|
|
603
|
-
return Promise.reject(Error('Missing serverMetadata in context'));
|
|
604
|
-
}
|
|
605
|
-
else if (selectedCredentials.length === 0) {
|
|
606
|
-
logger.warning(`No credentials selected for issuer: ${serverMetadata.issuer}`);
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
|
-
let counter = 0;
|
|
610
|
-
for (const credentialId of selectedCredentials) {
|
|
611
|
-
const localeBranding = credentialBranding?.[credentialId];
|
|
612
|
-
if (localeBranding && localeBranding.length > 0) {
|
|
613
|
-
const credential = credentialsToAccept.find((credAccept) => credAccept.credentialToAccept.id === credentialId || JSON.stringify(credAccept.types) === credentialId || credentialsToAccept[counter]);
|
|
614
|
-
counter++;
|
|
615
|
-
await context.agent.ibAddCredentialBranding({
|
|
616
|
-
vcHash: computeEntryHash(credential.rawVerifiableCredential),
|
|
617
|
-
issuerCorrelationId: new URL(serverMetadata.issuer).hostname,
|
|
618
|
-
localeBranding,
|
|
619
|
-
});
|
|
620
|
-
logger.log(`Credential branding for issuer ${serverMetadata.issuer} and type ${credentialId} stored with locales ${localeBranding.map((b) => b.locale).join(',')}`);
|
|
621
|
-
}
|
|
622
|
-
else {
|
|
623
|
-
logger.warning(`No credential branding found for issuer: ${serverMetadata.issuer} and type ${credentialId}`);
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
async oid4vciHolderStoreCredentials(args, context) {
|
|
628
|
-
function trimmed(input) {
|
|
629
|
-
const trim = input?.trim();
|
|
630
|
-
if (trim === '') {
|
|
631
|
-
return undefined;
|
|
632
|
-
}
|
|
633
|
-
return trim;
|
|
634
|
-
}
|
|
635
|
-
const { credentialsToAccept, openID4VCIClientState, credentialsSupported, serverMetadata, selectedCredentials } = args;
|
|
636
|
-
const mappedCredentialToAccept = credentialsToAccept[0];
|
|
637
|
-
if (selectedCredentials && selectedCredentials.length > 1) {
|
|
638
|
-
logger.error(`More than 1 credential selected ${selectedCredentials.join(', ')}, but current service only stores 1 credential!`);
|
|
639
|
-
}
|
|
640
|
-
// TODO determine when and how we should store credentials without key kmsKeyRef & id method), this should be tested with the code below
|
|
641
|
-
const issuanceOpt = args.issuanceOpt ?? mappedCredentialToAccept.credentialToAccept.issuanceOpt;
|
|
642
|
-
if (!issuanceOpt || !issuanceOpt.identifier) {
|
|
643
|
-
return Promise.reject(Error('issuanceOpt.identifier must me set in order to store a credential'));
|
|
644
|
-
}
|
|
645
|
-
const { kmsKeyRef, method } = issuanceOpt.identifier;
|
|
646
|
-
let persist = true;
|
|
647
|
-
const verifiableCredential = mappedCredentialToAccept.uniformVerifiableCredential;
|
|
648
|
-
const notificationId = mappedCredentialToAccept.credentialToAccept.credentialResponse.notification_id;
|
|
649
|
-
const subjectIssuance = mappedCredentialToAccept.credential_subject_issuance;
|
|
650
|
-
const notificationEndpoint = serverMetadata?.credentialIssuerMetadata?.notification_endpoint;
|
|
651
|
-
let holderCredential = undefined;
|
|
652
|
-
if (!notificationEndpoint) {
|
|
653
|
-
logger.log(`Notifications not supported by issuer ${serverMetadata?.issuer}. Will not provide a notification`);
|
|
654
|
-
}
|
|
655
|
-
else if (notificationEndpoint && !notificationId) {
|
|
656
|
-
logger.warning(`Notification endpoint available in issuer metadata with value ${notificationEndpoint}, but no ${notificationId} provided. Will not send a notification to issuer ${serverMetadata?.issuer}`);
|
|
657
|
-
}
|
|
658
|
-
else if (notificationEndpoint && notificationId) {
|
|
659
|
-
logger.log(`Notification id ${notificationId} found, will send back a notification to ${notificationEndpoint}`);
|
|
660
|
-
let event = 'credential_accepted';
|
|
661
|
-
if (Array.isArray(subjectIssuance?.notification_events_supported)) {
|
|
662
|
-
// experimental subject issuance, where a new credential is being created
|
|
663
|
-
event = subjectIssuance.notification_events_supported.includes('credential_accepted_holder_signed')
|
|
664
|
-
? 'credential_accepted_holder_signed'
|
|
665
|
-
: 'credential_deleted_holder_signed';
|
|
666
|
-
logger.log(`Subject issuance/signing will be used, with event`, event);
|
|
667
|
-
const issuerVC = mappedCredentialToAccept.credentialToAccept.credentialResponse.credential;
|
|
668
|
-
const wrappedIssuerVC = CredentialMapper.toWrappedVerifiableCredential(issuerVC, { hasher: this.hasher ?? defaultHasher });
|
|
669
|
-
console.log(`Wrapped VC: ${wrappedIssuerVC.type}, ${wrappedIssuerVC.format}`);
|
|
670
|
-
// We will use the subject of the VCI Issuer (the holder, as the issuer of the new credential, so the below is not a mistake!)
|
|
671
|
-
let issuer;
|
|
672
|
-
if (CredentialMapper.isWrappedSdJwtVerifiableCredential(wrappedIssuerVC)) {
|
|
673
|
-
issuer = trimmed(wrappedIssuerVC.decoded?.sub);
|
|
674
|
-
}
|
|
675
|
-
else if (CredentialMapper.isWrappedW3CVerifiableCredential(wrappedIssuerVC)) {
|
|
676
|
-
issuer = trimmed(wrappedIssuerVC.credential?.sub) ?? trimmed(this.idFromW3cCredentialSubject(wrappedIssuerVC));
|
|
677
|
-
}
|
|
678
|
-
else if (CredentialMapper.isWrappedMdocCredential(wrappedIssuerVC)) {
|
|
679
|
-
return Promise.reject(Error('mdoc not yet supported'));
|
|
680
|
-
}
|
|
681
|
-
if (!issuer) {
|
|
682
|
-
issuer = trimmed(verifiableCredential.credentialSubject?.id);
|
|
683
|
-
}
|
|
684
|
-
if (!issuer && openID4VCIClientState?.kid?.startsWith('did:')) {
|
|
685
|
-
issuer = parseDid(openID4VCIClientState?.kid).did;
|
|
686
|
-
}
|
|
687
|
-
if (!issuer && openID4VCIClientState?.jwk?.kid?.startsWith('did:')) {
|
|
688
|
-
issuer = parseDid(openID4VCIClientState.jwk.kid).did;
|
|
689
|
-
}
|
|
690
|
-
if (!issuer && openID4VCIClientState?.clientId) {
|
|
691
|
-
issuer = trimmed(openID4VCIClientState.clientId);
|
|
692
|
-
}
|
|
693
|
-
if (!issuer && openID4VCIClientState?.accessTokenResponse) {
|
|
694
|
-
const decodedJwt = decodeJWT(openID4VCIClientState.accessTokenResponse.access_token);
|
|
695
|
-
issuer = decodedJwt.payload.sub;
|
|
696
|
-
}
|
|
697
|
-
if (!issuer && mappedCredentialToAccept.credentialToAccept.issuanceOpt.identifier) {
|
|
698
|
-
const resolution = await context.agent.identifierManagedGet(mappedCredentialToAccept.credentialToAccept.issuanceOpt.identifier);
|
|
699
|
-
issuer = resolution.issuer;
|
|
700
|
-
}
|
|
701
|
-
if (!issuer) {
|
|
702
|
-
throw Error(`We could not determine the issuer, which means we cannot sign the credential`);
|
|
703
|
-
}
|
|
704
|
-
logger.log(`Issuer for self-issued credential will be: ${issuer}`);
|
|
705
|
-
const holderCredentialToSign = wrappedIssuerVC.decoded;
|
|
706
|
-
let proofFormat = 'lds';
|
|
707
|
-
if (wrappedIssuerVC.format.includes('jwt') && !wrappedIssuerVC.format.includes('mso_mdoc')) {
|
|
708
|
-
holderCredentialToSign.iss = issuer;
|
|
709
|
-
proofFormat = 'jwt';
|
|
710
|
-
}
|
|
711
|
-
if ('issuer' in holderCredentialToSign && !('iss' in holderCredentialToSign)) {
|
|
712
|
-
holderCredentialToSign.issuer = issuer;
|
|
713
|
-
}
|
|
714
|
-
if ('sub' in holderCredentialToSign) {
|
|
715
|
-
holderCredentialToSign.sub = issuer;
|
|
716
|
-
}
|
|
717
|
-
if ('credentialSubject' in holderCredentialToSign && !Array.isArray(holderCredentialToSign.credentialSubject)) {
|
|
718
|
-
holderCredentialToSign.credentialSubject.id = issuer;
|
|
719
|
-
}
|
|
720
|
-
if ('vc' in holderCredentialToSign) {
|
|
721
|
-
if (holderCredentialToSign.vc.credentialSubject) {
|
|
722
|
-
holderCredentialToSign.vc.credentialSubject.id = issuer;
|
|
723
|
-
}
|
|
724
|
-
holderCredentialToSign.vc.issuer = issuer;
|
|
725
|
-
delete holderCredentialToSign.vc.proof;
|
|
726
|
-
delete holderCredentialToSign.vc.issuanceDate;
|
|
727
|
-
}
|
|
728
|
-
delete holderCredentialToSign.proof;
|
|
729
|
-
delete holderCredentialToSign.issuanceDate;
|
|
730
|
-
delete holderCredentialToSign.iat;
|
|
731
|
-
logger.log(`Subject issuance/signing will sign credential of type ${proofFormat}:`, holderCredentialToSign);
|
|
732
|
-
const issuedVC = await context.agent.createVerifiableCredential({
|
|
733
|
-
credential: holderCredentialToSign,
|
|
734
|
-
fetchRemoteContexts: true,
|
|
735
|
-
save: false,
|
|
736
|
-
proofFormat,
|
|
737
|
-
});
|
|
738
|
-
if (!issuedVC) {
|
|
739
|
-
throw Error(`Could not issue holder credential from the wallet`);
|
|
740
|
-
}
|
|
741
|
-
logger.log(`Holder ${issuedVC.issuer} issued new credential with id ${issuedVC.id}`, issuedVC);
|
|
742
|
-
holderCredential = CredentialMapper.storedCredentialToOriginalFormat(issuedVC);
|
|
743
|
-
persist = event === 'credential_accepted_holder_signed';
|
|
744
|
-
}
|
|
745
|
-
const notificationRequest = {
|
|
746
|
-
notification_id: notificationId,
|
|
747
|
-
...(holderCredential && { credential: holderCredential }),
|
|
748
|
-
event,
|
|
749
|
-
};
|
|
750
|
-
await this.oid4vciHolderSendNotification({
|
|
751
|
-
openID4VCIClientState,
|
|
752
|
-
stored: persist,
|
|
753
|
-
credentialsToAccept,
|
|
754
|
-
credentialsSupported,
|
|
755
|
-
notificationRequest,
|
|
756
|
-
serverMetadata,
|
|
757
|
-
}, context);
|
|
758
|
-
}
|
|
759
|
-
const persistCredential = holderCredential
|
|
760
|
-
? CredentialMapper.storedCredentialToOriginalFormat(holderCredential)
|
|
761
|
-
: mappedCredentialToAccept.rawVerifiableCredential;
|
|
762
|
-
if (!persist && holderCredential) {
|
|
763
|
-
logger.log(`Will not persist credential, since we are signing as a holder and the issuer asked not to persist`);
|
|
764
|
-
}
|
|
765
|
-
else {
|
|
766
|
-
logger.log(`Persisting credential`, persistCredential);
|
|
767
|
-
const issuer = CredentialMapper.issuerCorrelationIdFromIssuerType(verifiableCredential.issuer);
|
|
768
|
-
const [subjectCorrelationType, subjectCorrelationId] = this.determineSubjectCorrelation(issuanceOpt.identifier, issuer);
|
|
769
|
-
const persistedCredential = await context.agent.crsAddCredential({
|
|
770
|
-
credential: {
|
|
771
|
-
rawDocument: ensureRawDocument(persistCredential),
|
|
772
|
-
kmsKeyRef: kmsKeyRef,
|
|
773
|
-
identifierMethod: method,
|
|
774
|
-
credentialRole: CredentialRole.HOLDER,
|
|
775
|
-
issuerCorrelationType: issuer?.startsWith('did:') ? CredentialCorrelationType.DID : CredentialCorrelationType.URL,
|
|
776
|
-
issuerCorrelationId: issuer,
|
|
777
|
-
subjectCorrelationType,
|
|
778
|
-
subjectCorrelationId,
|
|
779
|
-
},
|
|
780
|
-
});
|
|
781
|
-
await context.agent.emit(OID4VCIHolderEvent.CREDENTIAL_STORED, {
|
|
782
|
-
credential: persistedCredential,
|
|
783
|
-
vcHash: persistedCredential.hash,
|
|
784
|
-
});
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
async oid4vciHolderSendNotification(args, context) {
|
|
788
|
-
const { serverMetadata, notificationRequest, openID4VCIClientState } = args;
|
|
789
|
-
const notificationEndpoint = serverMetadata?.credentialIssuerMetadata?.notification_endpoint;
|
|
790
|
-
if (!notificationEndpoint) {
|
|
791
|
-
return;
|
|
792
|
-
}
|
|
793
|
-
else if (!openID4VCIClientState) {
|
|
794
|
-
return Promise.reject(Error('Missing openID4VCI client state in context'));
|
|
795
|
-
}
|
|
796
|
-
else if (!notificationRequest) {
|
|
797
|
-
return Promise.reject(Error('Missing notification request'));
|
|
798
|
-
}
|
|
799
|
-
logger.log(`Will send notification to ${notificationEndpoint}`, notificationRequest);
|
|
800
|
-
const client = await OpenID4VCIClient.fromState({ state: openID4VCIClientState });
|
|
801
|
-
await client.sendNotification({ notificationEndpoint }, notificationRequest, openID4VCIClientState?.accessTokenResponse?.access_token);
|
|
802
|
-
logger.log(`Notification to ${notificationEndpoint} has been dispatched`);
|
|
803
|
-
}
|
|
804
|
-
async getFederationTrust(args, context) {
|
|
805
|
-
const { requestData, serverMetadata, trustAnchors } = args;
|
|
806
|
-
if (trustAnchors.length === 0) {
|
|
807
|
-
return Promise.reject(Error('No trust anchors found'));
|
|
808
|
-
}
|
|
809
|
-
if (!requestData?.uri) {
|
|
810
|
-
return Promise.reject(Error('Missing request URI in context'));
|
|
811
|
-
}
|
|
812
|
-
if (!serverMetadata) {
|
|
813
|
-
return Promise.reject(Error('Missing serverMetadata in context'));
|
|
814
|
-
}
|
|
815
|
-
const url = new URL(requestData?.uri);
|
|
816
|
-
const params = new URLSearchParams(url.search);
|
|
817
|
-
const openidFederation = params.get('openid_federation');
|
|
818
|
-
const entityIdentifier = openidFederation ?? serverMetadata.issuer;
|
|
819
|
-
if (entityIdentifier.startsWith('http://')) {
|
|
820
|
-
console.warn(`OpenID federation does not support http://, only https:// allowed; got: (${url.toString()})`);
|
|
821
|
-
// OIDF always needs to be https
|
|
822
|
-
return [];
|
|
823
|
-
}
|
|
824
|
-
const result = await context.agent.identifierExternalResolveByOIDFEntityId({
|
|
825
|
-
method: 'entity_id',
|
|
826
|
-
trustAnchors: trustAnchors,
|
|
827
|
-
identifier: entityIdentifier,
|
|
828
|
-
});
|
|
829
|
-
return result.trustedAnchors;
|
|
830
|
-
}
|
|
831
|
-
async oid4vciHolderGetIssuerMetadata(args, context) {
|
|
832
|
-
const { issuer, errorOnNotFound = true } = args;
|
|
833
|
-
return MetadataClient.retrieveAllMetadata(issuer, { errorOnNotFound });
|
|
834
|
-
}
|
|
835
|
-
determineSubjectCorrelation(identifier, issuer) {
|
|
836
|
-
switch (identifier.method) {
|
|
837
|
-
case 'did':
|
|
838
|
-
if (isManagedIdentifierResult(identifier) && isManagedIdentifierDidResult(identifier)) {
|
|
839
|
-
return [CredentialCorrelationType.DID, identifier.did];
|
|
840
|
-
}
|
|
841
|
-
else if (isManagedIdentifierDidOpts(identifier)) {
|
|
842
|
-
return [CredentialCorrelationType.DID, typeof identifier.identifier === 'string' ? identifier.identifier : identifier.identifier.did];
|
|
843
|
-
}
|
|
844
|
-
break;
|
|
845
|
-
case 'kid':
|
|
846
|
-
if (isManagedIdentifierResult(identifier) && isManagedIdentifierKidResult(identifier)) {
|
|
847
|
-
return [CredentialCorrelationType.KID, identifier.kid];
|
|
848
|
-
}
|
|
849
|
-
else if (isManagedIdentifierDidOpts(identifier)) {
|
|
850
|
-
return [CredentialCorrelationType.KID, identifier.identifier];
|
|
851
|
-
}
|
|
852
|
-
break;
|
|
853
|
-
case 'x5c':
|
|
854
|
-
if (isManagedIdentifierResult(identifier) && isManagedIdentifierX5cResult(identifier)) {
|
|
855
|
-
return [CredentialCorrelationType.X509_SAN, identifier.x5c.join('\r\n')];
|
|
856
|
-
}
|
|
857
|
-
else if (isManagedIdentifierX5cOpts(identifier)) {
|
|
858
|
-
return [CredentialCorrelationType.X509_SAN, identifier.identifier.join('\r\n')];
|
|
859
|
-
}
|
|
860
|
-
break;
|
|
861
|
-
}
|
|
862
|
-
return [CredentialCorrelationType.URL, issuer];
|
|
863
|
-
}
|
|
864
|
-
idFromW3cCredentialSubject(wrappedIssuerVC) {
|
|
865
|
-
if (Array.isArray(wrappedIssuerVC.credential?.credentialSubject)) {
|
|
866
|
-
if (wrappedIssuerVC.credential?.credentialSubject.length > 0) {
|
|
867
|
-
return wrappedIssuerVC.credential?.credentialSubject[0].id;
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
else {
|
|
871
|
-
return wrappedIssuerVC.credential?.credentialSubject?.id;
|
|
872
|
-
}
|
|
873
|
-
return undefined;
|
|
874
|
-
}
|
|
875
|
-
getCredentialDefinition(issuanceOpt) {
|
|
876
|
-
if (issuanceOpt.format == 'ldp_vc' || issuanceOpt.format == 'jwt_vc_json-ld') {
|
|
877
|
-
return issuanceOpt.credential_definition;
|
|
878
|
-
}
|
|
879
|
-
return undefined;
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
//# sourceMappingURL=OID4VCIHolder.js.map
|