@govuk-one-login/mobile-wallet-mdoc-builder 0.1.0 → 0.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/dist/index.cjs +559 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +36 -7
- package/dist/index.d.ts +36 -7
- package/dist/index.js +536 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1,4 +1,30 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
24
|
+
let zod = require("zod");
|
|
25
|
+
let node_crypto = require("node:crypto");
|
|
26
|
+
node_crypto = __toESM(node_crypto, 1);
|
|
27
|
+
let cbor2 = require("cbor2");
|
|
2
28
|
//#region src/types/dateFormat.ts
|
|
3
29
|
/**
|
|
4
30
|
* Specifies the date encoding format for a data element.
|
|
@@ -19,13 +45,527 @@ let DateFormat = /* @__PURE__ */ function(DateFormat) {
|
|
|
19
45
|
/**
|
|
20
46
|
* Error thrown by the mdoc builder when construction or signing fails.
|
|
21
47
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
48
|
+
* When the failure is caused by input validation, `violations` holds the
|
|
49
|
+
* complete set of {@link ValidationError} entries which callers can inspect.
|
|
24
50
|
*/
|
|
25
51
|
var MdocBuilderError = class extends Error {
|
|
52
|
+
/** The validation violations that caused the failure, if any. */
|
|
53
|
+
violations;
|
|
26
54
|
constructor(message, options) {
|
|
27
55
|
super(message, options);
|
|
28
56
|
this.name = "MdocBuilderError";
|
|
57
|
+
if (options?.violations !== void 0) this.violations = options.violations;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/validation/constants.ts
|
|
62
|
+
const ELEMENT_VALUE_MAX_BYTES = 1572864;
|
|
63
|
+
const DEVICE_KEY_MAX_BYTES = 2048;
|
|
64
|
+
const CERTIFICATE_MAX_BYTES = 8192;
|
|
65
|
+
const STATUS_LIST_IDX_MAX = 4294967295;
|
|
66
|
+
const DOCUMENT_TYPE_MAX_LENGTH = 128;
|
|
67
|
+
const IDENTIFIER_MAX_LENGTH = 256;
|
|
68
|
+
const STRING_VALUE_MAX_LENGTH = 150;
|
|
69
|
+
const COLLECTION_MAX_ENTRIES = 256;
|
|
70
|
+
const VALIDATION_LIMITS = {
|
|
71
|
+
documentType: {
|
|
72
|
+
minLength: 1,
|
|
73
|
+
maxLength: DOCUMENT_TYPE_MAX_LENGTH
|
|
74
|
+
},
|
|
75
|
+
nameSpaces: {
|
|
76
|
+
namespaceKey: {
|
|
77
|
+
minLength: 1,
|
|
78
|
+
maxLength: IDENTIFIER_MAX_LENGTH
|
|
79
|
+
},
|
|
80
|
+
minDataElements: 1,
|
|
81
|
+
maxDataElements: COLLECTION_MAX_ENTRIES,
|
|
82
|
+
elementIdentifier: {
|
|
83
|
+
minLength: 1,
|
|
84
|
+
maxLength: IDENTIFIER_MAX_LENGTH
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
elementValue: {
|
|
88
|
+
string: {
|
|
89
|
+
minLength: 1,
|
|
90
|
+
maxLength: STRING_VALUE_MAX_LENGTH
|
|
91
|
+
},
|
|
92
|
+
number: {
|
|
93
|
+
min: Number.MIN_SAFE_INTEGER,
|
|
94
|
+
max: Number.MAX_SAFE_INTEGER
|
|
95
|
+
},
|
|
96
|
+
uint8Array: {
|
|
97
|
+
minByteLength: 1,
|
|
98
|
+
maxByteLength: ELEMENT_VALUE_MAX_BYTES
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
collections: {
|
|
102
|
+
minLength: 1,
|
|
103
|
+
maxLength: COLLECTION_MAX_ENTRIES
|
|
104
|
+
},
|
|
105
|
+
deviceKey: {
|
|
106
|
+
minByteLength: 1,
|
|
107
|
+
maxByteLength: DEVICE_KEY_MAX_BYTES
|
|
108
|
+
},
|
|
109
|
+
statusList: {
|
|
110
|
+
idx: {
|
|
111
|
+
min: 0,
|
|
112
|
+
max: STATUS_LIST_IDX_MAX
|
|
113
|
+
},
|
|
114
|
+
uri: { maxLength: 2048 }
|
|
115
|
+
},
|
|
116
|
+
certificateChain: {
|
|
117
|
+
minLength: 1,
|
|
118
|
+
entry: {
|
|
119
|
+
minByteLength: 1,
|
|
120
|
+
maxByteLength: CERTIFICATE_MAX_BYTES
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
//#endregion
|
|
125
|
+
//#region src/validation/helpers/latin1.ts
|
|
126
|
+
const LATIN1_PATTERN = /^[\u0000-\u00FF]*$/;
|
|
127
|
+
const LATIN1_MESSAGE = "must contain only Latin1 (ISO/IEC 8859-1) characters";
|
|
128
|
+
function isLatin1(value) {
|
|
129
|
+
return LATIN1_PATTERN.test(value);
|
|
130
|
+
}
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/validation/topLevelCredentialFields.ts
|
|
133
|
+
const { documentType, deviceKey, statusList, certificateChain } = VALIDATION_LIMITS;
|
|
134
|
+
function byteLengthSchema(minByteLength, maxByteLength) {
|
|
135
|
+
return zod.z.instanceof(Uint8Array).refine((value) => value.length >= minByteLength, { message: "must not be empty" }).refine((value) => value.length <= maxByteLength, { message: `must not exceed ${maxByteLength.toString()} bytes` });
|
|
136
|
+
}
|
|
137
|
+
const documentTypeSchema = zod.z.string().min(documentType.minLength).max(documentType.maxLength).refine(isLatin1, { message: LATIN1_MESSAGE });
|
|
138
|
+
const deviceKeySchema = byteLengthSchema(deviceKey.minByteLength, deviceKey.maxByteLength);
|
|
139
|
+
const statusListSchema = zod.z.object({
|
|
140
|
+
idx: zod.z.number().int().min(statusList.idx.min).max(statusList.idx.max),
|
|
141
|
+
uri: zod.z.url().max(statusList.uri.maxLength)
|
|
142
|
+
}).strict();
|
|
143
|
+
const certificateChainSchema = zod.z.array(byteLengthSchema(certificateChain.entry.minByteLength, certificateChain.entry.maxByteLength)).min(certificateChain.minLength);
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region src/validation/primitives.ts
|
|
146
|
+
const { string, number, uint8Array } = VALIDATION_LIMITS.elementValue;
|
|
147
|
+
const stringValueSchema = zod.z.string().min(string.minLength).max(string.maxLength).refine(isLatin1, { message: LATIN1_MESSAGE });
|
|
148
|
+
const numberValueSchema = zod.z.number().min(number.min).max(number.max);
|
|
149
|
+
const booleanValueSchema = zod.z.boolean();
|
|
150
|
+
const dateValueSchema = zod.z.date();
|
|
151
|
+
const uint8ArrayValueSchema = zod.z.instanceof(Uint8Array).refine((value) => value.length >= uint8Array.minByteLength, { message: "must not be empty" }).refine((value) => value.length <= uint8Array.maxByteLength, { message: `must not exceed ${uint8Array.maxByteLength.toString()} bytes` });
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region src/validation/dataElementValue.ts
|
|
154
|
+
const { minLength, maxLength } = VALIDATION_LIMITS.collections;
|
|
155
|
+
const latin1KeySchema = zod.z.string().refine(isLatin1, { message: LATIN1_MESSAGE });
|
|
156
|
+
const primitiveValueSchema = zod.z.union([
|
|
157
|
+
stringValueSchema,
|
|
158
|
+
numberValueSchema,
|
|
159
|
+
booleanValueSchema,
|
|
160
|
+
dateValueSchema,
|
|
161
|
+
uint8ArrayValueSchema
|
|
162
|
+
]);
|
|
163
|
+
function primitiveTypeTag(value) {
|
|
164
|
+
if (value instanceof Uint8Array) return "uint8Array";
|
|
165
|
+
if (value instanceof Date) return "date";
|
|
166
|
+
if (typeof value === "string") return "string";
|
|
167
|
+
if (typeof value === "number") return "number";
|
|
168
|
+
return "boolean";
|
|
169
|
+
}
|
|
170
|
+
function isHomogeneous(values) {
|
|
171
|
+
let tag;
|
|
172
|
+
for (const value of values) {
|
|
173
|
+
const current = primitiveTypeTag(value);
|
|
174
|
+
tag ??= current;
|
|
175
|
+
if (current !== tag) return false;
|
|
176
|
+
}
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
const HOMOGENEITY_MESSAGE = "all values must be the same primitive type";
|
|
180
|
+
const primitiveArraySchema = zod.z.array(primitiveValueSchema).min(minLength).max(maxLength).refine(isHomogeneous, { message: HOMOGENEITY_MESSAGE });
|
|
181
|
+
const primitiveMapSchema = zod.z.map(latin1KeySchema, primitiveValueSchema).refine((map) => map.size >= minLength, { message: "must not be empty" }).refine((map) => map.size <= maxLength, { message: `must not exceed ${maxLength.toString()} entries` }).refine((map) => isHomogeneous(map.values()), { message: HOMOGENEITY_MESSAGE });
|
|
182
|
+
const primitiveMapArraySchema = zod.z.array(zod.z.map(latin1KeySchema, primitiveValueSchema)).min(minLength).max(maxLength).refine((maps) => maps.every((map) => map.size >= minLength), { message: "each map must not be empty" }).refine((maps) => maps.every((map) => map.size <= maxLength), { message: `each map must not exceed ${maxLength.toString()} entries` }).refine((maps) => isHomogeneous(maps.flatMap((map) => [...map.values()])), { message: HOMOGENEITY_MESSAGE });
|
|
183
|
+
const dataElementValueSchema = zod.z.union([
|
|
184
|
+
primitiveMapArraySchema,
|
|
185
|
+
primitiveMapSchema,
|
|
186
|
+
primitiveArraySchema,
|
|
187
|
+
primitiveValueSchema
|
|
188
|
+
]);
|
|
189
|
+
//#endregion
|
|
190
|
+
//#region src/validation/nameSpaces.ts
|
|
191
|
+
const { namespaceKey, elementIdentifier, minDataElements, maxDataElements } = VALIDATION_LIMITS.nameSpaces;
|
|
192
|
+
function isDateTyped(value) {
|
|
193
|
+
if (value instanceof Date) return true;
|
|
194
|
+
if (value instanceof Map) return [...value.values()].some((v) => v instanceof Date);
|
|
195
|
+
if (Array.isArray(value)) return value.some(isDateTyped);
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
const namespaceKeySchema = zod.z.string().min(namespaceKey.minLength).max(namespaceKey.maxLength).refine(isLatin1, { message: LATIN1_MESSAGE });
|
|
199
|
+
const dataElementSchema = zod.z.object({
|
|
200
|
+
elementIdentifier: zod.z.string().min(elementIdentifier.minLength).max(elementIdentifier.maxLength).refine(isLatin1, { message: LATIN1_MESSAGE }),
|
|
201
|
+
elementValue: dataElementValueSchema,
|
|
202
|
+
dateFormat: zod.z.enum(DateFormat).optional()
|
|
203
|
+
}).superRefine((element, ctx) => {
|
|
204
|
+
if (!isDateTyped(element.elementValue) && element.dateFormat !== void 0) ctx.addIssue({
|
|
205
|
+
code: "custom",
|
|
206
|
+
message: "dateFormat must not be provided when elementValue is not date-typed",
|
|
207
|
+
path: ["dateFormat"]
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
const nameSpacesSchema = zod.z.map(namespaceKeySchema, zod.z.array(dataElementSchema).min(minDataElements).max(maxDataElements)).refine((nameSpaces) => nameSpaces.size >= 1, { message: "must not be empty" });
|
|
211
|
+
//#endregion
|
|
212
|
+
//#region src/validation/credentialValidity.ts
|
|
213
|
+
function credentialValiditySchema(now) {
|
|
214
|
+
return zod.z.object({
|
|
215
|
+
earliestValidFrom: zod.z.date().optional(),
|
|
216
|
+
validUntil: zod.z.date(),
|
|
217
|
+
expectedUpdate: zod.z.date().optional()
|
|
218
|
+
}).superRefine((validity, ctx) => {
|
|
219
|
+
if (validity.validUntil.getTime() <= now.getTime()) ctx.addIssue({
|
|
220
|
+
code: "custom",
|
|
221
|
+
message: "must be after the current time",
|
|
222
|
+
path: ["validUntil"]
|
|
223
|
+
});
|
|
224
|
+
if (validity.earliestValidFrom !== void 0 && validity.earliestValidFrom.getTime() >= validity.validUntil.getTime()) ctx.addIssue({
|
|
225
|
+
code: "custom",
|
|
226
|
+
message: "must be before validUntil",
|
|
227
|
+
path: ["earliestValidFrom"]
|
|
228
|
+
});
|
|
229
|
+
if (validity.expectedUpdate !== void 0 && validity.expectedUpdate.getTime() > validity.validUntil.getTime()) ctx.addIssue({
|
|
230
|
+
code: "custom",
|
|
231
|
+
message: "must be before or equal to validUntil",
|
|
232
|
+
path: ["expectedUpdate"]
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
//#endregion
|
|
237
|
+
//#region src/validation/errors.ts
|
|
238
|
+
function formatFieldPath(path) {
|
|
239
|
+
return path.reduce((acc, segment) => {
|
|
240
|
+
if (typeof segment === "number") return `${acc}[${segment.toString()}]`;
|
|
241
|
+
const key = String(segment);
|
|
242
|
+
return acc === "" ? key : `${acc}.${key}`;
|
|
243
|
+
}, "");
|
|
244
|
+
}
|
|
245
|
+
function mapZodErrorToValidationErrors(error) {
|
|
246
|
+
return error.issues.flatMap((issue) => {
|
|
247
|
+
if (issue.code === "unrecognized_keys") return issue.keys.map((key) => ({
|
|
248
|
+
field: formatFieldPath([...issue.path, key]),
|
|
249
|
+
message: issue.message
|
|
250
|
+
}));
|
|
251
|
+
return {
|
|
252
|
+
field: formatFieldPath(issue.path),
|
|
253
|
+
message: issue.message
|
|
254
|
+
};
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
//#endregion
|
|
258
|
+
//#region src/validation/validateMdocBuilderInput.ts
|
|
259
|
+
function mdocBuilderInputSchema(now) {
|
|
260
|
+
return zod.z.object({
|
|
261
|
+
documentType: documentTypeSchema,
|
|
262
|
+
nameSpaces: nameSpacesSchema,
|
|
263
|
+
deviceKey: deviceKeySchema,
|
|
264
|
+
credentialValidity: credentialValiditySchema(now),
|
|
265
|
+
statusList: statusListSchema,
|
|
266
|
+
certificateChain: certificateChainSchema
|
|
267
|
+
}).strict();
|
|
268
|
+
}
|
|
269
|
+
function validateMdocBuilderInput(input) {
|
|
270
|
+
const result = mdocBuilderInputSchema(/* @__PURE__ */ new Date()).safeParse(input);
|
|
271
|
+
return result.success ? [] : mapZodErrorToValidationErrors(result.error);
|
|
272
|
+
}
|
|
273
|
+
//#endregion
|
|
274
|
+
//#region src/deviceKey/helpers/padCoordinate.ts
|
|
275
|
+
const P256_COORDINATE_LENGTH = 32;
|
|
276
|
+
function decodeAndPadCoordinate(base64url) {
|
|
277
|
+
return padCoordinate(new Uint8Array(Buffer.from(base64url, "base64url")));
|
|
278
|
+
}
|
|
279
|
+
function padCoordinate(coordinate) {
|
|
280
|
+
if (coordinate.length === P256_COORDINATE_LENGTH) return new Uint8Array(coordinate);
|
|
281
|
+
const padded = new Uint8Array(P256_COORDINATE_LENGTH);
|
|
282
|
+
padded.set(coordinate, P256_COORDINATE_LENGTH - coordinate.length);
|
|
283
|
+
return padded;
|
|
284
|
+
}
|
|
285
|
+
//#endregion
|
|
286
|
+
//#region src/deviceKey/deviceKey.ts
|
|
287
|
+
const COSE_KEY_LABEL = {
|
|
288
|
+
/** Key Type (kty) */
|
|
289
|
+
KTY: 1,
|
|
290
|
+
/** EC Curve (crv) */
|
|
291
|
+
CRV: -1,
|
|
292
|
+
/** x-coordinate */
|
|
293
|
+
X: -2,
|
|
294
|
+
/** y-coordinate */
|
|
295
|
+
Y: -3
|
|
296
|
+
};
|
|
297
|
+
const COSE_KEY_VALUE = {
|
|
298
|
+
/** Key type: Elliptic Curve (EC2) */
|
|
299
|
+
KTY_EC2: 2,
|
|
300
|
+
/** Curve: P-256 */
|
|
301
|
+
CRV_P256: 1
|
|
302
|
+
};
|
|
303
|
+
const DEVICE_KEY_INFO_KEY = {
|
|
304
|
+
DEVICE_KEY: "deviceKey",
|
|
305
|
+
KEY_AUTHORIZATIONS: "keyAuthorizations",
|
|
306
|
+
NAME_SPACES: "nameSpaces"
|
|
307
|
+
};
|
|
308
|
+
function buildDeviceKeyInfo(spkiBytes, namespaceNames) {
|
|
309
|
+
const jwk = importSpki(spkiBytes).export({ format: "jwk" });
|
|
310
|
+
if (jwk.kty !== "EC") throw new MdocBuilderError(`Unsupported key type: ${String(jwk.kty)}. Only EC keys are supported.`);
|
|
311
|
+
if (jwk.crv !== "P-256") throw new MdocBuilderError(`Unsupported curve: ${String(jwk.crv)}. Only P-256 is supported.`);
|
|
312
|
+
const coseKey = buildCoseKey(jwk);
|
|
313
|
+
const deviceKeyInfo = /* @__PURE__ */ new Map();
|
|
314
|
+
deviceKeyInfo.set(DEVICE_KEY_INFO_KEY.DEVICE_KEY, coseKey);
|
|
315
|
+
if (namespaceNames.length > 0) {
|
|
316
|
+
const keyAuthorizations = /* @__PURE__ */ new Map();
|
|
317
|
+
keyAuthorizations.set(DEVICE_KEY_INFO_KEY.NAME_SPACES, namespaceNames);
|
|
318
|
+
deviceKeyInfo.set(DEVICE_KEY_INFO_KEY.KEY_AUTHORIZATIONS, keyAuthorizations);
|
|
319
|
+
}
|
|
320
|
+
return deviceKeyInfo;
|
|
321
|
+
}
|
|
322
|
+
function buildCoseKey(jwk) {
|
|
323
|
+
const x = decodeAndPadCoordinate(jwk.x);
|
|
324
|
+
const y = decodeAndPadCoordinate(jwk.y);
|
|
325
|
+
const coseKey = /* @__PURE__ */ new Map();
|
|
326
|
+
coseKey.set(COSE_KEY_LABEL.KTY, COSE_KEY_VALUE.KTY_EC2);
|
|
327
|
+
coseKey.set(COSE_KEY_LABEL.CRV, COSE_KEY_VALUE.CRV_P256);
|
|
328
|
+
coseKey.set(COSE_KEY_LABEL.X, x);
|
|
329
|
+
coseKey.set(COSE_KEY_LABEL.Y, y);
|
|
330
|
+
return coseKey;
|
|
331
|
+
}
|
|
332
|
+
function importSpki(spkiBytes) {
|
|
333
|
+
try {
|
|
334
|
+
return node_crypto.default.createPublicKey({
|
|
335
|
+
key: Buffer.from(spkiBytes),
|
|
336
|
+
format: "der",
|
|
337
|
+
type: "spki"
|
|
338
|
+
});
|
|
339
|
+
} catch (error) {
|
|
340
|
+
const message = error.message;
|
|
341
|
+
throw new MdocBuilderError(`Failed to import SPKI key: ${message}`, { cause: error });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
//#endregion
|
|
345
|
+
//#region src/cbor/cbor.ts
|
|
346
|
+
var TaggedValue = class {
|
|
347
|
+
tagNumber;
|
|
348
|
+
contents;
|
|
349
|
+
constructor(tagNumber, contents) {
|
|
350
|
+
this.tagNumber = tagNumber;
|
|
351
|
+
this.contents = contents;
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
const CBOR_TAG = {
|
|
355
|
+
/** Tag 0: Standard date/time string () */
|
|
356
|
+
TDATE: 0,
|
|
357
|
+
/** Tag 24: Encoded CBOR data item (bstr-wrapped CBOR) */
|
|
358
|
+
EMBEDDED_CBOR: 24,
|
|
359
|
+
/** Tag 1004: Full-date string (YYYY-MM-DD) */
|
|
360
|
+
FULL_DATE: 1004
|
|
361
|
+
};
|
|
362
|
+
function tdate(date) {
|
|
363
|
+
const iso = date.toISOString().replace(/\.\d{3}/, "");
|
|
364
|
+
return new TaggedValue(CBOR_TAG.TDATE, iso);
|
|
365
|
+
}
|
|
366
|
+
function fullDate(date) {
|
|
367
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) throw new MdocBuilderError(`fullDate requires a "YYYY-MM-DD" string, received: "${date}"`);
|
|
368
|
+
return new TaggedValue(CBOR_TAG.FULL_DATE, date);
|
|
369
|
+
}
|
|
370
|
+
function embeddedCbor(bytes) {
|
|
371
|
+
return new TaggedValue(CBOR_TAG.EMBEDDED_CBOR, bytes);
|
|
372
|
+
}
|
|
373
|
+
function encode(value) {
|
|
374
|
+
const prepared = convertTags(value);
|
|
375
|
+
return (0, cbor2.encode)(prepared);
|
|
376
|
+
}
|
|
377
|
+
function convertTags(value) {
|
|
378
|
+
if (value instanceof TaggedValue) return new cbor2.Tag(value.tagNumber, convertTags(value.contents));
|
|
379
|
+
if (value instanceof Map) {
|
|
380
|
+
const prepared = /* @__PURE__ */ new Map();
|
|
381
|
+
for (const [k, v] of value) prepared.set(convertTags(k), convertTags(v));
|
|
382
|
+
return prepared;
|
|
383
|
+
}
|
|
384
|
+
if (Array.isArray(value)) return value.map(convertTags);
|
|
385
|
+
return value;
|
|
386
|
+
}
|
|
387
|
+
//#endregion
|
|
388
|
+
//#region src/issuerSigned/buildSingleItem.ts
|
|
389
|
+
const SALT_LENGTH_BYTES = 16;
|
|
390
|
+
function generateDigestId(existingIds) {
|
|
391
|
+
let id;
|
|
392
|
+
do
|
|
393
|
+
id = (0, node_crypto.randomBytes)(4).readUInt32BE(0) >>> 1;
|
|
394
|
+
while (existingIds.has(id));
|
|
395
|
+
return id;
|
|
396
|
+
}
|
|
397
|
+
function encodeElementValue(element) {
|
|
398
|
+
const { elementValue, dateFormat } = element;
|
|
399
|
+
if (elementValue instanceof Date) return encodeDateValue(elementValue, dateFormat);
|
|
400
|
+
if (elementValue instanceof Map) return encodeDateMap(elementValue, dateFormat);
|
|
401
|
+
if (Array.isArray(elementValue)) return elementValue.map((item) => {
|
|
402
|
+
if (item instanceof Date) return encodeDateValue(item, dateFormat);
|
|
403
|
+
if (item instanceof Map) return encodeDateMap(item, dateFormat);
|
|
404
|
+
return item;
|
|
405
|
+
});
|
|
406
|
+
return elementValue;
|
|
407
|
+
}
|
|
408
|
+
function encodeDateMap(map, dateFormat) {
|
|
409
|
+
const result = /* @__PURE__ */ new Map();
|
|
410
|
+
for (const [key, value] of map) result.set(key, value instanceof Date ? encodeDateValue(value, dateFormat) : value);
|
|
411
|
+
return result;
|
|
412
|
+
}
|
|
413
|
+
function encodeDateValue(date, dateFormat) {
|
|
414
|
+
if (dateFormat === 0) return fullDate(`${date.getUTCFullYear().toString().padStart(4, "0")}-${(date.getUTCMonth() + 1).toString().padStart(2, "0")}-${date.getUTCDate().toString().padStart(2, "0")}`);
|
|
415
|
+
return tdate(date);
|
|
416
|
+
}
|
|
417
|
+
function buildSingleItem(element, usedIds) {
|
|
418
|
+
const digestId = generateDigestId(usedIds);
|
|
419
|
+
usedIds.add(digestId);
|
|
420
|
+
const salt = new Uint8Array((0, node_crypto.randomBytes)(SALT_LENGTH_BYTES));
|
|
421
|
+
return {
|
|
422
|
+
digestId,
|
|
423
|
+
tag24Bytes: encode(embeddedCbor(encode(/* @__PURE__ */ new Map([
|
|
424
|
+
["digestID", digestId],
|
|
425
|
+
["random", salt],
|
|
426
|
+
["elementIdentifier", element.elementIdentifier],
|
|
427
|
+
["elementValue", encodeElementValue(element)]
|
|
428
|
+
]))))
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
//#endregion
|
|
432
|
+
//#region src/issuerSigned/helpers/digest.ts
|
|
433
|
+
async function digestItem(tag24Bytes) {
|
|
434
|
+
const buffer = await crypto.subtle.digest("SHA-256", tag24Bytes);
|
|
435
|
+
return new Uint8Array(buffer);
|
|
436
|
+
}
|
|
437
|
+
//#endregion
|
|
438
|
+
//#region src/issuerSigned/issuerSignedItem.ts
|
|
439
|
+
async function buildIssuerSignedItems(nameSpaces) {
|
|
440
|
+
const issuerSignedItemBytes = /* @__PURE__ */ new Map();
|
|
441
|
+
const valueDigests = /* @__PURE__ */ new Map();
|
|
442
|
+
const namespaceResults = await Promise.all([...nameSpaces].map(async ([namespace, elements]) => {
|
|
443
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
444
|
+
const items = elements.map((element) => buildSingleItem(element, usedIds));
|
|
445
|
+
const digestEntries = await Promise.all(items.map(async ({ digestId, tag24Bytes }) => [digestId, await digestItem(tag24Bytes)]));
|
|
446
|
+
return {
|
|
447
|
+
namespace,
|
|
448
|
+
itemBytes: items.map(({ tag24Bytes }) => tag24Bytes),
|
|
449
|
+
digests: new Map(digestEntries)
|
|
450
|
+
};
|
|
451
|
+
}));
|
|
452
|
+
for (const { namespace, itemBytes, digests } of namespaceResults) {
|
|
453
|
+
issuerSignedItemBytes.set(namespace, itemBytes);
|
|
454
|
+
valueDigests.set(namespace, digests);
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
issuerSignedItemBytes,
|
|
458
|
+
valueDigests
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
//#endregion
|
|
462
|
+
//#region src/issuerSigned/assembleIssuerSigned.ts
|
|
463
|
+
function assembleIssuerSigned(nameSpaces, issuerAuth) {
|
|
464
|
+
const nameSpacesMap = /* @__PURE__ */ new Map();
|
|
465
|
+
for (const [namespace, items] of nameSpaces) nameSpacesMap.set(namespace, items.map((itemBytes) => embeddedCbor(itemBytes)));
|
|
466
|
+
const issuerSigned = /* @__PURE__ */ new Map();
|
|
467
|
+
issuerSigned.set("nameSpaces", nameSpacesMap);
|
|
468
|
+
issuerSigned.set("issuerAuth", issuerAuth);
|
|
469
|
+
return encode(issuerSigned);
|
|
470
|
+
}
|
|
471
|
+
//#endregion
|
|
472
|
+
//#region src/validityInfo/buildValidityInfo.ts
|
|
473
|
+
function buildValidityInfo(input) {
|
|
474
|
+
const signed = /* @__PURE__ */ new Date();
|
|
475
|
+
const validityInfo = {
|
|
476
|
+
signed,
|
|
477
|
+
validFrom: input.earliestValidFrom && input.earliestValidFrom > signed ? input.earliestValidFrom : signed,
|
|
478
|
+
validUntil: input.validUntil
|
|
479
|
+
};
|
|
480
|
+
if (input.expectedUpdate !== void 0) validityInfo.expectedUpdate = input.expectedUpdate;
|
|
481
|
+
return validityInfo;
|
|
482
|
+
}
|
|
483
|
+
//#endregion
|
|
484
|
+
//#region src/mso/buildMso.ts
|
|
485
|
+
function buildMso(input) {
|
|
486
|
+
const validityInfoMap = /* @__PURE__ */ new Map();
|
|
487
|
+
validityInfoMap.set("signed", tdate(input.validityInfo.signed));
|
|
488
|
+
validityInfoMap.set("validFrom", tdate(input.validityInfo.validFrom));
|
|
489
|
+
validityInfoMap.set("validUntil", tdate(input.validityInfo.validUntil));
|
|
490
|
+
if (input.validityInfo.expectedUpdate !== void 0) validityInfoMap.set("expectedUpdate", tdate(input.validityInfo.expectedUpdate));
|
|
491
|
+
const statusListMap = /* @__PURE__ */ new Map();
|
|
492
|
+
statusListMap.set("idx", input.statusList.idx);
|
|
493
|
+
statusListMap.set("uri", input.statusList.uri);
|
|
494
|
+
const statusMap = /* @__PURE__ */ new Map();
|
|
495
|
+
statusMap.set("status_list", statusListMap);
|
|
496
|
+
const mso = /* @__PURE__ */ new Map();
|
|
497
|
+
mso.set("version", "1.0");
|
|
498
|
+
mso.set("digestAlgorithm", "SHA-256");
|
|
499
|
+
mso.set("valueDigests", input.valueDigests);
|
|
500
|
+
mso.set("deviceKeyInfo", input.deviceKeyInfo);
|
|
501
|
+
mso.set("validityInfo", validityInfoMap);
|
|
502
|
+
mso.set("status", statusMap);
|
|
503
|
+
mso.set("docType", input.docType);
|
|
504
|
+
return encode(embeddedCbor(encode(mso)));
|
|
505
|
+
}
|
|
506
|
+
//#endregion
|
|
507
|
+
//#region src/issuerAuth/buildProtectedHeader.ts
|
|
508
|
+
const COSE_HEADER_ALG = 1;
|
|
509
|
+
const COSE_ALG_ES256 = -7;
|
|
510
|
+
function buildProtectedHeader() {
|
|
511
|
+
return encode(/* @__PURE__ */ new Map([[COSE_HEADER_ALG, COSE_ALG_ES256]]));
|
|
512
|
+
}
|
|
513
|
+
//#endregion
|
|
514
|
+
//#region src/issuerAuth/buildUnprotectedHeader.ts
|
|
515
|
+
const COSE_HEADER_X5CHAIN = 33;
|
|
516
|
+
function buildUnprotectedHeader(certificateChain) {
|
|
517
|
+
const [signingCertificate] = certificateChain;
|
|
518
|
+
return /* @__PURE__ */ new Map([[COSE_HEADER_X5CHAIN, signingCertificate]]);
|
|
519
|
+
}
|
|
520
|
+
//#endregion
|
|
521
|
+
//#region src/issuerAuth/buildToBeSigned.ts
|
|
522
|
+
const SIG_STRUCTURE_CONTEXT = "Signature1";
|
|
523
|
+
const EXTERNAL_AAD = /* @__PURE__ */ new Uint8Array(0);
|
|
524
|
+
function buildToBeSigned(protectedHeaderBytes, payloadBytes) {
|
|
525
|
+
return encode([
|
|
526
|
+
SIG_STRUCTURE_CONTEXT,
|
|
527
|
+
protectedHeaderBytes,
|
|
528
|
+
EXTERNAL_AAD,
|
|
529
|
+
payloadBytes
|
|
530
|
+
]);
|
|
531
|
+
}
|
|
532
|
+
//#endregion
|
|
533
|
+
//#region src/issuerAuth/assembleIssuerAuth.ts
|
|
534
|
+
const EXPECTED_SIGNATURE_LENGTH = 64;
|
|
535
|
+
async function assembleIssuerAuth(msoBytes, certificateChain, sign) {
|
|
536
|
+
const protectedHeader = buildProtectedHeader();
|
|
537
|
+
const unprotectedHeader = buildUnprotectedHeader(certificateChain);
|
|
538
|
+
const toBeSigned = buildToBeSigned(protectedHeader, msoBytes);
|
|
539
|
+
let signature;
|
|
540
|
+
try {
|
|
541
|
+
signature = await sign(toBeSigned);
|
|
542
|
+
} catch (error) {
|
|
543
|
+
throw new MdocBuilderError("Signing function threw an error", { cause: error });
|
|
544
|
+
}
|
|
545
|
+
if (!(signature instanceof Uint8Array)) throw new MdocBuilderError("Signing function must return a Uint8Array signature");
|
|
546
|
+
if (signature.length !== EXPECTED_SIGNATURE_LENGTH) throw new MdocBuilderError(`Invalid signature length: expected ${String(EXPECTED_SIGNATURE_LENGTH)} bytes, got ${String(signature.length)}`);
|
|
547
|
+
return [
|
|
548
|
+
protectedHeader,
|
|
549
|
+
unprotectedHeader,
|
|
550
|
+
msoBytes,
|
|
551
|
+
signature
|
|
552
|
+
];
|
|
553
|
+
}
|
|
554
|
+
//#endregion
|
|
555
|
+
//#region src/mdoc/mdoc.ts
|
|
556
|
+
var MdocOutput = class {
|
|
557
|
+
bytes;
|
|
558
|
+
constructor(bytes) {
|
|
559
|
+
this.bytes = Uint8Array.from(bytes);
|
|
560
|
+
}
|
|
561
|
+
asBase64Url() {
|
|
562
|
+
return Buffer.from(this.bytes).toString("base64url");
|
|
563
|
+
}
|
|
564
|
+
asHex() {
|
|
565
|
+
return Buffer.from(this.bytes).toString("hex");
|
|
566
|
+
}
|
|
567
|
+
asBytes() {
|
|
568
|
+
return Uint8Array.from(this.bytes);
|
|
29
569
|
}
|
|
30
570
|
};
|
|
31
571
|
//#endregion
|
|
@@ -34,16 +574,25 @@ var MdocBuilderError = class extends Error {
|
|
|
34
574
|
* Builds an mdoc (ISO 18013-5) document from the provided input.
|
|
35
575
|
*
|
|
36
576
|
* @param input - The mdoc builder input containing document data and metadata.
|
|
37
|
-
* @param sign - A signing function
|
|
577
|
+
* @param sign - A signing function called with the `toBeSigned` bytes.
|
|
38
578
|
* @returns A promise resolving to the built Mdoc document.
|
|
39
|
-
* @throws {MdocBuilderError}
|
|
579
|
+
* @throws {MdocBuilderError} If input validation fails, with the aggregated
|
|
580
|
+
* violations in the message. Errors from any other component propagate to the
|
|
581
|
+
* caller unchanged.
|
|
40
582
|
*/
|
|
41
|
-
function buildMdoc(input, sign) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
});
|
|
46
|
-
|
|
583
|
+
async function buildMdoc(input, sign) {
|
|
584
|
+
const violations = validateMdocBuilderInput(input);
|
|
585
|
+
if (violations.length > 0) throw new MdocBuilderError("Input validation failed", { violations });
|
|
586
|
+
const deviceKeyInfo = buildDeviceKeyInfo(input.deviceKey, [...input.nameSpaces.keys()]);
|
|
587
|
+
const { issuerSignedItemBytes, valueDigests } = await buildIssuerSignedItems(input.nameSpaces);
|
|
588
|
+
const validityInfo = buildValidityInfo(input.credentialValidity);
|
|
589
|
+
return new MdocOutput(assembleIssuerSigned(issuerSignedItemBytes, await assembleIssuerAuth(buildMso({
|
|
590
|
+
docType: input.documentType,
|
|
591
|
+
valueDigests,
|
|
592
|
+
deviceKeyInfo,
|
|
593
|
+
validityInfo,
|
|
594
|
+
statusList: input.statusList
|
|
595
|
+
}), input.certificateChain, sign)));
|
|
47
596
|
}
|
|
48
597
|
//#endregion
|
|
49
598
|
exports.DateFormat = DateFormat;
|