@gwydion-id/icao-9303-models 0.1.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.
Files changed (62) hide show
  1. package/LICENSES/CC0-1.0.txt +121 -0
  2. package/LICENSES/LGPL-3.0-or-later.txt +304 -0
  3. package/README.md +79 -0
  4. package/package.json +17 -0
  5. package/package.json.license +3 -0
  6. package/src/digitalseal.js +234 -0
  7. package/src/digitalsealv3.js +359 -0
  8. package/src/digitalsealv4.js +369 -0
  9. package/src/index.js +41 -0
  10. package/src/mrvadocument.js +465 -0
  11. package/src/mrvadocumentwithvds3.js +559 -0
  12. package/src/mrvadocumentwithvds4.js +559 -0
  13. package/src/mrvbdocument.js +466 -0
  14. package/src/mrvbdocumentwithvds3.js +560 -0
  15. package/src/mrvbdocumentwithvds4.js +560 -0
  16. package/src/td1document.js +408 -0
  17. package/src/td1documentwithvds3.js +479 -0
  18. package/src/td1documentwithvds4.js +479 -0
  19. package/src/td2document.js +379 -0
  20. package/src/td2documentwithvds3.js +466 -0
  21. package/src/td2documentwithvds4.js +466 -0
  22. package/src/td3document.js +391 -0
  23. package/src/td3documentwithvds3.js +466 -0
  24. package/src/td3documentwithvds4.js +466 -0
  25. package/src/traveldocument.js +306 -0
  26. package/src/utilities/bytes-to-date.js +23 -0
  27. package/src/utilities/c40-decode.js +225 -0
  28. package/src/utilities/c40-encode.js +254 -0
  29. package/src/utilities/c40-shift1.js +10 -0
  30. package/src/utilities/date-to-bytes.js +38 -0
  31. package/src/utilities/der-length-to-length.js +33 -0
  32. package/src/utilities/expand-year.js +26 -0
  33. package/src/utilities/gender-marker-to-mrz.js +14 -0
  34. package/src/utilities/generate-mrz-check-digit.js +139 -0
  35. package/src/utilities/icao-date.js +234 -0
  36. package/src/utilities/length-to-der-length.js +41 -0
  37. package/src/utilities/nationality-codes.js +303 -0
  38. package/src/utilities/normalize-mrz-string.js +19 -0
  39. package/src/utilities/optional-data-mrz.js +29 -0
  40. package/src/utilities/pad-mrz-string.js +16 -0
  41. package/src/utilities/set-signature-zone.js +34 -0
  42. package/src/utilities/stringify-validation-array.js +27 -0
  43. package/src/utilities/validate-hex-string.js +41 -0
  44. package/src/utilities/validate-identifier-code.js +38 -0
  45. package/src/utilities/validate-mrz-string.js +44 -0
  46. package/src/utilities/vds-magic.js +9 -0
  47. package/src/utilities/vds-signature-marker.js +9 -0
  48. package/src/utilities/vds-version-3.js +9 -0
  49. package/src/utilities/vds-version-4.js +9 -0
  50. package/src/visadocument.js +114 -0
  51. package/tests/models/digitalsealv4.test.js +67 -0
  52. package/tests/models/mrvadocument.test.js +61 -0
  53. package/tests/models/mrvbdocument.test.js +61 -0
  54. package/tests/models/td1document.test.js +50 -0
  55. package/tests/models/td2document.test.js +49 -0
  56. package/tests/models/td3document.test.js +88 -0
  57. package/tests/utilities/c40-encodation-scheme.test.js +34 -0
  58. package/tests/utilities/der-tlv-length-conversion.test.js +99 -0
  59. package/tests/utilities/document-utilities.test.js +199 -0
  60. package/tests/utilities/icao-constants.test.js +35 -0
  61. package/tests/utilities/icao-date.test.js +141 -0
  62. package/tests/utilities/vds-date-conversions.test.js +28 -0
@@ -0,0 +1,369 @@
1
+ // SPDX-FileCopyrightText: 2026 Don Geronimo <https://sentamal.in>
2
+ // SPDX-License-Identifier: LGPL-3.0-or-later
3
+
4
+ import { DigitalSeal } from "./digitalseal.js";
5
+ import { c40Encode } from "./utilities/c40-encode.js";
6
+ import { c40Decode } from "./utilities/c40-decode.js";
7
+ import { VDS_MAGIC } from "./utilities/vds-magic.js";
8
+ import { VDS_SIGNATURE_MARKER } from "./utilities/vds-signature-marker.js";
9
+ import { VDS_VERSION_4 } from "./utilities/vds-version-4.js";
10
+ import { lengthToDERLength } from "./utilities/length-to-der-length.js";
11
+ import { derLengthToLength } from "./utilities/der-length-to-length.js";
12
+ import { setSignatureZone } from "./utilities/set-signature-zone.js";
13
+ import { IcaoDate } from "./utilities/icao-date.js";
14
+
15
+ /**
16
+ * Stores properties and methods for ICAO 9303 visible digital seals (VDSs)
17
+ * version 4. Consult ICAO 9030 part 13 to see the differences between this
18
+ * version and other versions.
19
+ *
20
+ * `DigitalSealV4` is intended to be used to compose different kinds of
21
+ * machine-readable travel documents (MRTDs) with attached VDSs containing
22
+ * different document features. It is not intended to be instantiated
23
+ * directly.
24
+ */
25
+ export class DigitalSealV4 {
26
+ /**
27
+ * Create a `DigitalSealV4`.
28
+ * @param { Object } [opt] - An options object.
29
+ * @param { string } [opt.authorityCode] - A 3-character string consisting of
30
+ * the characters A-Z, 0-9, ' ', or <. A code from ISO-3166-1,
31
+ * ICAO 9303-3, or these user-assigned ranges are recommended: AAA-AAZ,
32
+ * QMA-QZZ, XAA-XZZ, or ZZA-ZZZ.
33
+ * @param { string } [opt.identifierCode] - A 4-character string consisting of
34
+ * the characters 0-9 and A-Z.
35
+ * @param { string } [opt.certReference] - A hex-string that uniquely
36
+ * identifies a certificate for a given signer.
37
+ * @param { string | Date | IcaoDate } [opt.issueDate] - A calendar date string in
38
+ * YYYY-MM-DD format, a `Date` object, or an `IcaoDate` object.
39
+ * @param { string | Date | IcaoDate } [opt.signatureDate] - A calendar date string in
40
+ * YYYY-MM-DD format, a `Date` object, or an `IcaoDate` object.
41
+ * @param { number } [opt.featureDefinition] - A number in the range of
42
+ * 0x01-0xFE.
43
+ * @param { number } [opt.typeCategory] - A number in the range of 0x01-0xFE.
44
+ * Odd numbers in the range between 0x01 and 0xFD shall be used for
45
+ * ICAO-specified document type categories.
46
+ * @param { Map<number, number[]> } [opt.features] - A store of
47
+ * digitally-encoded document features used to create the message zone.
48
+ * @param { number[] } [opt.signatureData] - The raw signature data generated
49
+ * by concatenating the header and message zone, hashing the result, and
50
+ * signing the hash with a cryptographic key.
51
+ * @param { number[] } [opt.headerZone] - The header zone of the VDS as
52
+ * defined by ICAO 9303 part 13.
53
+ * @param { number[] } [opt.messageZone] - The message zone of the VDS as
54
+ * defined by ICAO 9303 part 13.
55
+ * @param { number[] } [opt.signatureZone] - The signature zone of the VDS as
56
+ * a TLV of the signature marker, its length in BER/DER definite length
57
+ * form, and the raw signature data.
58
+ * @param { number[] } [opt.unsignedSeal] - A concatenation of the header zone
59
+ * and the message zone of the VDS.
60
+ * @param { number[] } [opt.signedSeal] - A concatenation of the header zone,
61
+ * the message zone, and the signature zone of the VDS.
62
+ */
63
+ constructor(opt) {
64
+ this.#digitalseal = new DigitalSeal();
65
+
66
+ this.authorityCode = opt?.authorityCode ?? "UTO";
67
+ this.identifierCode = opt?.identifierCode ?? "UTSS";
68
+ this.certReference = opt?.certReference ?? "00000";
69
+ this.issueDate = opt?.issueDate ?? "2007-04-15";
70
+ this.signatureDate = opt?.signatureDate ?? "2007-04-15";
71
+ this.featureDefinition = opt?.featureDefinition ?? 0x01;
72
+ this.typeCategory = opt?.typeCategory ?? 0x01;
73
+ this.features = opt?.features ?? new Map();
74
+ this.signatureData = opt?.signatureData ?? Array(64).fill(0);
75
+
76
+ if (opt?.headerZone) { this.headerZone = opt.headerZone; }
77
+ if (opt?.messageZone) { this.messageZone = opt.messageZone; }
78
+ if (opt?.signatureZone) { this.signatureZone = opt.signatureZone; }
79
+ if (opt?.unsignedSeal) { this.unsignedSeal = opt.unsignedSeal; }
80
+ if (opt?.signedSeal) { this.signedSeal = opt.signedSeal; }
81
+ }
82
+
83
+ // The object `DigitalSealV4` uses to compose itself.
84
+ #digitalseal;
85
+
86
+ /**
87
+ * A code identifying the authority who issued this seal.
88
+ * @type { string }
89
+ */
90
+ get authorityCode() { return this.#digitalseal.authorityCode; }
91
+ /**
92
+ * @param { string } value - A 3-character string consisting of the characters
93
+ * A-Z, 0-9, ' ', or <. A code from ISO-3166-1, ICAO 9303-3, or these
94
+ * user-assigned ranges are recommended: AAA-AAZ, QMA-QZZ, XAA-XZZ, or ZZA-ZZZ.
95
+ */
96
+ set authorityCode(value) { this.#digitalseal.authorityCode = value; }
97
+
98
+ /**
99
+ * A combination of a two-letter authority code and of two alphanumeric
100
+ * characters to identify a signer within the issuing authority.
101
+ * @type { string }
102
+ */
103
+ get identifierCode() { return this.#digitalseal.identifierCode; }
104
+ /**
105
+ * @param { string } value - A 4-character string consisting of the characters
106
+ * 0-9 and A-Z.
107
+ */
108
+ set identifierCode(value) { this.#digitalseal.identifierCode = value; }
109
+
110
+ /**
111
+ * A hex-string that uniquely identifies a certificate for a given signer.
112
+ * @type { string }
113
+ */
114
+ get certReference() { return this.#digitalseal.certReference; }
115
+ /**
116
+ * @param { string } value - A hex string.
117
+ */
118
+ set certReference(value) { this.#digitalseal.certReference = value; }
119
+
120
+ /**
121
+ * A date string on which the document was issued.
122
+ * @type { IcaoDate }
123
+ */
124
+ get issueDate() { return this.#digitalseal.issueDate; }
125
+ /**
126
+ * @param { string | Date | IcaoDate } value - A calendar date string in YYYY-MM-DD
127
+ * format, a `Date` object, or an `IcaoDate` object.
128
+ */
129
+ set issueDate(value) { this.#digitalseal.issueDate = value; }
130
+
131
+ /**
132
+ * A date string on which the seal was signed.
133
+ * @type { IcaoDate }
134
+ */
135
+ get signatureDate() { return this.#digitalseal.signatureDate; }
136
+ /**
137
+ * @param { string | Date | IcaoDate } value - A calendar date string in YYYY-MM-DD
138
+ * format, a `Date` object, or an `IcaoDate` object.
139
+ */
140
+ set signatureDate(value) { this.#digitalseal.signatureDate = value; }
141
+
142
+ /**
143
+ * A reference code to a document that defines the number and encoding of VDS
144
+ * features.
145
+ * @type { number }
146
+ */
147
+ get featureDefinition() { return this.#digitalseal.featureDefinition; }
148
+ /**
149
+ * @param { number } value - A number in the range of 0x01-0xFE.
150
+ */
151
+ set featureDefinition(value) { this.#digitalseal.featureDefinition = value; }
152
+
153
+ /**
154
+ * A code defining the document type category to which the seal is attached.
155
+ * @type { number }
156
+ */
157
+ get typeCategory() { return this.#digitalseal.typeCategory; }
158
+ /**
159
+ * @param { number } value - A number in the range of 0x01-0xFE. Odd numbers
160
+ * in the range between 0x01 and 0xFD shall be used for ICAO-specified
161
+ * document type categories.
162
+ */
163
+ set typeCategory(value) { this.#digitalseal.typeCategory = value; }
164
+
165
+ /**
166
+ * A store of digitally-encoded document features used to create the message
167
+ * zone.
168
+ * @type { Map<number, number[]>}
169
+ */
170
+ get features() { return this.#digitalseal.features; }
171
+ /**
172
+ * @param { Map<number, number[]> } value
173
+ */
174
+ set features(value) { this.#digitalseal.features = value; }
175
+
176
+ /**
177
+ * The raw signature data generated by concatenating the header and message
178
+ * zone, hashing the result, and signing the hash with a cryptographic
179
+ * key.
180
+ * @type { number[] }
181
+ */
182
+ get signatureData() { return this.#digitalseal.signatureData; }
183
+ /**
184
+ * @param { number[] } value
185
+ */
186
+ set signatureData(value) { this.#digitalseal.signatureData = value; }
187
+
188
+ /**
189
+ * The header zone of the VDS as defined by ICAO 9303 part 13.
190
+ * @type { number[] }
191
+ */
192
+ get headerZone() {
193
+ let output = [];
194
+ output.push(VDS_MAGIC);
195
+ output.push(VDS_VERSION_4);
196
+ output =
197
+ output.concat(c40Encode(this.authorityCode.padEnd(3, "<")));
198
+ output = output.concat(c40Encode(
199
+ this.identifierCode +
200
+ this.certReference.length.toString(16).padStart(2, "0") +
201
+ this.certReference
202
+ ));
203
+ output = output.concat(this.issueDate.toVDSArray());
204
+ output = output.concat(this.signatureDate.toVDSArray());
205
+ output.push(this.featureDefinition);
206
+ output.push(this.typeCategory);
207
+ return output;
208
+ }
209
+ /**
210
+ * @param { number[] } value
211
+ */
212
+ set headerZone(value) {
213
+ this.#setHeader(0, value);
214
+ }
215
+ /**
216
+ * Given a point 'start' in an array 'value', extract the header data.
217
+ * @param { number } start
218
+ * @param { number[] } value
219
+ * @returns { number } The point in the array where the method stopped
220
+ * reading.
221
+ */
222
+ #setHeader(start, value) {
223
+ if (value[0] !== VDS_MAGIC) {
224
+ throw new TypeError(
225
+ `Value '${value[0].toString(16).padStart(2, "0").toUpperCase()}' is ` +
226
+ `not an ICAO Digital Seal (` +
227
+ `${VDS_MAGIC.toString(16).padStart(2, "0").toUpperCase()}).`
228
+ );
229
+ }
230
+ if (value[1] !== VDS_VERSION_4) {
231
+ throw new TypeError(
232
+ `Value '${value[0].toString(16).padStart(2, "0").toUpperCase()}' is ` +
233
+ `not version 4 of an ICAO Digital Seal (` +
234
+ `${VDS_VERSION_4.toString(16).padStart(
235
+ 2, "0"
236
+ ).toUpperCase()}).`
237
+ );
238
+ }
239
+ start += 2;
240
+ this.authorityCode =
241
+ c40Decode(value.slice(start, start + 2)).trim();
242
+ start += 2;
243
+ const ID_LENGTH = c40Decode(value.slice(start, start + 4));
244
+ this.identifierCode = ID_LENGTH.substring(0, 4);
245
+ const CERT_REF_LENGTH = parseInt(ID_LENGTH.substring(4, 6), 16);
246
+ let certRefC40Length = Math.floor(CERT_REF_LENGTH / 3) * 2;
247
+ const CERT_REF_MODULUS = CERT_REF_LENGTH % 3;
248
+ if (CERT_REF_MODULUS) {
249
+ certRefC40Length += 2;
250
+ }
251
+ start += 4;
252
+ const CERT_REF_DECODE =
253
+ c40Decode(value.slice(start, start + certRefC40Length));
254
+ if (CERT_REF_LENGTH !== CERT_REF_DECODE.length) {
255
+ throw new RangeError(
256
+ `Length '${CERT_REF_LENGTH}' of certificate reference does not match ` +
257
+ `the actual length (${CERT_REF_DECODE.length}).`
258
+ );
259
+ } else {
260
+ this.certReference = CERT_REF_DECODE;
261
+ }
262
+ start += certRefC40Length;
263
+ this.issueDate = new IcaoDate(value.slice(start, start + 3));
264
+ start += 3;
265
+ this.signatureDate = new IcaoDate(value.slice(start, start + 3));
266
+ start += 3;
267
+ this.featureDefinition = value[start];
268
+ start += 1;
269
+ this.typeCategory = value[start];
270
+ start += 1;
271
+ return start;
272
+ }
273
+
274
+ /**
275
+ * The message zone of the VDS as defined by ICAO 9303 part 13.
276
+ * @type { number[] }
277
+ */
278
+ get messageZone() {
279
+ let output = [];
280
+ for (const [tag, value] of this.features) {
281
+ output.push(tag);
282
+ output = output.concat(lengthToDERLength(value.length));
283
+ output = output.concat(value);
284
+ }
285
+ return output;
286
+ }
287
+ /**
288
+ * @param { number[] } value
289
+ */
290
+ set messageZone(value) {
291
+ this.#setMessage(0, value);
292
+ }
293
+ /**
294
+ * Given a point 'start' in an array 'value', extract the document feature
295
+ * data.
296
+ * @param { number } start
297
+ * @param { number[] } value
298
+ * @returns { number } The point in the array where the method stopped
299
+ * reading.
300
+ */
301
+ #setMessage(start, value) {
302
+ this.features.clear();
303
+ while (start < value.length) {
304
+ if (value[start] === VDS_SIGNATURE_MARKER) {
305
+ break;
306
+ }
307
+ const TAG = value[start];
308
+ start += 1;
309
+ const LENGTH = derLengthToLength(value.slice(start));
310
+ start += lengthToDERLength(LENGTH).length;
311
+ const slicedValue = value.slice(start, start + LENGTH);
312
+ if (slicedValue.length !== LENGTH) {
313
+ throw new RangeError(
314
+ `Length '${LENGTH}' of document feature does not match the actual ` +
315
+ `length (${slicedValue.length}).`
316
+ );
317
+ }
318
+ this.features.set(TAG, slicedValue);
319
+ start += LENGTH;
320
+ }
321
+ return start;
322
+ }
323
+
324
+ /**
325
+ * The signature zone of the VDS as a TLV of the signature marker, its length
326
+ * in BER/DER definite length form, and the raw signature data.
327
+ * @type { number[] }
328
+ */
329
+ get signatureZone() { return this.#digitalseal.signatureZone; }
330
+ /**
331
+ * @param { number[] } value
332
+ */
333
+ set signatureZone(value) { this.#digitalseal.signatureZone = value; }
334
+
335
+ /**
336
+ * A concatenation of the header zone and the message zone of the VDS.
337
+ * @type { number[] }
338
+ */
339
+ get unsignedSeal() {
340
+ return this.headerZone.concat(this.messageZone);
341
+ }
342
+ /**
343
+ * @param { number[] } value
344
+ */
345
+ set unsignedSeal(value) {
346
+ let start = 0;
347
+ start = this.#setHeader(start, value);
348
+ this.#setMessage(start, value);
349
+ }
350
+
351
+ /**
352
+ * A concatenation of the header zone, the message zone, and the signature
353
+ * zone of the VDS.
354
+ * @type { number[] }
355
+ */
356
+ get signedSeal() {
357
+ return this.headerZone.concat(this.messageZone.concat(this.signatureZone));
358
+ }
359
+ /**
360
+ * @param { number[] } value
361
+ */
362
+ set signedSeal(value) {
363
+ let start = 0;
364
+ start = this.#setHeader(start, value);
365
+ start = this.#setMessage(start, value);
366
+ this.signatureData = setSignatureZone(start, value);
367
+ }
368
+ }
369
+
package/src/index.js ADDED
@@ -0,0 +1,41 @@
1
+ // SPDX-FileCopyrightText: 2026 Don Geronimo <https://sentamal.in>
2
+ // SPDX-License-Identifier: CC0-1.0
3
+
4
+ // Utility exports
5
+ export { NATIONALITY_CODES } from './utilities/nationality-codes.js';
6
+ export { IcaoDate } from './utilities/icao-date.js';
7
+ export { dateToBytes } from './utilities/dates-to-bytes.js';
8
+ export { bytesToDate } from './utilities/bytes-to-date.js';
9
+ export { c40Decode } from './utilities/c40-decode.js';
10
+ export { c40Encode } from './utilities/c40-encode.js';
11
+ export { derLengthToLength } from './utilities/der-length-to-length.js';
12
+ export { lengthToDERLength } from './utilities/length-to-der-length.js';
13
+
14
+ // Abstract models
15
+ export { DigitalSeal } from './digitalseal.js'; // TODO: Test Suite
16
+ export { TravelDocument } from './traveldocument.js'; // TODO: Test Suite
17
+ export { VisaDocument } from './visadocument.js'; // TODO: Test Suite
18
+
19
+ // Digital seals
20
+ export { DigitalSealV3 } from './digitalsealv3.js'; // TODO: Test Suite
21
+ export { DigitalSealV4 } from './digitalsealv4.js';
22
+
23
+ // Base document models
24
+ export { TD1Document } from './td1document.js';
25
+ export { TD2Document } from './td2document.js';
26
+ export { TD3Document } from './td3document.js';
27
+ export { MRVADocument } from './mrvadocument.js';
28
+ export { MRVBDocument } from './mrvbdocument.js';
29
+
30
+ // Composed document models
31
+ export { TD1DocumentWithVDS3 } from './td1documentwithvds3.js'; // TODO: Test Suite
32
+ export { TD1DocumentWithVDS4 } from './td1documentwithvds4.js'; // TODO: Test Suite
33
+ export { TD2DocumentWithVDS3 } from './td2documentwithvds3.js'; // TODO: Test Suite
34
+ export { TD2DocumentWithVDS4 } from './td2documentwithvds4.js'; // TODO: Test Suite
35
+ export { TD3DocumentWithVDS3 } from './td3documentwithvds3.js'; // TODO: Test Suite
36
+ export { TD3DocumentWithVDS4 } from './td3documentwithvds4.js'; // TODO: Test Suite
37
+ export { MRVADocumentWithVDS3 } from './mrvadocumentwithvds3.js'; // TODO: Test Suite
38
+ export { MRVADocumentWithVDS4 } from './mrvadocumentwithvds4.js'; // TODO: Test Suite
39
+ export { MRVBDocumentWithVDS3 } from './mrvbdocumentwithvds3.js'; // TODO: Test Suite
40
+ export { MRVBDocumentWithVDS4 } from './mrvbdocumentwithvds4.js'; // TODO: Test Suite
41
+