@digitalbazaar/oid4-client 5.1.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} +4 -4
- package/lib/{oid4vp.js → oid4vp/index.js} +2 -6
- package/lib/{verifier.js → oid4vp/verifier.js} +3 -16
- 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 +22 -230
- package/package.json +2 -2
- package/lib/convert.js +0 -430
package/lib/util.js
CHANGED
|
@@ -6,14 +6,15 @@ import {httpClient} from '@digitalbazaar/http-client';
|
|
|
6
6
|
|
|
7
7
|
const TEXT_ENCODER = new TextEncoder();
|
|
8
8
|
const ENCODED_PERIOD = TEXT_ENCODER.encode('.');
|
|
9
|
-
const WELL_KNOWN_REGEX = /\/\.well-known\/([^\/]+)/;
|
|
10
9
|
|
|
11
10
|
export function assert(x, name, type, optional = false) {
|
|
12
11
|
const article = type === 'object' ? 'an' : 'a';
|
|
13
|
-
|
|
12
|
+
const xType = typeof type === 'string' ?
|
|
13
|
+
typeof x : (x instanceof type && type);
|
|
14
|
+
if(x !== undefined && xType !== type) {
|
|
14
15
|
throw new TypeError(
|
|
15
16
|
`${optional ? 'When present, ' : ''} ` +
|
|
16
|
-
`"${name}" must be ${article} ${type}.`);
|
|
17
|
+
`"${name}" must be ${article} ${type?.name ?? type}.`);
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
|
|
@@ -46,101 +47,6 @@ export function createNamedError({message, name, details, cause} = {}) {
|
|
|
46
47
|
return error;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
export async function discoverIssuer({issuerConfigUrl, agent} = {}) {
|
|
50
|
-
try {
|
|
51
|
-
assert(issuerConfigUrl, 'issuerConfigUrl', 'string');
|
|
52
|
-
|
|
53
|
-
const response = await fetchJSON({url: issuerConfigUrl, agent});
|
|
54
|
-
if(!response.data) {
|
|
55
|
-
const error = new Error('Issuer configuration format is not JSON.');
|
|
56
|
-
error.name = 'DataError';
|
|
57
|
-
throw error;
|
|
58
|
-
}
|
|
59
|
-
const {data: issuerMetaData} = response;
|
|
60
|
-
const {issuer, authorization_server} = issuerMetaData;
|
|
61
|
-
|
|
62
|
-
if(authorization_server && authorization_server !== issuer) {
|
|
63
|
-
// not yet implemented
|
|
64
|
-
throw new Error('Separate authorization server not yet implemented.');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// validate `issuer`
|
|
68
|
-
if(!(typeof issuer === 'string' && issuer.startsWith('https://'))) {
|
|
69
|
-
const error = new Error('"issuer" is not an HTTPS URL.');
|
|
70
|
-
error.name = 'DataError';
|
|
71
|
-
throw error;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// ensure `credential_issuer` matches `issuer`, if present
|
|
75
|
-
const {credential_issuer} = issuerMetaData;
|
|
76
|
-
if(credential_issuer !== undefined && credential_issuer !== issuer) {
|
|
77
|
-
const error = new Error('"credential_issuer" must match "issuer".');
|
|
78
|
-
error.name = 'DataError';
|
|
79
|
-
throw error;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/* Validate `issuer` value against `issuerConfigUrl` (per RFC 8414):
|
|
83
|
-
|
|
84
|
-
The `origin` and `path` element must be parsed from `issuer` and checked
|
|
85
|
-
against `issuerConfigUrl` like so:
|
|
86
|
-
|
|
87
|
-
For issuer `<origin>` (no path), `issuerConfigUrl` must match:
|
|
88
|
-
`<origin>/.well-known/<any-path-segment>`
|
|
89
|
-
|
|
90
|
-
For issuer `<origin><path>`, `issuerConfigUrl` must be:
|
|
91
|
-
`<origin>/.well-known/<any-path-segment><path>` */
|
|
92
|
-
const {pathname: wellKnownPath} = new URL(issuerConfigUrl);
|
|
93
|
-
const anyPathSegment = wellKnownPath.match(WELL_KNOWN_REGEX)[1];
|
|
94
|
-
const {origin, pathname} = new URL(issuer);
|
|
95
|
-
let expectedConfigUrl = `${origin}/.well-known/${anyPathSegment}`;
|
|
96
|
-
if(pathname !== '/') {
|
|
97
|
-
expectedConfigUrl += pathname;
|
|
98
|
-
}
|
|
99
|
-
if(issuerConfigUrl !== expectedConfigUrl) {
|
|
100
|
-
// alternatively, against RFC 8414, but according to OID4VCI, make sure
|
|
101
|
-
// the issuer config URL matches:
|
|
102
|
-
// <origin><path>/.well-known/<any-path-segment>
|
|
103
|
-
expectedConfigUrl = origin;
|
|
104
|
-
if(pathname !== '/') {
|
|
105
|
-
expectedConfigUrl += pathname;
|
|
106
|
-
}
|
|
107
|
-
expectedConfigUrl += `/.well-known/${anyPathSegment}`;
|
|
108
|
-
if(issuerConfigUrl !== expectedConfigUrl) {
|
|
109
|
-
const error = new Error('"issuer" does not match configuration URL.');
|
|
110
|
-
error.name = 'DataError';
|
|
111
|
-
throw error;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// fetch AS meta data
|
|
116
|
-
const asMetaDataUrl =
|
|
117
|
-
`${origin}/.well-known/oauth-authorization-server${pathname}`;
|
|
118
|
-
const asMetaDataResponse = await fetchJSON({url: asMetaDataUrl, agent});
|
|
119
|
-
if(!asMetaDataResponse.data) {
|
|
120
|
-
const error = new Error('Authorization server meta data is not JSON.');
|
|
121
|
-
error.name = 'DataError';
|
|
122
|
-
throw error;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const {data: asMetaData} = response;
|
|
126
|
-
// merge AS meta data into total issuer config
|
|
127
|
-
const issuerConfig = {...issuerMetaData, ...asMetaData};
|
|
128
|
-
|
|
129
|
-
// ensure `token_endpoint` is valid
|
|
130
|
-
const {token_endpoint} = asMetaData;
|
|
131
|
-
assert(token_endpoint, 'token_endpoint', 'string');
|
|
132
|
-
|
|
133
|
-
// return merged config and separate issuer and AS configs
|
|
134
|
-
const metadata = {issuer: issuerMetaData, authorizationServer: asMetaData};
|
|
135
|
-
return {issuerConfig, metadata};
|
|
136
|
-
} catch(cause) {
|
|
137
|
-
const error = new Error('Could not get OpenID issuer configuration.');
|
|
138
|
-
error.name = 'OperationError';
|
|
139
|
-
error.cause = cause;
|
|
140
|
-
throw error;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
50
|
export function fetchJSON({url, agent} = {}) {
|
|
145
51
|
// allow these params to be passed / configured
|
|
146
52
|
const fetchOptions = {
|
|
@@ -154,126 +60,17 @@ export function fetchJSON({url, agent} = {}) {
|
|
|
154
60
|
return httpClient.get(url, fetchOptions);
|
|
155
61
|
}
|
|
156
62
|
|
|
157
|
-
export
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
"aud": "https://server.example.com",
|
|
168
|
-
"iat": 1659145924,
|
|
169
|
-
"nonce": "tZignsnFbp"
|
|
170
|
-
}
|
|
171
|
-
*/
|
|
172
|
-
|
|
173
|
-
if(exp === undefined) {
|
|
174
|
-
// default to 5 minute expiration time
|
|
175
|
-
exp = Math.floor(Date.now() / 1000) + 60 * 5;
|
|
176
|
-
}
|
|
177
|
-
if(nbf === undefined) {
|
|
178
|
-
// default to now
|
|
179
|
-
nbf = Math.floor(Date.now() / 1000);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const {id: kid} = signer;
|
|
183
|
-
const alg = _curveToAlg(signer.algorithm);
|
|
184
|
-
const payload = {nonce, iss, aud, exp, nbf};
|
|
185
|
-
const protectedHeader = {alg, kid};
|
|
186
|
-
|
|
187
|
-
return signJWT({payload, protectedHeader, signer});
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
export async function getCredentialOffer({url, agent} = {}) {
|
|
191
|
-
const {protocol, searchParams} = new URL(url);
|
|
192
|
-
if(protocol !== 'openid-credential-offer:') {
|
|
193
|
-
throw new SyntaxError(
|
|
194
|
-
'"url" must express a URL with the ' +
|
|
195
|
-
'"openid-credential-offer" protocol.');
|
|
196
|
-
}
|
|
197
|
-
const offer = searchParams.get('credential_offer');
|
|
198
|
-
if(offer) {
|
|
199
|
-
return JSON.parse(offer);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// try to fetch offer from URL
|
|
203
|
-
const offerUrl = searchParams.get('credential_offer_uri');
|
|
204
|
-
if(!offerUrl) {
|
|
205
|
-
throw new SyntaxError(
|
|
206
|
-
'OID4VCI credential offer must have "credential_offer" or ' +
|
|
207
|
-
'"credential_offer_uri".');
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if(!offerUrl.startsWith('https://')) {
|
|
211
|
-
const error = new Error(
|
|
212
|
-
`"credential_offer_uri" (${offerUrl}) must start with "https://".`);
|
|
213
|
-
error.name = 'NotSupportedError';
|
|
214
|
-
throw error;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const response = await fetchJSON({url: offerUrl, agent});
|
|
218
|
-
if(!response.data) {
|
|
219
|
-
const error = new Error(
|
|
220
|
-
`Credential offer fetched from "${offerUrl}" is not JSON.`);
|
|
221
|
-
error.name = 'DataError';
|
|
222
|
-
throw error;
|
|
223
|
-
}
|
|
224
|
-
return response.data;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
export function parseCredentialOfferUrl({url} = {}) {
|
|
228
|
-
assert(url, 'url', 'string');
|
|
229
|
-
|
|
230
|
-
/* Parse URL, e.g.:
|
|
231
|
-
|
|
232
|
-
'openid-credential-offer://?' +
|
|
233
|
-
'credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2F' +
|
|
234
|
-
'localhost%3A18443%2Fexchangers%2Fz19t8xb568tNRD1zVm9R5diXR%2F' +
|
|
235
|
-
'exchanges%2Fz1ADs3ur2s9tm6JUW6CnTiyn3%22%2C%22credentials' +
|
|
236
|
-
'%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22credential_definition' +
|
|
237
|
-
'%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww.w3.org%2F2018%2F' +
|
|
238
|
-
'credentials%2Fv1%22%2C%22https%3A%2F%2Fwww.w3.org%2F2018%2F' +
|
|
239
|
-
'credentials%2Fexamples%2Fv1%22%5D%2C%22type%22%3A%5B%22' +
|
|
240
|
-
'VerifiableCredential%22%2C%22UniversityDegreeCredential' +
|
|
241
|
-
'%22%5D%7D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams' +
|
|
242
|
-
'%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22' +
|
|
243
|
-
'pre-authorized_code%22%3A%22z1AEvnk2cqeRM1Mfv75vzHSUo%22%7D%7D%7D';
|
|
244
|
-
*/
|
|
245
|
-
const {protocol, searchParams} = new URL(url);
|
|
246
|
-
if(protocol !== 'openid-credential-offer:') {
|
|
247
|
-
throw new SyntaxError(
|
|
248
|
-
'"url" must express a URL with the ' +
|
|
249
|
-
'"openid-credential-offer" protocol.');
|
|
250
|
-
}
|
|
251
|
-
return JSON.parse(searchParams.get('credential_offer'));
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
export async function robustDiscoverIssuer({issuer, agent} = {}) {
|
|
255
|
-
// try issuer config URLs based on OID4VCI (first) and RFC 8414 (second)
|
|
256
|
-
const parsedIssuer = new URL(issuer);
|
|
257
|
-
const {origin} = parsedIssuer;
|
|
258
|
-
const path = parsedIssuer.pathname === '/' ? '' : parsedIssuer.pathname;
|
|
259
|
-
|
|
260
|
-
const issuerConfigUrls = [
|
|
261
|
-
// OID4VCI
|
|
262
|
-
`${origin}${path}/.well-known/openid-credential-issuer`,
|
|
263
|
-
// RFC 8414
|
|
264
|
-
`${origin}/.well-known/openid-credential-issuer${path}`
|
|
265
|
-
];
|
|
266
|
-
|
|
267
|
-
let error;
|
|
268
|
-
for(const issuerConfigUrl of issuerConfigUrls) {
|
|
269
|
-
try {
|
|
270
|
-
const config = await discoverIssuer({issuerConfigUrl, agent});
|
|
271
|
-
return config;
|
|
272
|
-
} catch(e) {
|
|
273
|
-
error = e;
|
|
274
|
-
}
|
|
63
|
+
export function parseJSON(x, name) {
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(x);
|
|
66
|
+
} catch(cause) {
|
|
67
|
+
throw createNamedError({
|
|
68
|
+
message: `Could not parse "${name}".`,
|
|
69
|
+
name: 'DataError',
|
|
70
|
+
details: {httpStatusCode: 400, public: true},
|
|
71
|
+
cause
|
|
72
|
+
});
|
|
275
73
|
}
|
|
276
|
-
throw error;
|
|
277
74
|
}
|
|
278
75
|
|
|
279
76
|
export function selectJwk({keys, kid, alg, kty, crv, use} = {}) {
|
|
@@ -319,6 +116,14 @@ export function selectJwk({keys, kid, alg, kty, crv, use} = {}) {
|
|
|
319
116
|
});
|
|
320
117
|
}
|
|
321
118
|
|
|
119
|
+
export async function sha256(data) {
|
|
120
|
+
if(typeof data === 'string') {
|
|
121
|
+
data = new TextEncoder().encode(data);
|
|
122
|
+
}
|
|
123
|
+
const algorithm = {name: 'SHA-256'};
|
|
124
|
+
return new Uint8Array(await crypto.subtle.digest(algorithm, data));
|
|
125
|
+
}
|
|
126
|
+
|
|
322
127
|
export async function signJWT({payload, protectedHeader, signer} = {}) {
|
|
323
128
|
// encode payload and protected header
|
|
324
129
|
const b64Payload = base64url.encode(JSON.stringify(payload));
|
|
@@ -346,16 +151,3 @@ export async function signJWT({payload, protectedHeader, signer} = {}) {
|
|
|
346
151
|
// create compact JWT
|
|
347
152
|
return `${jws.protected}.${jws.payload}.${jws.signature}`;
|
|
348
153
|
}
|
|
349
|
-
|
|
350
|
-
function _curveToAlg(crv) {
|
|
351
|
-
if(crv === 'Ed25519' || crv === 'Ed448') {
|
|
352
|
-
return 'EdDSA';
|
|
353
|
-
}
|
|
354
|
-
if(crv?.startsWith('P-')) {
|
|
355
|
-
return `ES${crv.slice(2)}`;
|
|
356
|
-
}
|
|
357
|
-
if(crv === 'secp256k1') {
|
|
358
|
-
return 'ES256K';
|
|
359
|
-
}
|
|
360
|
-
return crv;
|
|
361
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@digitalbazaar/oid4-client",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.0",
|
|
4
4
|
"description": "An OID4 (VC + VP) client",
|
|
5
5
|
"homepage": "https://github.com/digitalbazaar/oid4-client",
|
|
6
6
|
"author": {
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"@digitalbazaar/http-client": "^4.0.0",
|
|
27
27
|
"base64url-universal": "^2.0.0",
|
|
28
28
|
"jose": "^6.1.0",
|
|
29
|
+
"json-pointer": "^0.6.2",
|
|
29
30
|
"jsonpath-plus": "^10.3.0",
|
|
30
|
-
"jsonpointer": "^5.0.1",
|
|
31
31
|
"pkijs": "^3.2.5"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|