@digitalbazaar/oid4-client 5.0.0 → 5.2.0
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/lib/OID4Client.js +6 -74
- package/lib/convert/index.js +349 -0
- package/lib/index.js +9 -4
- package/lib/oid4vci/credentialOffer.js +138 -0
- package/lib/oid4vci/discovery.js +126 -0
- package/lib/oid4vci/proofs.js +50 -0
- package/lib/{authorizationRequest.js → oid4vp/authorizationRequest.js} +4 -11
- package/lib/{authorizationResponse.js → oid4vp/authorizationResponse.js} +85 -44
- package/lib/{oid4vp.js → oid4vp/index.js} +3 -6
- package/lib/oid4vp/verifier.js +102 -0
- package/lib/{x509.js → oid4vp/x509.js} +1 -1
- package/lib/query/dcql.js +244 -0
- package/lib/query/index.js +18 -0
- package/lib/query/match.js +80 -0
- package/lib/query/presentationExchange.js +328 -0
- package/lib/query/queryByExample.js +8 -0
- package/lib/query/util.js +105 -0
- package/lib/util.js +27 -232
- package/package.json +5 -3
- package/lib/convert.js +0 -430
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2022-2025 Digital Bazaar, Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import {assert, fetchJSON} from '../util.js';
|
|
5
|
+
|
|
6
|
+
const WELL_KNOWN_REGEX = /\/\.well-known\/([^\/]+)/;
|
|
7
|
+
|
|
8
|
+
export async function discoverIssuer({issuerConfigUrl, agent} = {}) {
|
|
9
|
+
try {
|
|
10
|
+
assert(issuerConfigUrl, 'issuerConfigUrl', 'string');
|
|
11
|
+
|
|
12
|
+
const response = await fetchJSON({url: issuerConfigUrl, agent});
|
|
13
|
+
if(!response.data) {
|
|
14
|
+
const error = new Error('Issuer configuration format is not JSON.');
|
|
15
|
+
error.name = 'DataError';
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
const {data: issuerMetaData} = response;
|
|
19
|
+
const {issuer, authorization_server} = issuerMetaData;
|
|
20
|
+
|
|
21
|
+
if(authorization_server && authorization_server !== issuer) {
|
|
22
|
+
// not yet implemented
|
|
23
|
+
throw new Error('Separate authorization server not yet implemented.');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// validate `issuer`
|
|
27
|
+
if(!(typeof issuer === 'string' && issuer.startsWith('https://'))) {
|
|
28
|
+
const error = new Error('"issuer" is not an HTTPS URL.');
|
|
29
|
+
error.name = 'DataError';
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ensure `credential_issuer` matches `issuer`, if present
|
|
34
|
+
const {credential_issuer} = issuerMetaData;
|
|
35
|
+
if(credential_issuer !== undefined && credential_issuer !== issuer) {
|
|
36
|
+
const error = new Error('"credential_issuer" must match "issuer".');
|
|
37
|
+
error.name = 'DataError';
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Validate `issuer` value against `issuerConfigUrl` (per RFC 8414):
|
|
42
|
+
|
|
43
|
+
The `origin` and `path` element must be parsed from `issuer` and checked
|
|
44
|
+
against `issuerConfigUrl` like so:
|
|
45
|
+
|
|
46
|
+
For issuer `<origin>` (no path), `issuerConfigUrl` must match:
|
|
47
|
+
`<origin>/.well-known/<any-path-segment>`
|
|
48
|
+
|
|
49
|
+
For issuer `<origin><path>`, `issuerConfigUrl` must be:
|
|
50
|
+
`<origin>/.well-known/<any-path-segment><path>` */
|
|
51
|
+
const {pathname: wellKnownPath} = new URL(issuerConfigUrl);
|
|
52
|
+
const anyPathSegment = wellKnownPath.match(WELL_KNOWN_REGEX)[1];
|
|
53
|
+
const {origin, pathname} = new URL(issuer);
|
|
54
|
+
let expectedConfigUrl = `${origin}/.well-known/${anyPathSegment}`;
|
|
55
|
+
if(pathname !== '/') {
|
|
56
|
+
expectedConfigUrl += pathname;
|
|
57
|
+
}
|
|
58
|
+
if(issuerConfigUrl !== expectedConfigUrl) {
|
|
59
|
+
// alternatively, against RFC 8414, but according to OID4VCI, make sure
|
|
60
|
+
// the issuer config URL matches:
|
|
61
|
+
// <origin><path>/.well-known/<any-path-segment>
|
|
62
|
+
expectedConfigUrl = origin;
|
|
63
|
+
if(pathname !== '/') {
|
|
64
|
+
expectedConfigUrl += pathname;
|
|
65
|
+
}
|
|
66
|
+
expectedConfigUrl += `/.well-known/${anyPathSegment}`;
|
|
67
|
+
if(issuerConfigUrl !== expectedConfigUrl) {
|
|
68
|
+
const error = new Error('"issuer" does not match configuration URL.');
|
|
69
|
+
error.name = 'DataError';
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// fetch AS meta data
|
|
75
|
+
const asMetaDataUrl =
|
|
76
|
+
`${origin}/.well-known/oauth-authorization-server${pathname}`;
|
|
77
|
+
const asMetaDataResponse = await fetchJSON({url: asMetaDataUrl, agent});
|
|
78
|
+
if(!asMetaDataResponse.data) {
|
|
79
|
+
const error = new Error('Authorization server meta data is not JSON.');
|
|
80
|
+
error.name = 'DataError';
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const {data: asMetaData} = response;
|
|
85
|
+
// merge AS meta data into total issuer config
|
|
86
|
+
const issuerConfig = {...issuerMetaData, ...asMetaData};
|
|
87
|
+
|
|
88
|
+
// ensure `token_endpoint` is valid
|
|
89
|
+
const {token_endpoint} = asMetaData;
|
|
90
|
+
assert(token_endpoint, 'token_endpoint', 'string');
|
|
91
|
+
|
|
92
|
+
// return merged config and separate issuer and AS configs
|
|
93
|
+
const metadata = {issuer: issuerMetaData, authorizationServer: asMetaData};
|
|
94
|
+
return {issuerConfig, metadata};
|
|
95
|
+
} catch(cause) {
|
|
96
|
+
const error = new Error('Could not get OpenID issuer configuration.');
|
|
97
|
+
error.name = 'OperationError';
|
|
98
|
+
error.cause = cause;
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function robustDiscoverIssuer({issuer, agent} = {}) {
|
|
104
|
+
// try issuer config URLs based on OID4VCI (first) and RFC 8414 (second)
|
|
105
|
+
const parsedIssuer = new URL(issuer);
|
|
106
|
+
const {origin} = parsedIssuer;
|
|
107
|
+
const path = parsedIssuer.pathname === '/' ? '' : parsedIssuer.pathname;
|
|
108
|
+
|
|
109
|
+
const issuerConfigUrls = [
|
|
110
|
+
// OID4VCI
|
|
111
|
+
`${origin}${path}/.well-known/openid-credential-issuer`,
|
|
112
|
+
// RFC 8414
|
|
113
|
+
`${origin}/.well-known/openid-credential-issuer${path}`
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
let error;
|
|
117
|
+
for(const issuerConfigUrl of issuerConfigUrls) {
|
|
118
|
+
try {
|
|
119
|
+
const config = await discoverIssuer({issuerConfigUrl, agent});
|
|
120
|
+
return config;
|
|
121
|
+
} catch(e) {
|
|
122
|
+
error = e;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2022-2025 Digital Bazaar, Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import {signJWT} from '../util.js';
|
|
5
|
+
|
|
6
|
+
export async function generateDIDProofJWT({
|
|
7
|
+
signer, nonce, iss, aud, exp, nbf
|
|
8
|
+
} = {}) {
|
|
9
|
+
/* Example:
|
|
10
|
+
{
|
|
11
|
+
"alg": "ES256",
|
|
12
|
+
"kid":"did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1"
|
|
13
|
+
}.
|
|
14
|
+
{
|
|
15
|
+
"iss": "s6BhdRkqt3",
|
|
16
|
+
"aud": "https://server.example.com",
|
|
17
|
+
"iat": 1659145924,
|
|
18
|
+
"nonce": "tZignsnFbp"
|
|
19
|
+
}
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
if(exp === undefined) {
|
|
23
|
+
// default to 5 minute expiration time
|
|
24
|
+
exp = Math.floor(Date.now() / 1000) + 60 * 5;
|
|
25
|
+
}
|
|
26
|
+
if(nbf === undefined) {
|
|
27
|
+
// default to now
|
|
28
|
+
nbf = Math.floor(Date.now() / 1000);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const {id: kid} = signer;
|
|
32
|
+
const alg = _curveToAlg(signer.algorithm);
|
|
33
|
+
const payload = {nonce, iss, aud, exp, nbf};
|
|
34
|
+
const protectedHeader = {alg, kid};
|
|
35
|
+
|
|
36
|
+
return signJWT({payload, protectedHeader, signer});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function _curveToAlg(crv) {
|
|
40
|
+
if(crv === 'Ed25519' || crv === 'Ed448') {
|
|
41
|
+
return 'EdDSA';
|
|
42
|
+
}
|
|
43
|
+
if(crv?.startsWith('P-')) {
|
|
44
|
+
return `ES${crv.slice(2)}`;
|
|
45
|
+
}
|
|
46
|
+
if(crv === 'secp256k1') {
|
|
47
|
+
return 'ES256K';
|
|
48
|
+
}
|
|
49
|
+
return crv;
|
|
50
|
+
}
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Copyright (c) 2023-2025 Digital Bazaar, Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
4
|
import {
|
|
5
|
-
assert, assertOptional, base64Encode,
|
|
6
|
-
|
|
5
|
+
assert, assertOptional, base64Encode,
|
|
6
|
+
createNamedError, fetchJSON, selectJwk, sha256
|
|
7
|
+
} from '../util.js';
|
|
7
8
|
import {decodeJwt, importX509, jwtVerify} from 'jose';
|
|
8
9
|
import {
|
|
9
10
|
hasDomainSubjectAltName, parseCertificateChain, verifyCertificateChain
|
|
@@ -269,7 +270,7 @@ async function _checkClientIdSchemeRequirements({
|
|
|
269
270
|
and it includes the client ID. */
|
|
270
271
|
} else if(clientIdScheme === 'x509_hash') {
|
|
271
272
|
// `x509_hash:<base64url sha256-hash of DER leaf cert>`
|
|
272
|
-
const hash = base64Encode(await
|
|
273
|
+
const hash = base64Encode(await sha256(chain[0].toBER()));
|
|
273
274
|
if(clientId !== hash) {
|
|
274
275
|
throw createNamedError({
|
|
275
276
|
message:
|
|
@@ -444,14 +445,6 @@ function _parseOID4VPUrl({url}) {
|
|
|
444
445
|
return {authorizationRequest};
|
|
445
446
|
}
|
|
446
447
|
|
|
447
|
-
async function _sha256(data) {
|
|
448
|
-
if(typeof data === 'string') {
|
|
449
|
-
data = new TextEncoder().encode(data);
|
|
450
|
-
}
|
|
451
|
-
const algorithm = {name: 'SHA-256'};
|
|
452
|
-
return new Uint8Array(await crypto.subtle.digest(algorithm, data));
|
|
453
|
-
}
|
|
454
|
-
|
|
455
448
|
function _throwKeyNotFound(protectedHeader) {
|
|
456
449
|
const error = new Error(
|
|
457
450
|
'Could not verify signed authorization request; ' +
|
|
@@ -1,68 +1,109 @@
|
|
|
1
1
|
/*!
|
|
2
2
|
* Copyright (c) 2023-2025 Digital Bazaar, Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
|
-
import {createNamedError, selectJwk} from '
|
|
4
|
+
import {createNamedError, selectJwk} from '../util.js';
|
|
5
5
|
import {EncryptJWT} from 'jose';
|
|
6
6
|
import {httpClient} from '@digitalbazaar/http-client';
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
7
|
+
import {pathsToVerifiableCredentialPointers} from '../convert/index.js';
|
|
8
|
+
import {resolvePointer} from '../query/util.js';
|
|
9
9
|
|
|
10
10
|
const TEXT_ENCODER = new TextEncoder();
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
// creates an authorization response without sending it; use `send()` to create
|
|
13
|
+
// and send one at once
|
|
14
|
+
export async function create({
|
|
13
15
|
verifiablePresentation,
|
|
14
16
|
presentationSubmission,
|
|
15
17
|
authorizationRequest,
|
|
16
18
|
vpToken,
|
|
17
|
-
encryptionOptions = {}
|
|
18
|
-
agent
|
|
19
|
+
encryptionOptions = {}
|
|
19
20
|
} = {}) {
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
if(!(verifiablePresentation || vpToken)) {
|
|
22
|
+
throw createNamedError({
|
|
23
|
+
message: 'One of "verifiablePresentation" or "vpToken" must be given.',
|
|
24
|
+
name: 'DataError'
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
// if no `vpToken` given, use VP
|
|
28
|
+
vpToken = vpToken ?? JSON.stringify(verifiablePresentation);
|
|
29
|
+
|
|
30
|
+
// if no `presentationSubmission` provided, auto-generate one
|
|
31
|
+
let generatedPresentationSubmission = false;
|
|
32
|
+
if(!presentationSubmission) {
|
|
33
|
+
({presentationSubmission} = createPresentationSubmission({
|
|
34
|
+
presentationDefinition: authorizationRequest.presentation_definition,
|
|
35
|
+
verifiablePresentation
|
|
36
|
+
}));
|
|
37
|
+
generatedPresentationSubmission = true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// prepare response body
|
|
41
|
+
const body = {};
|
|
42
|
+
|
|
43
|
+
// if `authorizationRequest.response_mode` is `direct.jwt` generate a JWT
|
|
44
|
+
if(authorizationRequest.response_mode === 'direct_post.jwt') {
|
|
45
|
+
if(submitsFormat({presentationSubmission, format: 'mso_mdoc'}) &&
|
|
46
|
+
!encryptionOptions?.mdl?.sessionTranscript) {
|
|
22
47
|
throw createNamedError({
|
|
23
|
-
message: '
|
|
48
|
+
message: '"encryptionOptions.mdl.sessionTranscript" is required ' +
|
|
49
|
+
'when submitting an mDL presentation.',
|
|
24
50
|
name: 'DataError'
|
|
25
51
|
});
|
|
26
52
|
}
|
|
27
|
-
// if no `vpToken` given, use VP
|
|
28
|
-
vpToken = vpToken ?? JSON.stringify(verifiablePresentation);
|
|
29
|
-
|
|
30
|
-
// if no `presentationSubmission` provided, auto-generate one
|
|
31
|
-
let generatedPresentationSubmission = false;
|
|
32
|
-
if(!presentationSubmission) {
|
|
33
|
-
({presentationSubmission} = createPresentationSubmission({
|
|
34
|
-
presentationDefinition: authorizationRequest.presentation_definition,
|
|
35
|
-
verifiablePresentation
|
|
36
|
-
}));
|
|
37
|
-
generatedPresentationSubmission = true;
|
|
38
|
-
}
|
|
39
53
|
|
|
40
|
-
|
|
41
|
-
|
|
54
|
+
const jwt = await _encrypt({
|
|
55
|
+
vpToken, presentationSubmission, authorizationRequest,
|
|
56
|
+
encryptionOptions
|
|
57
|
+
});
|
|
58
|
+
body.response = jwt;
|
|
59
|
+
} else {
|
|
60
|
+
// include vp token and presentation submittion directly in body
|
|
61
|
+
body.vp_token = vpToken;
|
|
62
|
+
body.presentation_submission = JSON.stringify(presentationSubmission);
|
|
63
|
+
}
|
|
42
64
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
name: 'DataError'
|
|
51
|
-
});
|
|
52
|
-
}
|
|
65
|
+
const authorizationResponse = body;
|
|
66
|
+
if(generatedPresentationSubmission) {
|
|
67
|
+
// return any generated presentation submission
|
|
68
|
+
return {authorizationResponse, presentationSubmission};
|
|
69
|
+
}
|
|
70
|
+
return {authorizationResponse};
|
|
71
|
+
}
|
|
53
72
|
|
|
54
|
-
|
|
55
|
-
|
|
73
|
+
export async function send({
|
|
74
|
+
verifiablePresentation,
|
|
75
|
+
presentationSubmission,
|
|
76
|
+
authorizationRequest,
|
|
77
|
+
vpToken,
|
|
78
|
+
encryptionOptions = {},
|
|
79
|
+
authorizationResponse,
|
|
80
|
+
agent
|
|
81
|
+
} = {}) {
|
|
82
|
+
try {
|
|
83
|
+
// create `authorizationResponse` if not passed
|
|
84
|
+
let generatedPresentationSubmission;
|
|
85
|
+
if(!authorizationResponse) {
|
|
86
|
+
({
|
|
87
|
+
authorizationResponse,
|
|
88
|
+
presentationSubmission: generatedPresentationSubmission
|
|
89
|
+
} = await create({
|
|
90
|
+
verifiablePresentation,
|
|
91
|
+
presentationSubmission,
|
|
92
|
+
authorizationRequest,
|
|
93
|
+
vpToken,
|
|
56
94
|
encryptionOptions
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
'
|
|
95
|
+
}));
|
|
96
|
+
} else if(verifiablePresentation || presentationSubmission || vpToken ||
|
|
97
|
+
encryptionOptions) {
|
|
98
|
+
throw new TypeError(
|
|
99
|
+
'Only "authorizationResponse" or its components ( ' +
|
|
100
|
+
'"verifiablePresentation", "presentationSubmission", "vpToken", ' +
|
|
101
|
+
'"encryptionOptions") can be passed, but not both.');
|
|
64
102
|
}
|
|
65
103
|
|
|
104
|
+
// prepare response body
|
|
105
|
+
const body = new URLSearchParams(authorizationResponse);
|
|
106
|
+
|
|
66
107
|
// send response
|
|
67
108
|
const response = await httpClient.post(authorizationRequest.response_uri, {
|
|
68
109
|
agent, body, headers: {accept: 'application/json'},
|
|
@@ -75,7 +116,7 @@ export async function send({
|
|
|
75
116
|
const result = response.data || {};
|
|
76
117
|
if(generatedPresentationSubmission) {
|
|
77
118
|
// return any generated presentation submission
|
|
78
|
-
return {result, presentationSubmission};
|
|
119
|
+
return {result, presentationSubmission: generatedPresentationSubmission};
|
|
79
120
|
}
|
|
80
121
|
return {result};
|
|
81
122
|
} catch(cause) {
|
|
@@ -299,7 +340,7 @@ function _matchesInputDescriptor({
|
|
|
299
340
|
|
|
300
341
|
// check for a value at at least one path
|
|
301
342
|
for(const pointer of pointers) {
|
|
302
|
-
const existing =
|
|
343
|
+
const existing = resolvePointer(verifiableCredential, pointer);
|
|
303
344
|
if(existing === undefined) {
|
|
304
345
|
// VC does not match
|
|
305
346
|
return false;
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export * as authzRequest from './authorizationRequest.js';
|
|
5
5
|
export * as authzResponse from './authorizationResponse.js';
|
|
6
|
-
export * as convert from '
|
|
6
|
+
export * as convert from '../convert/index.js';
|
|
7
|
+
export * as verifier from './verifier.js';
|
|
7
8
|
|
|
8
9
|
// backwards compatibility APIs
|
|
9
10
|
export {
|
|
@@ -13,11 +14,7 @@ export {
|
|
|
13
14
|
createPresentationSubmission,
|
|
14
15
|
send as sendAuthorizationResponse
|
|
15
16
|
} from './authorizationResponse.js';
|
|
16
|
-
export {
|
|
17
|
-
fromVpr, toVpr,
|
|
18
|
-
// exported for testing purposes only
|
|
19
|
-
_fromQueryByExampleQuery
|
|
20
|
-
} from './convert.js';
|
|
17
|
+
export {fromVpr, toVpr} from '../convert/index.js';
|
|
21
18
|
|
|
22
19
|
// Note: for examples of presentation request and responses, see:
|
|
23
20
|
// eslint-disable-next-line max-len
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2023-2025 Digital Bazaar, Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import {createNamedError, parseJSON, selectJwk} from '../util.js';
|
|
5
|
+
import {importJWK, jwtDecrypt} from 'jose';
|
|
6
|
+
|
|
7
|
+
// parses (and decrypts) an authz response from a response body object
|
|
8
|
+
export async function parseAuthorizationResponse({
|
|
9
|
+
body = {},
|
|
10
|
+
supportedResponseModes = ['direct_post.jwt', 'direct_post'],
|
|
11
|
+
getDecryptParameters
|
|
12
|
+
}) {
|
|
13
|
+
let responseMode;
|
|
14
|
+
const parsed = {};
|
|
15
|
+
let payload;
|
|
16
|
+
let protectedHeader;
|
|
17
|
+
|
|
18
|
+
supportedResponseModes = new Set(supportedResponseModes);
|
|
19
|
+
|
|
20
|
+
if(body.response) {
|
|
21
|
+
// `body.response` is present which must contain an encrypted JWT
|
|
22
|
+
responseMode = 'direct_post.jwt';
|
|
23
|
+
_assertSupportedResponseMode({responseMode, supportedResponseModes});
|
|
24
|
+
const jwt = body.response;
|
|
25
|
+
({
|
|
26
|
+
payload,
|
|
27
|
+
protectedHeader
|
|
28
|
+
} = await _decrypt({jwt, getDecryptParameters}));
|
|
29
|
+
parsed.presentationSubmission = payload.presentation_submission;
|
|
30
|
+
} else {
|
|
31
|
+
responseMode = 'direct_post';
|
|
32
|
+
_assertSupportedResponseMode({responseMode, supportedResponseModes});
|
|
33
|
+
payload = body;
|
|
34
|
+
parsed.presentationSubmission = parseJSON(
|
|
35
|
+
payload.presentation_submission, 'presentation_submission');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// `vp_token` is either:
|
|
39
|
+
// 1. a JSON object (a VP)
|
|
40
|
+
// 2. a JSON array (of something)
|
|
41
|
+
// 3. a JSON string (a quoted JWT: "<JWT>")
|
|
42
|
+
// 4. a JWT
|
|
43
|
+
// 5. a base64url-encoded mDL device response
|
|
44
|
+
// 6. unknown
|
|
45
|
+
const {vp_token} = payload;
|
|
46
|
+
if(typeof vp_token === 'string' &&
|
|
47
|
+
(vp_token.startsWith('{') || vp_token.startsWith('[') ||
|
|
48
|
+
vp_token.startsWith('"'))) {
|
|
49
|
+
parsed.vpToken = parseJSON(vp_token, 'vp_token');
|
|
50
|
+
} else {
|
|
51
|
+
parsed.vpToken = vp_token;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {responseMode, parsed, payload, protectedHeader};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function _assertSupportedResponseMode({
|
|
58
|
+
responseMode, supportedResponseModes
|
|
59
|
+
}) {
|
|
60
|
+
if(!supportedResponseModes.has(responseMode)) {
|
|
61
|
+
throw createNamedError({
|
|
62
|
+
message: `Unsupported response mode "${responseMode}".`,
|
|
63
|
+
name: 'NotSupportedError'
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function _decrypt({jwt, getDecryptParameters}) {
|
|
69
|
+
if(typeof getDecryptParameters !== 'function') {
|
|
70
|
+
throw new TypeError(
|
|
71
|
+
'"getDecryptParameters" is required for "direct_post.jwt" ' +
|
|
72
|
+
'response mode.');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const params = await getDecryptParameters({jwt});
|
|
76
|
+
const {keys} = params;
|
|
77
|
+
let {getKey} = params;
|
|
78
|
+
if(!getKey) {
|
|
79
|
+
// note: `jose` lib's JWK key set feature cannot be used and passed to
|
|
80
|
+
// `jwtDecrypt()` as the second parameter because the expected `alg`
|
|
81
|
+
// "ECDH-ES" is not a unsupported algorithm for selecting a key from a set
|
|
82
|
+
getKey = protectedHeader => {
|
|
83
|
+
if(protectedHeader.alg !== 'ECDH-ES') {
|
|
84
|
+
const error = createNamedError({
|
|
85
|
+
message: `Unsupported algorithm "${protectedHeader.alg}"; ` +
|
|
86
|
+
'algorithm must be "ECDH-ES".',
|
|
87
|
+
name: 'NotSupportedError',
|
|
88
|
+
details: {httpStatusCode: 400, public: true}
|
|
89
|
+
});
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
const jwk = selectJwk({keys, kid: protectedHeader.kid});
|
|
93
|
+
return importJWK(jwk, 'ECDH-ES');
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return jwtDecrypt(jwt, getKey, {
|
|
98
|
+
// only supported algorithms at this time:
|
|
99
|
+
contentEncryptionAlgorithms: ['A256GCM'],
|
|
100
|
+
keyManagementAlgorithms: ['ECDH-ES']
|
|
101
|
+
});
|
|
102
|
+
}
|