@parmana/verifier 2.0.1 → 2.0.3

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/src/trust.mjs CHANGED
@@ -1,121 +1,121 @@
1
- // Trust set: the keys a verifier accepts, each pinned by fingerprint.
2
- //
3
- // A key is used only when all of these hold:
4
- // - its PEM hashes to the pinned fingerprint (SHA-256 of the SPKI DER);
5
- // - its algorithm matches the algorithm the signature names;
6
- // - it is not marked compromised;
7
- // - the record's createdAt falls within [validFrom, validUntil).
8
- //
9
- // The trust set is supplied by the caller. The verifier never fetches keys.
10
-
11
- import { createHash, createPublicKey } from "node:crypto";
12
- import { parseTimestamp } from "./timestamp.mjs";
13
-
14
- export const ALGORITHM_NAMES = ["ed25519", "dilithium3"];
15
-
16
- export function keyFingerprint(publicKeyPem) {
17
- return fingerprintOfKey(createPublicKey(publicKeyPem));
18
- }
19
-
20
- export function fingerprintOfKey(publicKey) {
21
- const der = publicKey.export({ type: "spki", format: "der" });
22
- return `sha256:${createHash("sha256").update(der).digest("hex")}`;
23
- }
24
-
25
- // Parses a trust-set document into a Map keyId -> entry. Throws an Error with
26
- // a readable message when the document is malformed.
27
- export function parseTrustSet(document) {
28
- return parseTrustPolicy(document).keys;
29
- }
30
-
31
- // Parses a trust-set document into { keys, hybridRequiredFrom }. keys is the
32
- // Map that parseTrustSet returns. hybridRequiredFrom is the optional cutover:
33
- // a record created at or after it must carry a valid hybrid envelope. It is
34
- // null when the document does not set one. It must be an ISO-8601 UTC
35
- // timestamp ending in "Z", so that it means the same time everywhere.
36
- export function parseTrustPolicy(document) {
37
- if (document === null || typeof document !== "object" || !Array.isArray(document.keys)) {
38
- throw new Error('trust set must be an object with a "keys" array');
39
- }
40
-
41
- let hybridRequiredFrom = null;
42
-
43
- if (document.hybridRequiredFrom !== undefined && document.hybridRequiredFrom !== null) {
44
- if (typeof document.hybridRequiredFrom !== "string" || !/Z$/.test(document.hybridRequiredFrom) || Number.isNaN(parseTimestamp(document.hybridRequiredFrom))) {
45
- throw new Error('hybridRequiredFrom must be an ISO-8601 UTC timestamp ending in "Z"');
46
- }
47
-
48
- hybridRequiredFrom = document.hybridRequiredFrom;
49
- }
50
-
51
- // Off by default: a record with no hybrid envelope is refused unless the
52
- // trust file explicitly allows legacy-only records.
53
- if (document.allowLegacyOnly !== undefined && typeof document.allowLegacyOnly !== "boolean") {
54
- throw new Error('allowLegacyOnly must be true or false');
55
- }
56
-
57
- const allowLegacyOnly = document.allowLegacyOnly === true;
58
-
59
- // Off by default: a schema version 2 record (or one with no schemaVersion)
60
- // is refused unless the trust file explicitly allows it.
61
- if (document.allowLegacyV2 !== undefined && typeof document.allowLegacyV2 !== "boolean") {
62
- throw new Error('allowLegacyV2 must be true or false');
63
- }
64
-
65
- const allowLegacyV2 = document.allowLegacyV2 === true;
66
-
67
- const trustKeys = new Map();
68
-
69
- for (const [index, entry] of document.keys.entries()) {
70
- const at = `keys[${index}]`;
71
-
72
- if (entry === null || typeof entry !== "object") {
73
- throw new Error(`${at} must be an object`);
74
- }
75
-
76
- if (typeof entry.keyId !== "string" || entry.keyId === "") {
77
- throw new Error(`${at}.keyId must be a non-empty string`);
78
- }
79
-
80
- if (trustKeys.has(entry.keyId)) {
81
- throw new Error(`duplicate keyId "${entry.keyId}"`);
82
- }
83
-
84
- if (!ALGORITHM_NAMES.includes(entry.algorithm)) {
85
- throw new Error(`${at}.algorithm must be one of ${ALGORITHM_NAMES.join(", ")}`);
86
- }
87
-
88
- if (typeof entry.publicKeyPem !== "string") {
89
- throw new Error(`${at}.publicKeyPem must be a PEM string`);
90
- }
91
-
92
- if (typeof entry.fingerprint !== "string" || !/^sha256:[0-9a-f]{64}$/.test(entry.fingerprint)) {
93
- throw new Error(`${at}.fingerprint must be "sha256:" followed by 64 lowercase hex digits`);
94
- }
95
-
96
- if (typeof entry.validFrom !== "string" || Number.isNaN(parseTimestamp(entry.validFrom))) {
97
- throw new Error(`${at}.validFrom must be an ISO timestamp`);
98
- }
99
-
100
- if (entry.validUntil !== undefined && entry.validUntil !== null &&
101
- (typeof entry.validUntil !== "string" || Number.isNaN(parseTimestamp(entry.validUntil)))) {
102
- throw new Error(`${at}.validUntil must be an ISO timestamp or null`);
103
- }
104
-
105
- if (entry.compromised !== undefined && typeof entry.compromised !== "boolean") {
106
- throw new Error(`${at}.compromised must be true or false`);
107
- }
108
-
109
- trustKeys.set(entry.keyId, {
110
- keyId: entry.keyId,
111
- algorithm: entry.algorithm,
112
- publicKeyPem: entry.publicKeyPem,
113
- fingerprint: entry.fingerprint,
114
- validFrom: entry.validFrom,
115
- validUntil: entry.validUntil ?? null,
116
- compromised: entry.compromised ?? false,
117
- });
118
- }
119
-
120
- return { keys: trustKeys, hybridRequiredFrom, allowLegacyOnly, allowLegacyV2 };
121
- }
1
+ // Trust set: the keys a verifier accepts, each pinned by fingerprint.
2
+ //
3
+ // A key is used only when all of these hold:
4
+ // - its PEM hashes to the pinned fingerprint (SHA-256 of the SPKI DER);
5
+ // - its algorithm matches the algorithm the signature names;
6
+ // - it is not marked compromised;
7
+ // - the record's createdAt falls within [validFrom, validUntil).
8
+ //
9
+ // The trust set is supplied by the caller. The verifier never fetches keys.
10
+
11
+ import { createHash, createPublicKey } from "node:crypto";
12
+ import { parseTimestamp } from "./timestamp.mjs";
13
+
14
+ export const ALGORITHM_NAMES = ["ed25519", "dilithium3"];
15
+
16
+ export function keyFingerprint(publicKeyPem) {
17
+ return fingerprintOfKey(createPublicKey(publicKeyPem));
18
+ }
19
+
20
+ export function fingerprintOfKey(publicKey) {
21
+ const der = publicKey.export({ type: "spki", format: "der" });
22
+ return `sha256:${createHash("sha256").update(der).digest("hex")}`;
23
+ }
24
+
25
+ // Parses a trust-set document into a Map keyId -> entry. Throws an Error with
26
+ // a readable message when the document is malformed.
27
+ export function parseTrustSet(document) {
28
+ return parseTrustPolicy(document).keys;
29
+ }
30
+
31
+ // Parses a trust-set document into { keys, hybridRequiredFrom }. keys is the
32
+ // Map that parseTrustSet returns. hybridRequiredFrom is the optional cutover:
33
+ // a record created at or after it must carry a valid hybrid envelope. It is
34
+ // null when the document does not set one. It must be an ISO-8601 UTC
35
+ // timestamp ending in "Z", so that it means the same time everywhere.
36
+ export function parseTrustPolicy(document) {
37
+ if (document === null || typeof document !== "object" || !Array.isArray(document.keys)) {
38
+ throw new Error('trust set must be an object with a "keys" array');
39
+ }
40
+
41
+ let hybridRequiredFrom = null;
42
+
43
+ if (document.hybridRequiredFrom !== undefined && document.hybridRequiredFrom !== null) {
44
+ if (typeof document.hybridRequiredFrom !== "string" || !/Z$/.test(document.hybridRequiredFrom) || Number.isNaN(parseTimestamp(document.hybridRequiredFrom))) {
45
+ throw new Error('hybridRequiredFrom must be an ISO-8601 UTC timestamp ending in "Z"');
46
+ }
47
+
48
+ hybridRequiredFrom = document.hybridRequiredFrom;
49
+ }
50
+
51
+ // Off by default: a record with no hybrid envelope is refused unless the
52
+ // trust file explicitly allows legacy-only records.
53
+ if (document.allowLegacyOnly !== undefined && typeof document.allowLegacyOnly !== "boolean") {
54
+ throw new Error('allowLegacyOnly must be true or false');
55
+ }
56
+
57
+ const allowLegacyOnly = document.allowLegacyOnly === true;
58
+
59
+ // Off by default: a schema version 2 record (or one with no schemaVersion)
60
+ // is refused unless the trust file explicitly allows it.
61
+ if (document.allowLegacyV2 !== undefined && typeof document.allowLegacyV2 !== "boolean") {
62
+ throw new Error('allowLegacyV2 must be true or false');
63
+ }
64
+
65
+ const allowLegacyV2 = document.allowLegacyV2 === true;
66
+
67
+ const trustKeys = new Map();
68
+
69
+ for (const [index, entry] of document.keys.entries()) {
70
+ const at = `keys[${index}]`;
71
+
72
+ if (entry === null || typeof entry !== "object") {
73
+ throw new Error(`${at} must be an object`);
74
+ }
75
+
76
+ if (typeof entry.keyId !== "string" || entry.keyId === "") {
77
+ throw new Error(`${at}.keyId must be a non-empty string`);
78
+ }
79
+
80
+ if (trustKeys.has(entry.keyId)) {
81
+ throw new Error(`duplicate keyId "${entry.keyId}"`);
82
+ }
83
+
84
+ if (!ALGORITHM_NAMES.includes(entry.algorithm)) {
85
+ throw new Error(`${at}.algorithm must be one of ${ALGORITHM_NAMES.join(", ")}`);
86
+ }
87
+
88
+ if (typeof entry.publicKeyPem !== "string") {
89
+ throw new Error(`${at}.publicKeyPem must be a PEM string`);
90
+ }
91
+
92
+ if (typeof entry.fingerprint !== "string" || !/^sha256:[0-9a-f]{64}$/.test(entry.fingerprint)) {
93
+ throw new Error(`${at}.fingerprint must be "sha256:" followed by 64 lowercase hex digits`);
94
+ }
95
+
96
+ if (typeof entry.validFrom !== "string" || Number.isNaN(parseTimestamp(entry.validFrom))) {
97
+ throw new Error(`${at}.validFrom must be an ISO timestamp`);
98
+ }
99
+
100
+ if (entry.validUntil !== undefined && entry.validUntil !== null &&
101
+ (typeof entry.validUntil !== "string" || Number.isNaN(parseTimestamp(entry.validUntil)))) {
102
+ throw new Error(`${at}.validUntil must be an ISO timestamp or null`);
103
+ }
104
+
105
+ if (entry.compromised !== undefined && typeof entry.compromised !== "boolean") {
106
+ throw new Error(`${at}.compromised must be true or false`);
107
+ }
108
+
109
+ trustKeys.set(entry.keyId, {
110
+ keyId: entry.keyId,
111
+ algorithm: entry.algorithm,
112
+ publicKeyPem: entry.publicKeyPem,
113
+ fingerprint: entry.fingerprint,
114
+ validFrom: entry.validFrom,
115
+ validUntil: entry.validUntil ?? null,
116
+ compromised: entry.compromised ?? false,
117
+ });
118
+ }
119
+
120
+ return { keys: trustKeys, hybridRequiredFrom, allowLegacyOnly, allowLegacyV2 };
121
+ }