@credo-ts/openid4vc 0.6.0-pr-2195-20250322195244 → 0.6.0-pr-2324-20250625125220
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/build/openid4vc-holder/OpenId4VcHolderApi.d.ts +10 -6
- package/build/openid4vc-holder/OpenId4VcHolderApi.js +2 -4
- package/build/openid4vc-holder/OpenId4VcHolderApi.js.map +1 -1
- package/build/openid4vc-holder/OpenId4VciHolderService.d.ts +14 -19
- package/build/openid4vc-holder/OpenId4VciHolderService.js +425 -203
- package/build/openid4vc-holder/OpenId4VciHolderService.js.map +1 -1
- package/build/openid4vc-holder/OpenId4VciHolderServiceOptions.d.ts +117 -37
- package/build/openid4vc-holder/OpenId4VciHolderServiceOptions.js +1 -0
- package/build/openid4vc-holder/OpenId4VciHolderServiceOptions.js.map +1 -1
- package/build/openid4vc-holder/OpenId4vpHolderService.js +24 -15
- package/build/openid4vc-holder/OpenId4vpHolderService.js.map +1 -1
- package/build/openid4vc-issuer/OpenId4VcIssuerModuleConfig.d.ts +21 -0
- package/build/openid4vc-issuer/OpenId4VcIssuerModuleConfig.js +11 -0
- package/build/openid4vc-issuer/OpenId4VcIssuerModuleConfig.js.map +1 -1
- package/build/openid4vc-issuer/OpenId4VcIssuerService.d.ts +7 -3
- package/build/openid4vc-issuer/OpenId4VcIssuerService.js +387 -167
- package/build/openid4vc-issuer/OpenId4VcIssuerService.js.map +1 -1
- package/build/openid4vc-issuer/OpenId4VcIssuerServiceOptions.d.ts +67 -27
- package/build/openid4vc-issuer/index.d.ts +1 -1
- package/build/openid4vc-issuer/index.js +2 -1
- package/build/openid4vc-issuer/index.js.map +1 -1
- package/build/openid4vc-issuer/repository/OpenId4VcIssuanceSessionRecord.d.ts +29 -5
- package/build/openid4vc-issuer/repository/OpenId4VcIssuanceSessionRecord.js +2 -0
- package/build/openid4vc-issuer/repository/OpenId4VcIssuanceSessionRecord.js.map +1 -1
- package/build/openid4vc-issuer/repository/OpenId4VcIssuerRecord.d.ts +12 -7
- package/build/openid4vc-issuer/repository/OpenId4VcIssuerRecord.js +15 -3
- package/build/openid4vc-issuer/repository/OpenId4VcIssuerRecord.js.map +1 -1
- package/build/openid4vc-issuer/router/accessTokenEndpoint.js +41 -13
- package/build/openid4vc-issuer/router/accessTokenEndpoint.js.map +1 -1
- package/build/openid4vc-issuer/router/authorizationChallengeEndpoint.js +102 -33
- package/build/openid4vc-issuer/router/authorizationChallengeEndpoint.js.map +1 -1
- package/build/openid4vc-issuer/router/credentialEndpoint.js +42 -10
- package/build/openid4vc-issuer/router/credentialEndpoint.js.map +1 -1
- package/build/openid4vc-issuer/router/jwksEndpoint.js +2 -2
- package/build/openid4vc-issuer/router/jwksEndpoint.js.map +1 -1
- package/build/openid4vc-issuer/util/txCode.d.ts +1 -1
- package/build/openid4vc-issuer/util/txCode.js +3 -1
- package/build/openid4vc-issuer/util/txCode.js.map +1 -1
- package/build/openid4vc-verifier/OpenId4VpVerifierService.d.ts +1 -1
- package/build/openid4vc-verifier/OpenId4VpVerifierService.js +70 -65
- package/build/openid4vc-verifier/OpenId4VpVerifierService.js.map +1 -1
- package/build/openid4vc-verifier/OpenId4VpVerifierServiceOptions.d.ts +7 -1
- package/build/shared/callbacks.d.ts +6 -4
- package/build/shared/callbacks.js +212 -69
- package/build/shared/callbacks.js.map +1 -1
- package/build/shared/models/CredentialHolderBinding.d.ts +65 -11
- package/build/shared/models/OpenId4VcJwtIssuer.d.ts +10 -5
- package/build/shared/models/OpenId4VciCredentialFormatProfile.d.ts +1 -0
- package/build/shared/models/OpenId4VciCredentialFormatProfile.js +1 -0
- package/build/shared/models/OpenId4VciCredentialFormatProfile.js.map +1 -1
- package/build/shared/router/tenants.js +2 -2
- package/build/shared/router/tenants.js.map +1 -1
- package/build/shared/utils.d.ts +4 -9
- package/build/shared/utils.js +27 -44
- package/build/shared/utils.js.map +1 -1
- package/package.json +14 -14
|
@@ -35,17 +35,17 @@ let OpenId4VcIssuerService = class OpenId4VcIssuerService {
|
|
|
35
35
|
this.openId4VcIssuanceSessionRepository = openId4VcIssuanceSessionRepository;
|
|
36
36
|
}
|
|
37
37
|
async createStatelessCredentialOffer(agentContext, options) {
|
|
38
|
-
const { authorizationCodeFlowConfig, issuer,
|
|
38
|
+
const { authorizationCodeFlowConfig, issuer, credentialConfigurationIds } = options;
|
|
39
39
|
const vcIssuer = this.getIssuer(agentContext);
|
|
40
40
|
const issuerMetadata = await this.getIssuerMetadata(agentContext, issuer);
|
|
41
|
-
const uniqueOfferedCredentials = Array.from(new Set(options.
|
|
42
|
-
if (uniqueOfferedCredentials.length !==
|
|
41
|
+
const uniqueOfferedCredentials = Array.from(new Set(options.credentialConfigurationIds));
|
|
42
|
+
if (uniqueOfferedCredentials.length !== credentialConfigurationIds.length) {
|
|
43
43
|
throw new core_1.CredoError('All offered credentials must have unique ids.');
|
|
44
44
|
}
|
|
45
45
|
// Check if all the offered credential configuration ids have a scope value. If not, it won't be possible to actually request
|
|
46
46
|
// issuance of the credential later on
|
|
47
47
|
(0, openid4vci_1.extractScopesForCredentialConfigurationIds)({
|
|
48
|
-
credentialConfigurationIds: options.
|
|
48
|
+
credentialConfigurationIds: options.credentialConfigurationIds,
|
|
49
49
|
issuerMetadata,
|
|
50
50
|
throwOnConfigurationWithoutScope: true,
|
|
51
51
|
});
|
|
@@ -53,7 +53,7 @@ let OpenId4VcIssuerService = class OpenId4VcIssuerService {
|
|
|
53
53
|
throw new core_1.CredoError('Stateless offers can only be created for external authorization servers. Make sure to configure an external authorization server on the issuer record, and provide the authoriation server url.');
|
|
54
54
|
}
|
|
55
55
|
const { credentialOffer, credentialOfferObject } = await vcIssuer.createCredentialOffer({
|
|
56
|
-
credentialConfigurationIds: options.
|
|
56
|
+
credentialConfigurationIds: options.credentialConfigurationIds,
|
|
57
57
|
grants: {
|
|
58
58
|
authorization_code: {
|
|
59
59
|
authorization_server: authorizationCodeFlowConfig.authorizationServerUrl,
|
|
@@ -68,14 +68,14 @@ let OpenId4VcIssuerService = class OpenId4VcIssuerService {
|
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
70
|
async createCredentialOffer(agentContext, options) {
|
|
71
|
-
const { preAuthorizedCodeFlowConfig, authorizationCodeFlowConfig, issuer,
|
|
71
|
+
const { preAuthorizedCodeFlowConfig, authorizationCodeFlowConfig, issuer, credentialConfigurationIds, version = 'v1.draft11-15', authorization, } = options;
|
|
72
72
|
if (!preAuthorizedCodeFlowConfig && !authorizationCodeFlowConfig) {
|
|
73
73
|
throw new core_1.CredoError('Authorization Config or Pre-Authorized Config must be provided.');
|
|
74
74
|
}
|
|
75
75
|
const vcIssuer = this.getIssuer(agentContext);
|
|
76
76
|
const issuerMetadata = await this.getIssuerMetadata(agentContext, issuer);
|
|
77
|
-
const uniqueOfferedCredentials = Array.from(new Set(options.
|
|
78
|
-
if (uniqueOfferedCredentials.length !==
|
|
77
|
+
const uniqueOfferedCredentials = Array.from(new Set(options.credentialConfigurationIds));
|
|
78
|
+
if (uniqueOfferedCredentials.length !== credentialConfigurationIds.length) {
|
|
79
79
|
throw new core_1.CredoError('All offered credentials must have unique ids.');
|
|
80
80
|
}
|
|
81
81
|
if (uniqueOfferedCredentials.length === 0) {
|
|
@@ -91,7 +91,7 @@ let OpenId4VcIssuerService = class OpenId4VcIssuerService {
|
|
|
91
91
|
// issuance of the credential later on. For pre-auth it's not needed to add a scope.
|
|
92
92
|
if (options.authorizationCodeFlowConfig) {
|
|
93
93
|
(0, openid4vci_1.extractScopesForCredentialConfigurationIds)({
|
|
94
|
-
credentialConfigurationIds: options.
|
|
94
|
+
credentialConfigurationIds: options.credentialConfigurationIds,
|
|
95
95
|
issuerMetadata,
|
|
96
96
|
throwOnConfigurationWithoutScope: true,
|
|
97
97
|
});
|
|
@@ -102,12 +102,12 @@ let OpenId4VcIssuerService = class OpenId4VcIssuerService {
|
|
|
102
102
|
authorizationCodeFlowConfig,
|
|
103
103
|
});
|
|
104
104
|
const { credentialOffer, credentialOfferObject } = await vcIssuer.createCredentialOffer({
|
|
105
|
-
credentialConfigurationIds: options.
|
|
105
|
+
credentialConfigurationIds: options.credentialConfigurationIds,
|
|
106
106
|
grants,
|
|
107
107
|
credentialOfferUri: hostedCredentialOfferUri,
|
|
108
108
|
credentialOfferScheme: options.baseUri,
|
|
109
109
|
issuerMetadata: {
|
|
110
|
-
originalDraftVersion: version === 'v1.draft11-
|
|
110
|
+
originalDraftVersion: version === 'v1.draft11-15' ? openid4vci_1.Openid4vciDraftVersion.Draft11 : openid4vci_1.Openid4vciDraftVersion.Draft15,
|
|
111
111
|
...issuerMetadata,
|
|
112
112
|
},
|
|
113
113
|
});
|
|
@@ -128,6 +128,16 @@ let OpenId4VcIssuerService = class OpenId4VcIssuerService {
|
|
|
128
128
|
required: true,
|
|
129
129
|
}
|
|
130
130
|
: undefined,
|
|
131
|
+
dpop: authorization?.requireDpop
|
|
132
|
+
? {
|
|
133
|
+
required: true,
|
|
134
|
+
}
|
|
135
|
+
: undefined,
|
|
136
|
+
walletAttestation: authorization?.requireWalletAttestation
|
|
137
|
+
? {
|
|
138
|
+
required: true,
|
|
139
|
+
}
|
|
140
|
+
: undefined,
|
|
131
141
|
// TODO: how to mix pre-auth and auth? Need to do state checks
|
|
132
142
|
preAuthorizedCode: credentialOfferObject.grants?.[oauth2_1.preAuthorizedCodeGrantIdentifier]?.['pre-authorized_code'],
|
|
133
143
|
userPin: preAuthorizedCodeFlowConfig?.txCode
|
|
@@ -153,25 +163,95 @@ let OpenId4VcIssuerService = class OpenId4VcIssuerService {
|
|
|
153
163
|
]);
|
|
154
164
|
const { issuanceSession } = options;
|
|
155
165
|
const issuer = await this.getIssuerByIssuerId(agentContext, options.issuanceSession.issuerId);
|
|
156
|
-
const vcIssuer = this.getIssuer(agentContext);
|
|
166
|
+
const vcIssuer = this.getIssuer(agentContext, { issuanceSessionId: issuanceSession.id });
|
|
157
167
|
const issuerMetadata = await this.getIssuerMetadata(agentContext, issuer);
|
|
158
168
|
const parsedCredentialRequest = vcIssuer.parseCredentialRequest({
|
|
169
|
+
issuerMetadata,
|
|
159
170
|
credentialRequest: options.credentialRequest,
|
|
160
171
|
});
|
|
161
|
-
const { credentialRequest, credentialIdentifier, format,
|
|
172
|
+
const { credentialRequest, credentialIdentifier, format, } = parsedCredentialRequest;
|
|
162
173
|
if (credentialIdentifier) {
|
|
163
174
|
throw new oauth2_1.Oauth2ServerErrorResponseError({
|
|
164
175
|
error: oauth2_1.Oauth2ErrorCodes.InvalidCredentialRequest,
|
|
165
176
|
error_description: `Using unsupported 'credential_identifier'`,
|
|
166
177
|
});
|
|
167
178
|
}
|
|
168
|
-
if (!format) {
|
|
179
|
+
if (credentialRequest.format && !format) {
|
|
180
|
+
throw new oauth2_1.Oauth2ServerErrorResponseError({
|
|
181
|
+
error: oauth2_1.Oauth2ErrorCodes.UnsupportedCredentialFormat,
|
|
182
|
+
error_description: `Unsupported credential request based on format '${credentialRequest.format}'`,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
if (parsedCredentialRequest.credentialConfigurationId && !parsedCredentialRequest.credentialConfiguration) {
|
|
169
186
|
throw new oauth2_1.Oauth2ServerErrorResponseError({
|
|
170
187
|
error: oauth2_1.Oauth2ErrorCodes.UnsupportedCredentialFormat,
|
|
171
|
-
error_description: `Unsupported credential
|
|
188
|
+
error_description: `Unsupported credential request based on credential configuration id ${credentialRequest.credential_configuration_id}`,
|
|
172
189
|
});
|
|
173
190
|
}
|
|
174
|
-
|
|
191
|
+
const { credentialConfiguration, credentialConfigurationId } = this.getCredentialConfigurationsForRequest({
|
|
192
|
+
issuanceSession,
|
|
193
|
+
issuerMetadata,
|
|
194
|
+
requestFormat: format,
|
|
195
|
+
credentialConfigurations: parsedCredentialRequest.credentialConfiguration && parsedCredentialRequest.credentialConfigurationId
|
|
196
|
+
? {
|
|
197
|
+
[parsedCredentialRequest.credentialConfigurationId]: parsedCredentialRequest.credentialConfiguration,
|
|
198
|
+
}
|
|
199
|
+
: undefined,
|
|
200
|
+
authorization: options.authorization,
|
|
201
|
+
});
|
|
202
|
+
const verifiedCredentialRequestProofs = await this.verifyCredentialRequestProofs(agentContext, {
|
|
203
|
+
issuanceSession,
|
|
204
|
+
issuer,
|
|
205
|
+
parsedCredentialRequest,
|
|
206
|
+
credentialConfiguration,
|
|
207
|
+
credentialConfigurationId,
|
|
208
|
+
});
|
|
209
|
+
const signedCredentials = await this.getSignedCredentials(agentContext, {
|
|
210
|
+
credentialRequest,
|
|
211
|
+
issuanceSession,
|
|
212
|
+
issuer,
|
|
213
|
+
credentialConfiguration,
|
|
214
|
+
credentialConfigurationId,
|
|
215
|
+
requestFormat: format,
|
|
216
|
+
authorization: options.authorization,
|
|
217
|
+
credentialRequestToCredentialMapper: options.credentialRequestToCredentialMapper,
|
|
218
|
+
credentialRequestProofs: verifiedCredentialRequestProofs,
|
|
219
|
+
});
|
|
220
|
+
// NOTE: nonce in credential response is deprecated in newer drafts, but for now we keep it in
|
|
221
|
+
const { cNonce, cNonceExpiresInSeconds } = await this.createNonce(agentContext, issuer);
|
|
222
|
+
const credentialResponse = vcIssuer.createCredentialResponse({
|
|
223
|
+
credential: credentialRequest.proof ? signedCredentials.credentials[0] : undefined,
|
|
224
|
+
credentials: credentialRequest.proofs ? signedCredentials.credentials : undefined,
|
|
225
|
+
cNonce,
|
|
226
|
+
cNonceExpiresInSeconds,
|
|
227
|
+
credentialRequest: parsedCredentialRequest,
|
|
228
|
+
});
|
|
229
|
+
issuanceSession.issuedCredentials.push(credentialConfigurationId);
|
|
230
|
+
const newState = issuanceSession.issuedCredentials.length >=
|
|
231
|
+
issuanceSession.credentialOfferPayload.credential_configuration_ids.length
|
|
232
|
+
? OpenId4VcIssuanceSessionState_1.OpenId4VcIssuanceSessionState.Completed
|
|
233
|
+
: OpenId4VcIssuanceSessionState_1.OpenId4VcIssuanceSessionState.CredentialsPartiallyIssued;
|
|
234
|
+
await this.updateState(agentContext, issuanceSession, newState);
|
|
235
|
+
return {
|
|
236
|
+
credentialResponse,
|
|
237
|
+
issuanceSession,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
async verifyCredentialRequestProofs(agentContext, options) {
|
|
241
|
+
const { parsedCredentialRequest, issuer, issuanceSession, credentialConfiguration, credentialConfigurationId } = options;
|
|
242
|
+
const { proofs } = parsedCredentialRequest;
|
|
243
|
+
const vcIssuer = this.getIssuer(agentContext, { issuanceSessionId: issuanceSession.id });
|
|
244
|
+
const issuerMetadata = await this.getIssuerMetadata(agentContext, issuer);
|
|
245
|
+
// FIXME: verify request against the configuration
|
|
246
|
+
// - key attestations required
|
|
247
|
+
// - proof types supported
|
|
248
|
+
// - signing alg values supported
|
|
249
|
+
// - key attestation level met.
|
|
250
|
+
const allowedProofTypes = credentialConfiguration.proof_types_supported ?? {
|
|
251
|
+
jwt: { proof_signing_alg_values_supported: (0, utils_1.getSupportedJwaSignatureAlgorithms)(agentContext) },
|
|
252
|
+
};
|
|
253
|
+
const [proofType, proofValue] = Object.entries(proofs ?? {})[0] ?? [];
|
|
254
|
+
if (!proofType || !proofValue || proofValue.length === 0) {
|
|
175
255
|
const { cNonce, cNonceExpiresInSeconds } = await this.createNonce(agentContext, issuer);
|
|
176
256
|
throw new oauth2_1.Oauth2ServerErrorResponseError({
|
|
177
257
|
error: oauth2_1.Oauth2ErrorCodes.InvalidProof,
|
|
@@ -180,38 +260,66 @@ let OpenId4VcIssuerService = class OpenId4VcIssuerService {
|
|
|
180
260
|
c_nonce_expires_in: cNonceExpiresInSeconds,
|
|
181
261
|
});
|
|
182
262
|
}
|
|
263
|
+
if (proofType !== 'jwt' && proofType !== 'attestation') {
|
|
264
|
+
throw new oauth2_1.Oauth2ServerErrorResponseError({
|
|
265
|
+
error: oauth2_1.Oauth2ErrorCodes.InvalidProof,
|
|
266
|
+
error_description: `Proof type '${proofType}' is not supported `,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
const supportedProofType = allowedProofTypes[proofType];
|
|
270
|
+
if (!supportedProofType) {
|
|
271
|
+
throw new oauth2_1.Oauth2ServerErrorResponseError({
|
|
272
|
+
error: oauth2_1.Oauth2ErrorCodes.InvalidProof,
|
|
273
|
+
error_description: `Proof type '${proofType}' is not supported for credential configuration '${credentialConfigurationId}'`,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
if (proofType === 'attestation' && proofValue.length !== 1) {
|
|
277
|
+
throw new oauth2_1.Oauth2ServerErrorResponseError({
|
|
278
|
+
error: oauth2_1.Oauth2ErrorCodes.InvalidProof,
|
|
279
|
+
error_description: "Only a single proofs entry is supported for proof type 'attestation'",
|
|
280
|
+
});
|
|
281
|
+
}
|
|
183
282
|
await this.updateState(agentContext, issuanceSession, OpenId4VcIssuanceSessionState_1.OpenId4VcIssuanceSessionState.CredentialRequestReceived);
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const { signer, payload } = await vcIssuer.verifyCredentialRequestJwtProof({
|
|
283
|
+
if (proofType === 'attestation') {
|
|
284
|
+
const keyAttestationJwt = proofValue[0];
|
|
285
|
+
const keyAttestation = await vcIssuer.verifyCredentialRequestAttestationProof({
|
|
188
286
|
issuerMetadata,
|
|
189
|
-
|
|
190
|
-
clientId: options.issuanceSession.clientId,
|
|
287
|
+
keyAttestationJwt,
|
|
191
288
|
});
|
|
192
|
-
if (!
|
|
193
|
-
const { cNonce, cNonceExpiresInSeconds } = await this.createNonce(agentContext, issuer);
|
|
289
|
+
if (!supportedProofType.proof_signing_alg_values_supported.includes(keyAttestation.header.alg)) {
|
|
194
290
|
throw new oauth2_1.Oauth2ServerErrorResponseError({
|
|
195
291
|
error: oauth2_1.Oauth2ErrorCodes.InvalidProof,
|
|
196
|
-
error_description: '
|
|
197
|
-
c_nonce: cNonce,
|
|
198
|
-
c_nonce_expires_in: cNonceExpiresInSeconds,
|
|
292
|
+
error_description: `Proof signing alg value '${keyAttestation.header.alg}' is not supported for proof type 'attestation' in credentail configuration '${credentialConfigurationId}'`,
|
|
199
293
|
});
|
|
200
294
|
}
|
|
201
|
-
|
|
202
|
-
if (!previousNonce)
|
|
203
|
-
previousNonce = payload.nonce;
|
|
204
|
-
if (previousNonce !== payload.nonce) {
|
|
295
|
+
if (!keyAttestation.payload.nonce) {
|
|
205
296
|
const { cNonce, cNonceExpiresInSeconds } = await this.createNonce(agentContext, issuer);
|
|
206
297
|
throw new oauth2_1.Oauth2ServerErrorResponseError({
|
|
207
298
|
error: oauth2_1.Oauth2ErrorCodes.InvalidProof,
|
|
208
|
-
error_description: '
|
|
299
|
+
error_description: 'Missing nonce in attestation proof in credential request. If no nonce is present in the attestation, use the jwt proof type instead',
|
|
209
300
|
c_nonce: cNonce,
|
|
210
301
|
c_nonce_expires_in: cNonceExpiresInSeconds,
|
|
211
302
|
});
|
|
212
303
|
}
|
|
213
|
-
|
|
214
|
-
|
|
304
|
+
if (supportedProofType.key_attestations_required && keyAttestation) {
|
|
305
|
+
const expectedKeyStorage = supportedProofType.key_attestations_required.key_storage;
|
|
306
|
+
const expectedUserAuthentication = supportedProofType.key_attestations_required.user_authentication;
|
|
307
|
+
if (expectedKeyStorage &&
|
|
308
|
+
!expectedKeyStorage.some((keyStorage) => keyAttestation.payload.key_storage?.includes(keyStorage))) {
|
|
309
|
+
throw new oauth2_1.Oauth2ServerErrorResponseError({
|
|
310
|
+
error: oauth2_1.Oauth2ErrorCodes.InvalidProof,
|
|
311
|
+
error_description: `Insufficent key_storage for key attestation. Proof type 'attestation' for credential configuration '${credentialConfigurationId}', expects one of key_storage values ${expectedKeyStorage.join(', ')}`,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
if (expectedUserAuthentication &&
|
|
315
|
+
!expectedUserAuthentication.some((userAuthentication) => keyAttestation.payload.user_authentication?.includes(userAuthentication))) {
|
|
316
|
+
throw new oauth2_1.Oauth2ServerErrorResponseError({
|
|
317
|
+
error: oauth2_1.Oauth2ErrorCodes.InvalidProof,
|
|
318
|
+
error_description: `Insufficent user_authentication for key attestation. Proof type 'attestation' for credential configuration '${credentialConfigurationId}', expects one of user_authentication values ${expectedUserAuthentication.join(', ')}`,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
await this.verifyNonce(agentContext, issuer, keyAttestation.payload.nonce).catch(async (error) => {
|
|
215
323
|
const { cNonce, cNonceExpiresInSeconds } = await this.createNonce(agentContext, issuer);
|
|
216
324
|
throw new oauth2_1.Oauth2ServerErrorResponseError({
|
|
217
325
|
error: oauth2_1.Oauth2ErrorCodes.InvalidNonce,
|
|
@@ -222,36 +330,167 @@ let OpenId4VcIssuerService = class OpenId4VcIssuerService {
|
|
|
222
330
|
cause: error,
|
|
223
331
|
});
|
|
224
332
|
});
|
|
225
|
-
|
|
333
|
+
return {
|
|
334
|
+
bindingMethod: 'jwk',
|
|
335
|
+
keys: keyAttestation.payload.attested_keys.map((attestedKey) => {
|
|
336
|
+
return {
|
|
337
|
+
method: 'jwk',
|
|
338
|
+
jwk: core_1.Kms.PublicJwk.fromUnknown(attestedKey),
|
|
339
|
+
};
|
|
340
|
+
}),
|
|
341
|
+
proofType: 'attestation',
|
|
342
|
+
// It's up to the credential request mapper to ensure we trust the key attestation signer
|
|
343
|
+
// For x5c it's kinda covered already.
|
|
344
|
+
keyAttestation,
|
|
345
|
+
};
|
|
226
346
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
347
|
+
if (proofType === 'jwt') {
|
|
348
|
+
let firstNonce = undefined;
|
|
349
|
+
const proofSigners = [];
|
|
350
|
+
for (const jwt of proofValue) {
|
|
351
|
+
const { signer, payload, header, keyAttestation } = await vcIssuer.verifyCredentialRequestJwtProof({
|
|
352
|
+
issuerMetadata,
|
|
353
|
+
jwt,
|
|
354
|
+
clientId: options.issuanceSession.clientId,
|
|
355
|
+
});
|
|
356
|
+
// TOOD: we should probably do this check before signature verification, but we then we
|
|
357
|
+
// first need to decode the jwt
|
|
358
|
+
if (!supportedProofType.proof_signing_alg_values_supported.includes(header.alg)) {
|
|
359
|
+
throw new oauth2_1.Oauth2ServerErrorResponseError({
|
|
360
|
+
error: oauth2_1.Oauth2ErrorCodes.InvalidProof,
|
|
361
|
+
error_description: `Proof signing alg value '${header.alg}' is not supported for proof type 'jwt' in credentail configuration '${credentialConfigurationId}'`,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
if (signer.method !== 'jwk' && signer.method !== 'did') {
|
|
365
|
+
throw new oauth2_1.Oauth2ServerErrorResponseError({
|
|
366
|
+
error: oauth2_1.Oauth2ErrorCodes.InvalidProof,
|
|
367
|
+
error_description: "Only 'jwk' and 'did' binding methods supported for jwt proof",
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
if (proofSigners[0] && signer.method !== proofSigners[0].method) {
|
|
371
|
+
throw new oauth2_1.Oauth2ServerErrorResponseError({
|
|
372
|
+
error: oauth2_1.Oauth2ErrorCodes.InvalidProof,
|
|
373
|
+
error_description: "All proofs must be signed using the same binding method. Found a mix of 'did' and 'jwk'",
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
if (proofSigners[0] && signer.alg !== proofSigners[0].alg) {
|
|
377
|
+
throw new oauth2_1.Oauth2ServerErrorResponseError({
|
|
378
|
+
error: oauth2_1.Oauth2ErrorCodes.InvalidProof,
|
|
379
|
+
error_description: "All proofs must be signed using the same alg value. Found a mix of different 'alg' values.",
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
if (keyAttestation && signer.method === 'did') {
|
|
383
|
+
throw new oauth2_1.Oauth2ServerErrorResponseError({
|
|
384
|
+
error: oauth2_1.Oauth2ErrorCodes.InvalidProof,
|
|
385
|
+
error_description: "Binding method 'did' is not supported when a key attestation is provided.",
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
if (supportedProofType.key_attestations_required && !keyAttestation) {
|
|
389
|
+
throw new oauth2_1.Oauth2ServerErrorResponseError({
|
|
390
|
+
error: oauth2_1.Oauth2ErrorCodes.InvalidProof,
|
|
391
|
+
error_description: `Missing required key attestation. Key attestations are required for proof type 'jwt' in credentail configuration '${credentialConfigurationId}'`,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
if (supportedProofType.key_attestations_required && keyAttestation) {
|
|
395
|
+
const expectedKeyStorage = supportedProofType.key_attestations_required.key_storage;
|
|
396
|
+
const expectedUserAuthentication = supportedProofType.key_attestations_required.user_authentication;
|
|
397
|
+
if (expectedKeyStorage &&
|
|
398
|
+
!expectedKeyStorage.some((keyStorage) => keyAttestation.payload.key_storage?.includes(keyStorage))) {
|
|
399
|
+
throw new oauth2_1.Oauth2ServerErrorResponseError({
|
|
400
|
+
error: oauth2_1.Oauth2ErrorCodes.InvalidProof,
|
|
401
|
+
error_description: `Insufficent key_storage for key attestation. Proof type 'jwt' for credential configuration '${credentialConfigurationId}', expects one of key_storage values ${expectedKeyStorage.join(', ')}`,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
if (expectedUserAuthentication &&
|
|
405
|
+
!expectedUserAuthentication.some((userAuthentication) => keyAttestation.payload.user_authentication?.includes(userAuthentication))) {
|
|
406
|
+
throw new oauth2_1.Oauth2ServerErrorResponseError({
|
|
407
|
+
error: oauth2_1.Oauth2ErrorCodes.InvalidProof,
|
|
408
|
+
error_description: `Insufficent user_authentication for key attestation. Proof type 'jwt' for credential configuration '${credentialConfigurationId}', expects one of user_authentication values ${expectedUserAuthentication.join(', ')}`,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (keyAttestation && proofValue.length > 1) {
|
|
413
|
+
throw new oauth2_1.Oauth2ServerErrorResponseError({
|
|
414
|
+
error: oauth2_1.Oauth2ErrorCodes.InvalidProof,
|
|
415
|
+
error_description: "Only a single proofs entry is supported when jwt proof header contains 'key_attestation'",
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
if (!payload.nonce) {
|
|
419
|
+
const { cNonce, cNonceExpiresInSeconds } = await this.createNonce(agentContext, issuer);
|
|
420
|
+
throw new oauth2_1.Oauth2ServerErrorResponseError({
|
|
421
|
+
error: oauth2_1.Oauth2ErrorCodes.InvalidProof,
|
|
422
|
+
error_description: 'Missing nonce in proof(s) in credential request',
|
|
423
|
+
c_nonce: cNonce,
|
|
424
|
+
c_nonce_expires_in: cNonceExpiresInSeconds,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
// Set previous nonce if not yet set (first iteration)
|
|
428
|
+
if (!firstNonce)
|
|
429
|
+
firstNonce = payload.nonce;
|
|
430
|
+
if (firstNonce !== payload.nonce) {
|
|
431
|
+
const { cNonce, cNonceExpiresInSeconds } = await this.createNonce(agentContext, issuer);
|
|
432
|
+
throw new oauth2_1.Oauth2ServerErrorResponseError({
|
|
433
|
+
error: oauth2_1.Oauth2ErrorCodes.InvalidProof,
|
|
434
|
+
error_description: 'Not all nonce values in proofs are equal',
|
|
435
|
+
c_nonce: cNonce,
|
|
436
|
+
c_nonce_expires_in: cNonceExpiresInSeconds,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
// Verify the nonce
|
|
440
|
+
await this.verifyNonce(agentContext, issuer, payload.nonce).catch(async (error) => {
|
|
441
|
+
const { cNonce, cNonceExpiresInSeconds } = await this.createNonce(agentContext, issuer);
|
|
442
|
+
throw new oauth2_1.Oauth2ServerErrorResponseError({
|
|
443
|
+
error: oauth2_1.Oauth2ErrorCodes.InvalidNonce,
|
|
444
|
+
error_description: 'Invalid nonce in credential request',
|
|
445
|
+
c_nonce: cNonce,
|
|
446
|
+
c_nonce_expires_in: cNonceExpiresInSeconds,
|
|
447
|
+
}, {
|
|
448
|
+
cause: error,
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
if (keyAttestation) {
|
|
452
|
+
return {
|
|
453
|
+
proofType: 'jwt',
|
|
454
|
+
bindingMethod: 'jwk',
|
|
455
|
+
keys: keyAttestation.payload.attested_keys.map((attestedKey) => {
|
|
456
|
+
return {
|
|
457
|
+
method: 'jwk',
|
|
458
|
+
jwk: core_1.Kms.PublicJwk.fromUnknown(attestedKey),
|
|
459
|
+
};
|
|
460
|
+
}),
|
|
461
|
+
keyAttestation,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
proofSigners.push(signer);
|
|
465
|
+
}
|
|
466
|
+
if (proofSigners[0].method === 'did') {
|
|
467
|
+
const signers = proofSigners;
|
|
468
|
+
return {
|
|
469
|
+
proofType: 'jwt',
|
|
470
|
+
bindingMethod: 'did',
|
|
471
|
+
keys: signers.map((signer) => ({
|
|
472
|
+
didUrl: signer.didUrl,
|
|
473
|
+
method: 'did',
|
|
474
|
+
jwk: core_1.Kms.PublicJwk.fromUnknown(signer.publicJwk),
|
|
475
|
+
})),
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
proofType: 'jwt',
|
|
480
|
+
bindingMethod: 'jwk',
|
|
481
|
+
keys: proofSigners.map((signer) => {
|
|
482
|
+
return {
|
|
483
|
+
method: 'jwk',
|
|
484
|
+
jwk: core_1.Kms.PublicJwk.fromUnknown(signer.publicJwk),
|
|
485
|
+
};
|
|
486
|
+
}),
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
// This will not happen, but to make TS happy
|
|
490
|
+
throw new oauth2_1.Oauth2ServerErrorResponseError({
|
|
491
|
+
error: oauth2_1.Oauth2ErrorCodes.InvalidProof,
|
|
492
|
+
error_description: 'Missing required proof(s) in credential request',
|
|
244
493
|
});
|
|
245
|
-
issuanceSession.issuedCredentials.push(signedCredentials.credentialConfigurationId);
|
|
246
|
-
const newState = issuanceSession.issuedCredentials.length >=
|
|
247
|
-
issuanceSession.credentialOfferPayload.credential_configuration_ids.length
|
|
248
|
-
? OpenId4VcIssuanceSessionState_1.OpenId4VcIssuanceSessionState.Completed
|
|
249
|
-
: OpenId4VcIssuanceSessionState_1.OpenId4VcIssuanceSessionState.CredentialsPartiallyIssued;
|
|
250
|
-
await this.updateState(agentContext, issuanceSession, newState);
|
|
251
|
-
return {
|
|
252
|
-
credentialResponse,
|
|
253
|
-
issuanceSession,
|
|
254
|
-
};
|
|
255
494
|
}
|
|
256
495
|
async findIssuanceSessionsByQuery(agentContext, query, queryOptions) {
|
|
257
496
|
return this.openId4VcIssuanceSessionRepository.findByQuery(agentContext, query, queryOptions);
|
|
@@ -272,17 +511,18 @@ let OpenId4VcIssuerService = class OpenId4VcIssuerService {
|
|
|
272
511
|
return this.openId4VcIssuerRepository.update(agentContext, issuer);
|
|
273
512
|
}
|
|
274
513
|
async createIssuer(agentContext, options) {
|
|
514
|
+
const kms = agentContext.resolve(core_1.Kms.KeyManagementApi);
|
|
275
515
|
// TODO: ideally we can store additional data with a key, such as:
|
|
276
516
|
// - createdAt
|
|
277
517
|
// - purpose
|
|
278
|
-
const accessTokenSignerKey = await
|
|
279
|
-
|
|
518
|
+
const accessTokenSignerKey = await kms.createKey({
|
|
519
|
+
type: options.accessTokenSignerKeyType ?? { kty: 'OKP', crv: 'Ed25519' },
|
|
280
520
|
});
|
|
281
521
|
const openId4VcIssuer = new repository_1.OpenId4VcIssuerRecord({
|
|
282
522
|
issuerId: options.issuerId ?? core_1.utils.uuid(),
|
|
283
523
|
display: options.display,
|
|
284
524
|
dpopSigningAlgValuesSupported: options.dpopSigningAlgValuesSupported,
|
|
285
|
-
|
|
525
|
+
accessTokenPublicJwk: accessTokenSignerKey.publicJwk,
|
|
286
526
|
authorizationServerConfigs: options.authorizationServerConfigs,
|
|
287
527
|
credentialConfigurationsSupported: options.credentialConfigurationsSupported,
|
|
288
528
|
batchCredentialIssuance: options.batchCredentialIssuance,
|
|
@@ -292,12 +532,17 @@ let OpenId4VcIssuerService = class OpenId4VcIssuerService {
|
|
|
292
532
|
return openId4VcIssuer;
|
|
293
533
|
}
|
|
294
534
|
async rotateAccessTokenSigningKey(agentContext, issuer, options) {
|
|
295
|
-
const
|
|
296
|
-
|
|
535
|
+
const kms = agentContext.resolve(core_1.Kms.KeyManagementApi);
|
|
536
|
+
const previousKey = issuer.resolvedAccessTokenPublicJwk;
|
|
537
|
+
const accessTokenSignerKey = await kms.createKey({
|
|
538
|
+
type: options?.accessTokenSignerKeyType ?? { kty: 'OKP', crv: 'Ed25519' },
|
|
297
539
|
});
|
|
298
|
-
|
|
299
|
-
issuer.accessTokenPublicKeyFingerprint = accessTokenSignerKey.fingerprint;
|
|
540
|
+
issuer.accessTokenPublicJwk = accessTokenSignerKey.publicJwk;
|
|
300
541
|
await this.openId4VcIssuerRepository.update(agentContext, issuer);
|
|
542
|
+
// Remove previous key
|
|
543
|
+
await kms.deleteKey({
|
|
544
|
+
keyId: previousKey.keyId,
|
|
545
|
+
});
|
|
301
546
|
}
|
|
302
547
|
/**
|
|
303
548
|
* @param fetchExternalAuthorizationServerMetadata defaults to false
|
|
@@ -356,18 +601,17 @@ let OpenId4VcIssuerService = class OpenId4VcIssuerService {
|
|
|
356
601
|
const jwsService = agentContext.dependencyManager.resolve(core_1.JwsService);
|
|
357
602
|
const cNonceExpiresInSeconds = this.openId4VcIssuerConfig.cNonceExpiresInSeconds;
|
|
358
603
|
const cNonceExpiresAt = (0, utils_1.addSecondsToDate)(new Date(), cNonceExpiresInSeconds);
|
|
359
|
-
const key =
|
|
360
|
-
const jwk = (0, core_1.getJwkFromKey)(key);
|
|
604
|
+
const key = issuer.resolvedAccessTokenPublicJwk;
|
|
361
605
|
const cNonce = await jwsService.createJwsCompact(agentContext, {
|
|
362
|
-
key,
|
|
606
|
+
keyId: key.keyId,
|
|
363
607
|
payload: core_1.JwtPayload.fromJson({
|
|
364
608
|
iss: issuerMetadata.credentialIssuer.credential_issuer,
|
|
365
609
|
exp: (0, utils_1.dateToSeconds)(cNonceExpiresAt),
|
|
366
610
|
}),
|
|
367
611
|
protectedHeaderOptions: {
|
|
368
612
|
typ: 'credo+cnonce',
|
|
369
|
-
kid:
|
|
370
|
-
alg:
|
|
613
|
+
kid: key.keyId,
|
|
614
|
+
alg: key.signatureAlgorithm,
|
|
371
615
|
},
|
|
372
616
|
});
|
|
373
617
|
return {
|
|
@@ -384,8 +628,7 @@ let OpenId4VcIssuerService = class OpenId4VcIssuerService {
|
|
|
384
628
|
async verifyNonce(agentContext, issuer, cNonce) {
|
|
385
629
|
const issuerMetadata = await this.getIssuerMetadata(agentContext, issuer);
|
|
386
630
|
const jwsService = agentContext.dependencyManager.resolve(core_1.JwsService);
|
|
387
|
-
const key =
|
|
388
|
-
const jwk = (0, core_1.getJwkFromKey)(key);
|
|
631
|
+
const key = issuer.resolvedAccessTokenPublicJwk;
|
|
389
632
|
const jwt = core_1.Jwt.fromSerializedJwt(cNonce);
|
|
390
633
|
jwt.payload.validate();
|
|
391
634
|
if (jwt.payload.iss !== issuerMetadata.credentialIssuer.credential_issuer) {
|
|
@@ -396,17 +639,18 @@ let OpenId4VcIssuerService = class OpenId4VcIssuerService {
|
|
|
396
639
|
}
|
|
397
640
|
const verification = await jwsService.verifyJws(agentContext, {
|
|
398
641
|
jws: cNonce,
|
|
399
|
-
|
|
642
|
+
jwsSigner: {
|
|
643
|
+
method: 'jwk',
|
|
644
|
+
jwk: key,
|
|
645
|
+
},
|
|
400
646
|
});
|
|
401
|
-
if (!verification.
|
|
402
|
-
.map((singerKey) => singerKey.fingerprint)
|
|
403
|
-
.includes(issuer.accessTokenPublicKeyFingerprint)) {
|
|
647
|
+
if (!verification.isValid) {
|
|
404
648
|
throw new core_1.CredoError('Invalid nonce');
|
|
405
649
|
}
|
|
406
650
|
}
|
|
407
|
-
getIssuer(agentContext) {
|
|
651
|
+
getIssuer(agentContext, options = {}) {
|
|
408
652
|
return new openid4vci_1.Openid4vciIssuer({
|
|
409
|
-
callbacks: (0, callbacks_1.getOid4vcCallbacks)(agentContext),
|
|
653
|
+
callbacks: (0, callbacks_1.getOid4vcCallbacks)(agentContext, options),
|
|
410
654
|
});
|
|
411
655
|
}
|
|
412
656
|
getOauth2Client(agentContext) {
|
|
@@ -414,9 +658,9 @@ let OpenId4VcIssuerService = class OpenId4VcIssuerService {
|
|
|
414
658
|
callbacks: (0, callbacks_1.getOid4vcCallbacks)(agentContext),
|
|
415
659
|
});
|
|
416
660
|
}
|
|
417
|
-
getOauth2AuthorizationServer(agentContext) {
|
|
661
|
+
getOauth2AuthorizationServer(agentContext, options = {}) {
|
|
418
662
|
return new oauth2_1.Oauth2AuthorizationServer({
|
|
419
|
-
callbacks: (0, callbacks_1.getOid4vcCallbacks)(agentContext),
|
|
663
|
+
callbacks: (0, callbacks_1.getOid4vcCallbacks)(agentContext, options),
|
|
420
664
|
});
|
|
421
665
|
}
|
|
422
666
|
getResourceServer(agentContext, issuerRecord) {
|
|
@@ -449,6 +693,7 @@ let OpenId4VcIssuerService = class OpenId4VcIssuerService {
|
|
|
449
693
|
});
|
|
450
694
|
}
|
|
451
695
|
async getGrantsFromConfig(agentContext, config) {
|
|
696
|
+
const kms = agentContext.resolve(core_1.Kms.KeyManagementApi);
|
|
452
697
|
const { preAuthorizedCodeFlowConfig, authorizationCodeFlowConfig, issuerMetadata } = config;
|
|
453
698
|
// TOOD: export type
|
|
454
699
|
const grants = {};
|
|
@@ -456,7 +701,7 @@ let OpenId4VcIssuerService = class OpenId4VcIssuerService {
|
|
|
456
701
|
if (preAuthorizedCodeFlowConfig) {
|
|
457
702
|
const { txCode, authorizationServerUrl, preAuthorizedCode } = preAuthorizedCodeFlowConfig;
|
|
458
703
|
grants[oauth2_1.preAuthorizedCodeGrantIdentifier] = {
|
|
459
|
-
'pre-authorized_code': preAuthorizedCode ??
|
|
704
|
+
'pre-authorized_code': preAuthorizedCode ?? core_1.TypedArrayEncoder.toBase64URL(kms.randomBytes({ length: 32 })),
|
|
460
705
|
tx_code: txCode,
|
|
461
706
|
authorization_server: config.issuerMetadata.credentialIssuer.authorization_servers
|
|
462
707
|
? authorizationServerUrl
|
|
@@ -477,8 +722,7 @@ let OpenId4VcIssuerService = class OpenId4VcIssuerService {
|
|
|
477
722
|
issuer_state:
|
|
478
723
|
// TODO: the issuer_state should not be guessable, so it's best if we generate it and now allow the user to provide it?
|
|
479
724
|
// but same is true for the pre-auth code and users of credo can also provide that value. We can't easily do unique constraint with askat
|
|
480
|
-
authorizationCodeFlowConfig.issuerState ??
|
|
481
|
-
core_1.TypedArrayEncoder.toBase64URL(agentContext.wallet.getRandomValues(32)),
|
|
725
|
+
authorizationCodeFlowConfig.issuerState ?? core_1.TypedArrayEncoder.toBase64URL(kms.randomBytes({ length: 32 })),
|
|
482
726
|
authorization_server: config.issuerMetadata.credentialIssuer.authorization_servers
|
|
483
727
|
? authorizationServerUrl
|
|
484
728
|
: undefined,
|
|
@@ -486,38 +730,23 @@ let OpenId4VcIssuerService = class OpenId4VcIssuerService {
|
|
|
486
730
|
}
|
|
487
731
|
return grants;
|
|
488
732
|
}
|
|
489
|
-
async getHolderBindingFromRequestProofs(agentContext, proofSigners) {
|
|
490
|
-
const credentialHolderBindings = [];
|
|
491
|
-
for (const signer of proofSigners) {
|
|
492
|
-
if (signer.method === 'custom' || signer.method === 'x5c') {
|
|
493
|
-
throw new core_1.CredoError(`Only 'jwk' and 'did' based holder binding is supported`);
|
|
494
|
-
}
|
|
495
|
-
if (signer.method === 'jwk') {
|
|
496
|
-
const jwk = (0, core_1.getJwkFromJson)(signer.publicJwk);
|
|
497
|
-
credentialHolderBindings.push({
|
|
498
|
-
method: 'jwk',
|
|
499
|
-
jwk,
|
|
500
|
-
key: jwk.key,
|
|
501
|
-
});
|
|
502
|
-
}
|
|
503
|
-
if (signer.method === 'did') {
|
|
504
|
-
const key = await (0, utils_1.getKeyFromDid)(agentContext, signer.didUrl);
|
|
505
|
-
credentialHolderBindings.push({
|
|
506
|
-
method: 'did',
|
|
507
|
-
didUrl: signer.didUrl,
|
|
508
|
-
key,
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
return credentialHolderBindings;
|
|
513
|
-
}
|
|
514
733
|
getCredentialConfigurationsForRequest(options) {
|
|
515
|
-
const { requestFormat, issuanceSession, issuerMetadata, authorization } = options;
|
|
734
|
+
const { requestFormat, issuanceSession, issuerMetadata, authorization, credentialConfigurations } = options;
|
|
516
735
|
// Check against all credential configurations
|
|
517
|
-
const configurationsMatchingRequest =
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
736
|
+
const configurationsMatchingRequest = credentialConfigurations
|
|
737
|
+
? credentialConfigurations
|
|
738
|
+
: requestFormat
|
|
739
|
+
? (0, openid4vci_1.getCredentialConfigurationsMatchingRequestFormat)({
|
|
740
|
+
requestFormat,
|
|
741
|
+
credentialConfigurations: issuerMetadata.credentialIssuer.credential_configurations_supported,
|
|
742
|
+
})
|
|
743
|
+
: undefined;
|
|
744
|
+
if (!configurationsMatchingRequest) {
|
|
745
|
+
throw new oauth2_1.Oauth2ServerErrorResponseError({
|
|
746
|
+
error: oauth2_1.Oauth2ErrorCodes.InvalidCredentialRequest,
|
|
747
|
+
error_description: `Either 'credential_configuration_id' or 'format' needs to be defined'`,
|
|
748
|
+
});
|
|
749
|
+
}
|
|
521
750
|
if (Object.keys(configurationsMatchingRequest).length === 0) {
|
|
522
751
|
throw new oauth2_1.Oauth2ServerErrorResponseError({
|
|
523
752
|
error: oauth2_1.Oauth2ErrorCodes.InvalidCredentialRequest,
|
|
@@ -542,9 +771,12 @@ let OpenId4VcIssuerService = class OpenId4VcIssuerService {
|
|
|
542
771
|
}
|
|
543
772
|
// For pre-auth we allow all ids from the offer
|
|
544
773
|
if (authorization.accessToken.payload['pre-authorized_code']) {
|
|
774
|
+
// We return the first one that matches all checks. Pre draft 15 it could be multiple entries, but only if you offer
|
|
775
|
+
// multiple credentials of the same type. We need to do checks on this, so we pick the first one
|
|
776
|
+
const [credentialConfigurationId, credentialConfiguration] = Object.entries(configurationsMatchingRequestAndOfferNotIssued)[0];
|
|
545
777
|
return {
|
|
546
|
-
|
|
547
|
-
|
|
778
|
+
credentialConfigurationId,
|
|
779
|
+
credentialConfiguration,
|
|
548
780
|
};
|
|
549
781
|
}
|
|
550
782
|
// Limit to scopes from the token
|
|
@@ -558,20 +790,16 @@ let OpenId4VcIssuerService = class OpenId4VcIssuerService {
|
|
|
558
790
|
status: 403,
|
|
559
791
|
});
|
|
560
792
|
}
|
|
793
|
+
// We return the first one that matches all checks. Pre draft 15 it could be multiple entries, but only if you offer
|
|
794
|
+
// multiple credentials of the same type. We need to do checks on this, so we pick the first one
|
|
795
|
+
const [credentialConfigurationId, credentialConfiguration] = Object.entries(configurationsMatchingRequestOfferScope)[0];
|
|
561
796
|
return {
|
|
562
|
-
|
|
563
|
-
|
|
797
|
+
credentialConfigurationId,
|
|
798
|
+
credentialConfiguration: credentialConfiguration,
|
|
564
799
|
};
|
|
565
800
|
}
|
|
566
801
|
async getSignedCredentials(agentContext, options) {
|
|
567
|
-
const { issuanceSession,
|
|
568
|
-
const issuerMetadata = await this.getIssuerMetadata(agentContext, issuer);
|
|
569
|
-
const { credentialConfigurations, credentialConfigurationIds } = this.getCredentialConfigurationsForRequest({
|
|
570
|
-
issuanceSession,
|
|
571
|
-
issuerMetadata,
|
|
572
|
-
requestFormat,
|
|
573
|
-
authorization,
|
|
574
|
-
});
|
|
802
|
+
const { issuanceSession, credentialConfiguration, credentialConfigurationId, credentialRequestProofs } = options;
|
|
575
803
|
const mapper = options.credentialRequestToCredentialMapper ?? this.openId4VcIssuerConfig.credentialRequestToCredentialMapper;
|
|
576
804
|
let verification = undefined;
|
|
577
805
|
// NOTE: this will throw an error if the verifier module is not registered and there is a
|
|
@@ -596,28 +824,24 @@ let OpenId4VcIssuerService = class OpenId4VcIssuerService {
|
|
|
596
824
|
throw new core_1.CredoError(`Verified authorization response for verification session with id '${session.id}' does not have presenationExchange or dcql defined.`);
|
|
597
825
|
}
|
|
598
826
|
}
|
|
599
|
-
const holderBindings = await this.getHolderBindingFromRequestProofs(agentContext, options.proofSigners);
|
|
600
827
|
const signOptions = await mapper({
|
|
601
828
|
agentContext,
|
|
602
829
|
issuanceSession,
|
|
603
|
-
|
|
830
|
+
holderBinding: credentialRequestProofs,
|
|
604
831
|
credentialOffer: issuanceSession.credentialOfferPayload,
|
|
605
832
|
verification,
|
|
606
833
|
credentialRequest: options.credentialRequest,
|
|
607
834
|
credentialRequestFormat: options.requestFormat,
|
|
608
|
-
// Macthing credential configuration
|
|
609
|
-
|
|
610
|
-
|
|
835
|
+
// Macthing credential configuration
|
|
836
|
+
credentialConfiguration,
|
|
837
|
+
credentialConfigurationId,
|
|
611
838
|
// Authorization
|
|
612
839
|
authorization: options.authorization,
|
|
613
840
|
});
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
// needs a separate proof), but for now it needs to match
|
|
619
|
-
if (signOptions.credentials.length !== holderBindings.length) {
|
|
620
|
-
throw new core_1.CredoError(`Credential request to credential mapper returned '${signOptions.credentials.length}' to be signed, while only '${holderBindings.length}' holder binding entries were provided. Make sure to return one credential for each holder binding entry`);
|
|
841
|
+
const expectedLength = credentialRequestProofs.keys.length;
|
|
842
|
+
// NOTE: we may want to allow a mismatch between this (as there is a match batch length), but for now it needs to match
|
|
843
|
+
if (signOptions.credentials.length !== expectedLength) {
|
|
844
|
+
throw new core_1.CredoError(`Credential request to credential mapper returned '${signOptions.credentials.length}' to be signed, while '${expectedLength}' holder binding entries were provided. Make sure to return one credential for each holder binding entry`);
|
|
621
845
|
}
|
|
622
846
|
if (signOptions.format === core_1.ClaimFormat.JwtVc || signOptions.format === core_1.ClaimFormat.LdpVc) {
|
|
623
847
|
const oid4vciFormatMap = {
|
|
@@ -625,40 +849,44 @@ let OpenId4VcIssuerService = class OpenId4VcIssuerService {
|
|
|
625
849
|
[shared_1.OpenId4VciCredentialFormatProfile.JwtVcJsonLd]: core_1.ClaimFormat.JwtVc,
|
|
626
850
|
[shared_1.OpenId4VciCredentialFormatProfile.LdpVc]: core_1.ClaimFormat.LdpVc,
|
|
627
851
|
};
|
|
628
|
-
const expectedClaimFormat = oid4vciFormatMap[
|
|
852
|
+
const expectedClaimFormat = oid4vciFormatMap[credentialConfiguration.format];
|
|
629
853
|
if (signOptions.format !== expectedClaimFormat) {
|
|
630
854
|
throw new core_1.CredoError(`Invalid credential format returned by sign options. Expected '${expectedClaimFormat}', received '${signOptions.format}'.`);
|
|
631
855
|
}
|
|
632
856
|
return {
|
|
633
|
-
|
|
634
|
-
format: requestFormat.format,
|
|
857
|
+
format: credentialConfiguration.format,
|
|
635
858
|
credentials: (await Promise.all(signOptions.credentials.map((credential) => this.signW3cCredential(agentContext, signOptions.format, credential).then((signed) => signed.encoded)))),
|
|
636
859
|
};
|
|
637
860
|
}
|
|
638
861
|
if (signOptions.format === core_1.ClaimFormat.SdJwtVc) {
|
|
639
|
-
if (
|
|
640
|
-
|
|
862
|
+
if (credentialConfiguration.format !== shared_1.OpenId4VciCredentialFormatProfile.SdJwtVc &&
|
|
863
|
+
credentialConfiguration.format !== shared_1.OpenId4VciCredentialFormatProfile.SdJwtDc) {
|
|
864
|
+
throw new core_1.CredoError(`Invalid credential format returned by sign options. Expected '${core_1.ClaimFormat.SdJwtVc}', received '${signOptions.format}'.`);
|
|
641
865
|
}
|
|
642
|
-
if (!signOptions.credentials.every((c) => c.payload.vct ===
|
|
643
|
-
throw new core_1.CredoError(`One or more vct values of the offered credential(s) do not match the vct of the requested credential. Offered ${Array.from(new Set(signOptions.credentials.map((c) => `'${c.payload.vct}'`))).join(', ')} Requested '${
|
|
866
|
+
if (!signOptions.credentials.every((c) => c.payload.vct === credentialConfiguration.vct)) {
|
|
867
|
+
throw new core_1.CredoError(`One or more vct values of the offered credential(s) do not match the vct of the requested credential. Offered ${Array.from(new Set(signOptions.credentials.map((c) => `'${c.payload.vct}'`))).join(', ')} Requested '${credentialConfiguration.vct}'.`);
|
|
644
868
|
}
|
|
645
869
|
const sdJwtVcApi = agentContext.dependencyManager.resolve(core_1.SdJwtVcApi);
|
|
646
870
|
return {
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
871
|
+
format: credentialConfiguration.format,
|
|
872
|
+
credentials: await Promise.all(signOptions.credentials.map((credential) => sdJwtVcApi
|
|
873
|
+
.sign({
|
|
874
|
+
...credential,
|
|
875
|
+
// Set header type based on the oid4vci format
|
|
876
|
+
headerType: credentialConfiguration.format,
|
|
877
|
+
})
|
|
878
|
+
.then((signed) => signed.compact))),
|
|
650
879
|
};
|
|
651
880
|
}
|
|
652
881
|
if (signOptions.format === core_1.ClaimFormat.MsoMdoc) {
|
|
653
|
-
if (signOptions.format !==
|
|
654
|
-
throw new core_1.CredoError(`Invalid credential format returned by sign options. Expected '${
|
|
882
|
+
if (signOptions.format !== credentialConfiguration.format) {
|
|
883
|
+
throw new core_1.CredoError(`Invalid credential format returned by sign options. Expected '${credentialConfiguration.format}', received '${signOptions.format}'.`);
|
|
655
884
|
}
|
|
656
|
-
if (!signOptions.credentials.every((c) => c.docType ===
|
|
657
|
-
throw new core_1.CredoError(`One or more doctype values of the offered credential(s) do not match the doctype of the requested credential. Offered ${Array.from(new Set(signOptions.credentials.map((c) => `'${c.docType}'`))).join(', ')} Requested '${
|
|
885
|
+
if (!signOptions.credentials.every((c) => c.docType === credentialConfiguration.doctype)) {
|
|
886
|
+
throw new core_1.CredoError(`One or more doctype values of the offered credential(s) do not match the doctype of the requested credential. Offered ${Array.from(new Set(signOptions.credentials.map((c) => `'${c.docType}'`))).join(', ')} Requested '${credentialConfiguration.doctype}'.`);
|
|
658
887
|
}
|
|
659
888
|
const mdocApi = agentContext.dependencyManager.resolve(core_1.MdocApi);
|
|
660
889
|
return {
|
|
661
|
-
credentialConfigurationId: signOptions.credentialConfigurationId,
|
|
662
890
|
format: shared_1.OpenId4VciCredentialFormatProfile.MsoMdoc,
|
|
663
891
|
credentials: await Promise.all(signOptions.credentials.map((credential) => mdocApi.sign(credential).then((signed) => signed.base64Url))),
|
|
664
892
|
};
|
|
@@ -666,24 +894,16 @@ let OpenId4VcIssuerService = class OpenId4VcIssuerService {
|
|
|
666
894
|
throw new core_1.CredoError(`Unsupported credential format ${signOptions.format}`);
|
|
667
895
|
}
|
|
668
896
|
async signW3cCredential(agentContext, format, options) {
|
|
669
|
-
const
|
|
897
|
+
const publicJwk = await (0, utils_1.getPublicJwkFromDid)(agentContext, options.verificationMethod);
|
|
670
898
|
if (format === core_1.ClaimFormat.JwtVc) {
|
|
671
|
-
const supportedSignatureAlgorithms = (0, core_1.getJwkFromKey)(key).supportedSignatureAlgorithms;
|
|
672
|
-
if (supportedSignatureAlgorithms.length === 0) {
|
|
673
|
-
throw new core_1.CredoError(`No supported JWA signature algorithms found for key with keyType ${key.keyType}`);
|
|
674
|
-
}
|
|
675
|
-
const alg = supportedSignatureAlgorithms[0];
|
|
676
|
-
if (!alg) {
|
|
677
|
-
throw new core_1.CredoError(`No supported JWA signature algorithms for key type ${key.keyType}`);
|
|
678
|
-
}
|
|
679
899
|
return await this.w3cCredentialService.signCredential(agentContext, {
|
|
680
900
|
format: core_1.ClaimFormat.JwtVc,
|
|
681
901
|
credential: options.credential,
|
|
682
902
|
verificationMethod: options.verificationMethod,
|
|
683
|
-
alg,
|
|
903
|
+
alg: publicJwk.signatureAlgorithm,
|
|
684
904
|
});
|
|
685
905
|
}
|
|
686
|
-
const proofType = (0, utils_1.
|
|
906
|
+
const proofType = (0, utils_1.getProofTypeFromPublicJwk)(agentContext, publicJwk);
|
|
687
907
|
return await this.w3cCredentialService.signCredential(agentContext, {
|
|
688
908
|
format: core_1.ClaimFormat.LdpVc,
|
|
689
909
|
credential: options.credential,
|