@blamejs/pki 0.8.31 → 0.8.49
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/CHANGELOG.md +401 -0
- package/README.md +66 -15
- package/bin/pki.js +716 -30
- package/examples/issue-a-certificate.js +90 -0
- package/examples/post-quantum.js +66 -0
- package/examples/read-a-certificate.js +56 -0
- package/examples/revoke-and-check.js +86 -0
- package/examples/sign-and-verify-cms.js +63 -0
- package/index.js +18 -5
- package/lib/acme.js +223 -74
- package/lib/alt-sig.js +441 -0
- package/lib/asn1-der.js +48 -25
- package/lib/attrcert-sign.js +27 -19
- package/lib/cbor-det.js +24 -14
- package/lib/cmc-build.js +9 -2
- package/lib/cmc-verify.js +14 -3
- package/lib/cmp-build.js +17 -5
- package/lib/cmp-session.js +24 -11
- package/lib/cmp-verify.js +28 -19
- package/lib/cms-compress.js +3 -1
- package/lib/cms-decrypt.js +102 -7
- package/lib/cms-encrypt.js +59 -7
- package/lib/cms-sign.js +31 -9
- package/lib/cms-verify.js +22 -18
- package/lib/composite-kem.js +30 -18
- package/lib/composite-sig.js +29 -14
- package/lib/constants.js +26 -0
- package/lib/crl-sign.js +111 -19
- package/lib/crmf-sign.js +167 -15
- package/lib/csr-sign.js +117 -6
- package/lib/ct.js +1008 -58
- package/lib/est.js +29 -16
- package/lib/framework-error.js +12 -0
- package/lib/guard-async.js +1 -1
- package/lib/guard-bytes.js +103 -20
- package/lib/guard-crypto.js +205 -3
- package/lib/guard-encoding.js +6 -1
- package/lib/guard-identifier.js +55 -8
- package/lib/guard-intrinsic.js +43 -0
- package/lib/guard-json.js +122 -9
- package/lib/guard-limits.js +12 -1
- package/lib/guard-list.js +63 -8
- package/lib/guard-name.js +167 -20
- package/lib/guard-range.js +43 -5
- package/lib/guard-text.js +41 -0
- package/lib/guard-verdict.js +39 -8
- package/lib/hpke.js +688 -42
- package/lib/http-transport.js +156 -4
- package/lib/identity-match.js +4 -4
- package/lib/inspect.js +33 -6
- package/lib/jose.js +93 -25
- package/lib/key.js +18 -13
- package/lib/lint.js +165 -1
- package/lib/merkle.js +233 -27
- package/lib/ocsp.js +27 -14
- package/lib/oid.js +14 -4
- package/lib/path-validate.js +143 -61
- package/lib/pkcs12-build.js +7 -2
- package/lib/pki-build.js +31 -7
- package/lib/possession.js +575 -0
- package/lib/related-cert.js +708 -0
- package/lib/revocation-fetch.js +6 -6
- package/lib/scep.js +15 -5
- package/lib/schema-c509.js +5 -2
- package/lib/schema-cmc.js +1 -1
- package/lib/schema-cmp.js +8 -8
- package/lib/schema-cms.js +2 -15
- package/lib/schema-csr.js +69 -0
- package/lib/schema-engine.js +10 -3
- package/lib/schema-pkix.js +228 -2
- package/lib/schema-tsp.js +7 -6
- package/lib/sign-scheme.js +22 -9
- package/lib/sigstore.js +684 -168
- package/lib/sleep.js +8 -1
- package/lib/smime.js +22 -1
- package/lib/tlog.js +1235 -0
- package/lib/tls-cert-compress.js +25 -8
- package/lib/tsp-sign.js +120 -35
- package/lib/tuf.js +1395 -0
- package/lib/validator-tpm.js +4 -1
- package/lib/verify-seam.js +7 -1
- package/lib/webauthn-mds.js +41 -4
- package/lib/webauthn.js +43 -25
- package/lib/webcrypto.js +48 -46
- package/lib/x509-sign.js +225 -19
- package/package.json +2 -1
- package/sbom.cdx.json +6 -6
package/lib/tuf.js
ADDED
|
@@ -0,0 +1,1395 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @module pki.tuf
|
|
6
|
+
* @nav Transparency
|
|
7
|
+
* @title TUF
|
|
8
|
+
* @fullname The Update Framework: metadata verification and root rotation
|
|
9
|
+
* @order 230
|
|
10
|
+
* @slug tuf
|
|
11
|
+
*
|
|
12
|
+
* @intro
|
|
13
|
+
* The Update Framework's metadata layer: the signed documents a repository
|
|
14
|
+
* publishes to say which keys may sign what, and the rules a client applies
|
|
15
|
+
* before believing any of it. This is the trust root a sigstore verifier
|
|
16
|
+
* would otherwise have pinned by hand.
|
|
17
|
+
*
|
|
18
|
+
* TUF signs the canonical JSON form of a document's `signed` object, so
|
|
19
|
+
* `canonicalJson` is the wire format rather than a convenience: it sorts
|
|
20
|
+
* object keys by code point, emits no whitespace, and escapes only backslash
|
|
21
|
+
* and quote, leaving a literal control character as itself. `JSON.stringify`
|
|
22
|
+
* would write `\n` where this writes a newline byte, so the two produce
|
|
23
|
+
* different signatures over the same document. A float is refused rather than
|
|
24
|
+
* rounded, because the reference encoder cannot write one.
|
|
25
|
+
*
|
|
26
|
+
* `keyId` derives a key's identifier as the hex SHA-256 of its own canonical
|
|
27
|
+
* form, and `verifySignatures` recomputes it for every key a role names, so a
|
|
28
|
+
* document cannot list one key under another's identifier. A threshold counts
|
|
29
|
+
* one verified signature per distinct identifier, which is what the
|
|
30
|
+
* specification requires of a client meeting the same key twice.
|
|
31
|
+
*
|
|
32
|
+
* `updateRoot` walks the root chain: each new root must be signed by a
|
|
33
|
+
* threshold of the keys the trusted root names AND a threshold of its own, and
|
|
34
|
+
* its version must be exactly one more than the trusted one. That pair of
|
|
35
|
+
* rules is what makes a key rotation safe and a skipped intermediate
|
|
36
|
+
* impossible. Verifying only, and transport-free: nothing here fetches.
|
|
37
|
+
*
|
|
38
|
+
* @card
|
|
39
|
+
* Canonical JSON signing preimage, key-identifier binding, threshold
|
|
40
|
+
* verification, and the root-rotation chain walk. Fail-closed, no transport.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
var constants = require("./constants");
|
|
44
|
+
var frameworkError = require("./framework-error");
|
|
45
|
+
var guard = require("./guard-all");
|
|
46
|
+
var intrinsic = require("./guard-intrinsic");
|
|
47
|
+
var rfc3339 = require("./rfc3339");
|
|
48
|
+
|
|
49
|
+
var TufError = frameworkError.TufError;
|
|
50
|
+
function _err(code, message, cause) { return new TufError(code, message, cause); }
|
|
51
|
+
|
|
52
|
+
var _isArray = intrinsic.isArray;
|
|
53
|
+
var _isSafeInteger = intrinsic.isSafeInteger;
|
|
54
|
+
var _keys = intrinsic.keys;
|
|
55
|
+
var _create = intrinsic.create;
|
|
56
|
+
var _hasOwn = intrinsic.hasOwn;
|
|
57
|
+
var _bufferFrom = intrinsic.bufferFrom;
|
|
58
|
+
var _bufferConcat = intrinsic.bufferConcat;
|
|
59
|
+
var _bufToString = intrinsic.bufToString;
|
|
60
|
+
var _push = intrinsic.push;
|
|
61
|
+
var _strSlice = intrinsic.uncurry(String.prototype.slice);
|
|
62
|
+
var _codePointAt = intrinsic.uncurry(String.prototype.codePointAt);
|
|
63
|
+
var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
|
|
64
|
+
var _numToString = intrinsic.uncurry(Number.prototype.toString);
|
|
65
|
+
var _dateParse = intrinsic.dateParse;
|
|
66
|
+
var _byteLength = intrinsic.byteLength;
|
|
67
|
+
var _sort = intrinsic.sort;
|
|
68
|
+
var _join = intrinsic.join;
|
|
69
|
+
|
|
70
|
+
var CANONICAL_MAX_DEPTH = constants.LIMITS.JSON_MAX_DEPTH;
|
|
71
|
+
var KEY_ID_HEX_LEN = 64;
|
|
72
|
+
var TUF_ROLES = ["root", "timestamp", "snapshot", "targets"];
|
|
73
|
+
|
|
74
|
+
/** @internal Canonical JSON escapes ONLY these two, each with a single backslash. Everything else,
|
|
75
|
+
* including a byte below 0x20, is emitted as itself. A generic JSON writer escapes far more, and
|
|
76
|
+
* would produce a different preimage for the same document. */
|
|
77
|
+
function _canonicalString(s, out, budget) {
|
|
78
|
+
/** @internal Every string reaching the encoding passes here, keys and values alike, so the
|
|
79
|
+
* well-formed check is made once. Canonical JSON is the preimage a TUF signature covers, and an
|
|
80
|
+
* unpaired surrogate would be converted to U+FFFD, so two documents differing only in that code unit
|
|
81
|
+
* would have the same signing bytes.
|
|
82
|
+
* @guard-via guard\.text\.assertWellFormedUtf16\( */
|
|
83
|
+
guard.text.assertWellFormedUtf16(s, TufError, "tuf/bad-input", "a canonical JSON string");
|
|
84
|
+
_emit(out, "\"", budget);
|
|
85
|
+
var start = 0;
|
|
86
|
+
for (var i = 0; i < s.length; i++) {
|
|
87
|
+
var c = _codePointAt(s, i);
|
|
88
|
+
if (c === 0x5c || c === 0x22) {
|
|
89
|
+
if (i > start) _emit(out, _strSlice(s, start, i), budget);
|
|
90
|
+
_emit(out, c === 0x5c ? "\\\\" : "\\\"", budget);
|
|
91
|
+
start = i + 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (start < s.length) _emit(out, _strSlice(s, start), budget);
|
|
95
|
+
_emit(out, "\"", budget);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** @internal Keys compared by CODE POINT. A default sort compares UTF-16 code units, which orders a
|
|
99
|
+
* surrogate pair below U+FFFF where the code point orders it above, so the two disagree on any
|
|
100
|
+
* document carrying a key outside the basic plane. */
|
|
101
|
+
function _byCodePoint(a, b) {
|
|
102
|
+
var n = a.length < b.length ? a.length : b.length;
|
|
103
|
+
for (var i = 0; i < n; i++) {
|
|
104
|
+
var ca = _codePointAt(a, i), cb = _codePointAt(b, i);
|
|
105
|
+
if (ca !== cb) return ca < cb ? -1 : 1;
|
|
106
|
+
}
|
|
107
|
+
return a.length === b.length ? 0 : (a.length < b.length ? -1 : 1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** @internal Whether a value is a plain record: an object whose prototype is `Object.prototype` or null.
|
|
111
|
+
* Canonical JSON encodes an object by its own enumerable keys, which is only the whole value when the
|
|
112
|
+
* value has no state anywhere else, and the line is drawn by PROTOTYPE rather than by naming the classes
|
|
113
|
+
* to refuse. A denylist of the JS built-ins left every HOST object admitted: a Blob, a stream, a
|
|
114
|
+
* CryptoKey, an X509Certificate, a WebAssembly module and a collection iterator each have internal state
|
|
115
|
+
* and no enumerable keys, so each encoded as `{}` after the named built-ins were already refused.
|
|
116
|
+
* Drawn this way it also refuses a class nobody has written yet.
|
|
117
|
+
*
|
|
118
|
+
* A revoked Proxy throws on the prototype read itself, so the read is guarded and the refusal is this
|
|
119
|
+
* module's own rather than whatever the Proxy raised. */
|
|
120
|
+
function _isPlainRecordByPrototype(value) {
|
|
121
|
+
var proto;
|
|
122
|
+
try { proto = intrinsic.getPrototypeOf(value); }
|
|
123
|
+
catch (e) { void e; return false; }
|
|
124
|
+
return proto === null || proto === _OBJECT_PROTOTYPE;
|
|
125
|
+
}
|
|
126
|
+
/** @internal The sentinel comes from `guard.intrinsic`, captured at load, rather than from a live
|
|
127
|
+
* `Object.prototype` read: comparing against the live global would let a replacement decide what counts
|
|
128
|
+
* as a plain record, and the whole point of this check is that nothing but a plain record is signed. */
|
|
129
|
+
var _OBJECT_PROTOTYPE = intrinsic.ObjectProto;
|
|
130
|
+
|
|
131
|
+
/** @internal Whether a value can be inspected at all. A revoked Proxy throws on every operation, so the
|
|
132
|
+
* first thing to touch it raises its TypeError rather than this module's typed refusal. */
|
|
133
|
+
function _isProbeable(value) {
|
|
134
|
+
try { intrinsic.getPrototypeOf(value); return true; }
|
|
135
|
+
catch (e) { void e; return false; }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** @internal Every fragment is charged to a budget shared across the whole walk, and the budget is what
|
|
139
|
+
* bounds the work. The depth cap bounds the RECURSION and says nothing about the output, because a value
|
|
140
|
+
* that shares a subobject with itself is a graph rather than a tree and each reference is expanded
|
|
141
|
+
* independently: `v = [v, v]` repeated n times is n levels deep and 2 to the n leaves wide. MEASURED on
|
|
142
|
+
* the unbudgeted walk, 24 levels produced 67,108,861 bytes in 1.8 seconds and 25 ended the process with a
|
|
143
|
+
* fatal out-of-memory on a 2 GiB heap, from an input of 25 arrays (CWE-770). Charging each fragment
|
|
144
|
+
* refuses before the next doubling is accumulated, so the cost follows the budget rather than the shape
|
|
145
|
+
* of the input. The bound is the same `C.LIMITS.JSON_MAX_BYTES` a TUF document itself is read under: a
|
|
146
|
+
* canonical form larger than any document this module will parse cannot be the preimage of one. */
|
|
147
|
+
function _emit(out, text, budget) {
|
|
148
|
+
/** @internal The charge is in UTF-8 BYTES, which is the unit the budget is in and the unit the returned
|
|
149
|
+
* buffer is measured in. `text.length` counts UTF-16 code units, so a string outside the Basic Latin
|
|
150
|
+
* range undercharged by its encoded width: MEASURED, 1,048,574 CJK code points produced 3,145,724 bytes
|
|
151
|
+
* against a 1,048,576-byte budget, because each costs three bytes and was charged as one. */
|
|
152
|
+
budget.spend(_byteLength(text, "utf8"), "the canonical form");
|
|
153
|
+
_push(out, text);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function _canonical(value, out, depth, budget) {
|
|
157
|
+
if (depth > CANONICAL_MAX_DEPTH) {
|
|
158
|
+
throw _err("tuf/too-deep", "the document nests deeper than " + CANONICAL_MAX_DEPTH + " levels");
|
|
159
|
+
}
|
|
160
|
+
if (value === null) { _emit(out, "null", budget); return; }
|
|
161
|
+
if (value === true) { _emit(out, "true", budget); return; }
|
|
162
|
+
if (value === false) { _emit(out, "false", budget); return; }
|
|
163
|
+
if (typeof value === "string") { _canonicalString(value, out, budget); return; }
|
|
164
|
+
if (typeof value === "number") {
|
|
165
|
+
/** @internal A float has no canonical form here: the reference encoder refuses one outright, so
|
|
166
|
+
* a document a conforming signer produced contains none. A value past the exactly-representable
|
|
167
|
+
* range is refused for the same reason a narrowed one would be wrong: the digits written would
|
|
168
|
+
* not be the digits signed. */
|
|
169
|
+
if (!_isSafeInteger(value)) {
|
|
170
|
+
throw _err("tuf/bad-input", "canonical JSON encodes only exact integers; " +
|
|
171
|
+
guard.text.showValue(value) + " is not one");
|
|
172
|
+
}
|
|
173
|
+
_emit(out, _numToString(value, 10), budget);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
/** @internal A revoked Proxy throws on EVERY operation, `Array.isArray` among them, so the array test
|
|
177
|
+
* below would raise the Proxy's own TypeError before any check of this module's ran. Probing it once
|
|
178
|
+
* here, inside a guard, makes the refusal this module's own with its own code. */
|
|
179
|
+
if (typeof value === "object" && !_isProbeable(value)) {
|
|
180
|
+
throw _err("tuf/bad-input", "canonical JSON cannot read this value: it is a revoked Proxy, which " +
|
|
181
|
+
"throws on every operation, so there is nothing to encode");
|
|
182
|
+
}
|
|
183
|
+
if (_isArray(value)) {
|
|
184
|
+
_emit(out, "[", budget);
|
|
185
|
+
for (var i = 0; i < value.length; i++) {
|
|
186
|
+
if (i > 0) _emit(out, ",", budget);
|
|
187
|
+
_canonical(value[i], out, depth + 1, budget);
|
|
188
|
+
}
|
|
189
|
+
_emit(out, "]", budget);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (typeof value === "object") {
|
|
193
|
+
/** @internal Canonical JSON admits an object, an array, a string, an integer, a boolean and null.
|
|
194
|
+
* A built-in exotic is none of them, and encoding it as "every own enumerable key" silently made a
|
|
195
|
+
* Date and a Map into `{}` and a Buffer into an index map of its bytes. This output is the preimage
|
|
196
|
+
* a TUF signature covers, so a field encoding to `{}` is a field the signature does not cover while
|
|
197
|
+
* it is still in the caller's object: the signed form and the document the caller believes they
|
|
198
|
+
* signed would differ with nothing reporting it. The encoder already refuses a float, a function,
|
|
199
|
+
* undefined and a BigInt, so admitting these was the same rule applied to part of its range. */
|
|
200
|
+
if (!_isPlainRecordByPrototype(value)) {
|
|
201
|
+
throw _err("tuf/bad-input", "canonical JSON encodes a plain object, an array, a string, an " +
|
|
202
|
+
"integer, a boolean or null; " + guard.text.showValue(value) + " carries a prototype other than " +
|
|
203
|
+
"Object.prototype, so it is a class instance whose state is not in its own keys, and encoding it " +
|
|
204
|
+
"by those keys would sign a different document than the one it stands for");
|
|
205
|
+
}
|
|
206
|
+
var names = _sort(_keys(value), _byCodePoint);
|
|
207
|
+
_emit(out, "{", budget);
|
|
208
|
+
for (var k = 0; k < names.length; k++) {
|
|
209
|
+
if (k > 0) _emit(out, ",", budget);
|
|
210
|
+
_canonicalString(names[k], out, budget);
|
|
211
|
+
_emit(out, ":", budget);
|
|
212
|
+
_canonical(value[names[k]], out, depth + 1, budget);
|
|
213
|
+
}
|
|
214
|
+
_emit(out, "}", budget);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
throw _err("tuf/bad-input", "canonical JSON has no encoding for " + guard.text.showValue(typeof value));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* @primitive pki.tuf.canonicalJson
|
|
222
|
+
* @signature pki.tuf.canonicalJson(value) -> Buffer
|
|
223
|
+
* @since 0.8.41
|
|
224
|
+
* @status stable
|
|
225
|
+
* @spec TUF
|
|
226
|
+
* @related pki.tuf.keyId, pki.tuf.verifySignatures
|
|
227
|
+
*
|
|
228
|
+
* The canonical JSON encoding of a value, as UTF-8 bytes. This is the preimage TUF signatures cover,
|
|
229
|
+
* so it is a wire format and not a formatting choice: object keys are sorted by code point, no
|
|
230
|
+
* whitespace is emitted, and inside a string only backslash and quote are escaped. A literal control
|
|
231
|
+
* character is emitted as itself, which is where `JSON.stringify` differs: it would write `\n` and the
|
|
232
|
+
* signature would be over other bytes.
|
|
233
|
+
*
|
|
234
|
+
* Only strings, exact integers, booleans, null, arrays and plain objects have an encoding. A float,
|
|
235
|
+
* a number outside the exactly-representable integers, `undefined` and a function are refused with
|
|
236
|
+
* `tuf/bad-input` rather than approximated, and a document nesting past `C.LIMITS.JSON_MAX_DEPTH` is
|
|
237
|
+
* `tuf/too-deep`.
|
|
238
|
+
*
|
|
239
|
+
* @example
|
|
240
|
+
* pki.tuf.canonicalJson({ b: 2, a: 1 }).toString("utf8"); // '{"a":1,"b":2}'
|
|
241
|
+
*/
|
|
242
|
+
function canonicalJson(value) {
|
|
243
|
+
var out = [];
|
|
244
|
+
var budget = guard.limits.budget({ maxBytes: constants.LIMITS.JSON_MAX_BYTES });
|
|
245
|
+
try {
|
|
246
|
+
_canonical(value, out, 0, budget);
|
|
247
|
+
} catch (e) {
|
|
248
|
+
if (guard.limits.isBudgetExceeded(e)) {
|
|
249
|
+
throw _err("tuf/too-large", "the canonical form of this document exceeds " +
|
|
250
|
+
constants.LIMITS.JSON_MAX_BYTES + " bytes; a value that shares a subobject with itself expands " +
|
|
251
|
+
"once per reference, so a document of a few dozen fields can encode to gigabytes", e);
|
|
252
|
+
}
|
|
253
|
+
throw e;
|
|
254
|
+
}
|
|
255
|
+
return _bufferFrom(_join(out, ""), "utf8");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* @primitive pki.tuf.keyId
|
|
260
|
+
* @signature pki.tuf.keyId(key) -> string
|
|
261
|
+
* @since 0.8.41
|
|
262
|
+
* @status stable
|
|
263
|
+
* @spec TUF
|
|
264
|
+
* @related pki.tuf.canonicalJson, pki.tuf.verifySignatures
|
|
265
|
+
*
|
|
266
|
+
* A key's identifier: the lowercase hex SHA-256 of the key's own canonical JSON form. The
|
|
267
|
+
* specification says a client must calculate each identifier to check it is correct for its key,
|
|
268
|
+
* which is what stops a document listing one key under another's identifier, so this is a
|
|
269
|
+
* verification input rather than a label.
|
|
270
|
+
*
|
|
271
|
+
* A key object carries `keytype`, `scheme` and `keyval`; a value missing any of them is `tuf/bad-key`.
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* pki.tuf.keyId({ keytype: "ed25519", scheme: "ed25519",
|
|
275
|
+
* keyval: { public: "00".repeat(32) } }).length; // -> 64
|
|
276
|
+
*/
|
|
277
|
+
function keyId(key) {
|
|
278
|
+
if (!key || typeof key !== "object" || _isArray(key)) {
|
|
279
|
+
throw _err("tuf/bad-key", "a TUF key is an object carrying keytype, scheme and keyval");
|
|
280
|
+
}
|
|
281
|
+
/** @internal The key is COPIED before it is validated, and the copy is what gets hashed. Validating the
|
|
282
|
+
* caller's object and then handing that same object to `canonicalJson` is two reads of every field, and
|
|
283
|
+
* an accessor answers each separately: a key could validate as `keytype: "ed25519"` and be hashed as
|
|
284
|
+
* `keytype: 42`, so the identifier returned would name a shape that never passed validation. The
|
|
285
|
+
* identifier is the identity a role's `keyids` list matches against, which is what makes that a
|
|
286
|
+
* question of which key is authorized rather than of tidiness.
|
|
287
|
+
*
|
|
288
|
+
* `snapshotDeep` refuses an accessor-backed member outright rather than reading it once, which is the
|
|
289
|
+
* stronger answer and the one `verifySignatures` already takes for the same records. */
|
|
290
|
+
var copied = guard.bytes.snapshotDeep(key, TufError, "tuf/bad-key", "a TUF key");
|
|
291
|
+
if (typeof copied.keytype !== "string" || typeof copied.scheme !== "string" ||
|
|
292
|
+
!copied.keyval || typeof copied.keyval !== "object") {
|
|
293
|
+
throw _err("tuf/bad-key", "a TUF key carries keytype, scheme and keyval; one of them is missing " +
|
|
294
|
+
"or is not of its stated type");
|
|
295
|
+
}
|
|
296
|
+
/** @internal Through the captured digest, which is what binds a key to the identifier a role names.
|
|
297
|
+
* Reached off the live hash prototype, a replacement decided that binding: a different Ed25519 key
|
|
298
|
+
* filed under an authorized key's identifier was accepted, and metadata it signed reported
|
|
299
|
+
* `verified: true` where the check otherwise throws `tuf/bad-key`. */
|
|
300
|
+
return _bufToString(guard.crypto.digest("sha256", canonicalJson(copied)), "hex");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** @internal The major version this build implements, as the TEXT a conforming document carries. The
|
|
304
|
+
* comparison is made on that text rather than on a converted number, so there is no width a major
|
|
305
|
+
* version may not have: converted, a long one needed a digit cap, and the cap reported `1000000.0.0`
|
|
306
|
+
* as malformed when it is a well-formed version this build simply does not implement. A numeric
|
|
307
|
+
* identifier carries no leading zeros, so one spelling per value and a text comparison is exact. */
|
|
308
|
+
var SUPPORTED_SPEC_MAJOR = "1";
|
|
309
|
+
|
|
310
|
+
/** @internal Does an RFC 3339 date-time state UTC? `Z` and `+00:00` do: RFC 3339 sec. 4.3 says those two
|
|
311
|
+
* "imply that UTC is the preferred reference point for the specified time", which is the assertion the
|
|
312
|
+
* TUF specification sec. 4.2.3 makes when it says time is always in UTC. `-00:00` is reserved by that
|
|
313
|
+
* same clause for "the time in UTC is known, but the offset to local time is unknown", which it calls
|
|
314
|
+
* semantically different, so it names the right instant while declining to state UTC as the reference
|
|
315
|
+
* and is refused here. Every other offset states a different reference point. The string has already
|
|
316
|
+
* passed the RFC 3339 scanner, so this reads only the offset at its end. */
|
|
317
|
+
function _isUtcOffset(v) {
|
|
318
|
+
var n = v.length;
|
|
319
|
+
if (n === 0) return false;
|
|
320
|
+
var last = _charCodeAt(v, n - 1);
|
|
321
|
+
if (last === 0x5a || last === 0x7a) return true;
|
|
322
|
+
if (n < 6) return false;
|
|
323
|
+
if (_charCodeAt(v, n - 6) !== 0x2b) return false;
|
|
324
|
+
return _charCodeAt(v, n - 5) === 0x30 && _charCodeAt(v, n - 4) === 0x30 &&
|
|
325
|
+
_charCodeAt(v, n - 3) === 0x3a &&
|
|
326
|
+
_charCodeAt(v, n - 2) === 0x30 && last === 0x30;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** @internal A semver NUMERIC IDENTIFIER starting at `i`: the index after it, or -1. "A numeric
|
|
330
|
+
* identifier MUST NOT include leading zeroes", so "0" is one and "01" is not. */
|
|
331
|
+
function _numericId(v, i, n) {
|
|
332
|
+
if (i >= n) return -1;
|
|
333
|
+
var first = _charCodeAt(v, i);
|
|
334
|
+
if (first < 0x30 || first > 0x39) return -1;
|
|
335
|
+
var j = i + 1;
|
|
336
|
+
while (j < n) { var c = _charCodeAt(v, j); if (c < 0x30 || c > 0x39) break; j++; }
|
|
337
|
+
if (first === 0x30 && j - i > 1) return -1;
|
|
338
|
+
return j;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** @internal The dot-separated identifiers of a pre-release or build part: the index after the last
|
|
342
|
+
* one, or -1. Each is alphanumerics and hyphens and may not be empty. The parts differ in one rule:
|
|
343
|
+
* a pre-release identifier that is all digits "MUST NOT include leading zeroes", while a build
|
|
344
|
+
* identifier carries no such restriction, so `1.2.3+0010` is a version and `1.2.3-0010` is not. */
|
|
345
|
+
function _dotSeparatedIds(v, i, n, isPreRelease) {
|
|
346
|
+
for (;;) {
|
|
347
|
+
var start = i, allDigits = true;
|
|
348
|
+
while (i < n) {
|
|
349
|
+
var c = _charCodeAt(v, i);
|
|
350
|
+
var digit = c >= 0x30 && c <= 0x39;
|
|
351
|
+
if (!digit && !((c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a) || c === 0x2d)) break;
|
|
352
|
+
if (!digit) allDigits = false;
|
|
353
|
+
i++;
|
|
354
|
+
}
|
|
355
|
+
if (i === start) return -1;
|
|
356
|
+
if (isPreRelease && allDigits && _charCodeAt(v, start) === 0x30 && i - start > 1) return -1;
|
|
357
|
+
if (i < n && _charCodeAt(v, i) === 0x2e) { i++; continue; }
|
|
358
|
+
return i;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** @internal The MAJOR component of a semantic version, or null where the string is not one. The WHOLE
|
|
363
|
+
* string is held to the grammar, because the specification sec. 4.3 states the field's format: "A
|
|
364
|
+
* string that contains the version number of the TUF specification. Its format follows the Semantic
|
|
365
|
+
* Versioning 2.0.0 (semver) specification". Reading the leading digits alone accepted a field that is
|
|
366
|
+
* not a version at all: "1.", "1.0", "1.foo", "1.0.0oops", "1.0.0.0" and a trailing space all passed
|
|
367
|
+
* as supported 1.x metadata. Scanned by character rather than split or matched, `lib/` taking no
|
|
368
|
+
* regular expressions.
|
|
369
|
+
*
|
|
370
|
+
* Shape: three numeric identifiers separated by dots, then an optional "-" pre-release and an
|
|
371
|
+
* optional "+" build, and nothing after either. */
|
|
372
|
+
function _specMajor(v) {
|
|
373
|
+
var n = v.length;
|
|
374
|
+
var end = _numericId(v, 0, n);
|
|
375
|
+
if (end < 0) return null;
|
|
376
|
+
var major = _strSlice(v, 0, end);
|
|
377
|
+
var i = end;
|
|
378
|
+
for (var part = 0; part < 2; part++) {
|
|
379
|
+
if (i >= n || _charCodeAt(v, i) !== 0x2e) return null;
|
|
380
|
+
i = _numericId(v, i + 1, n);
|
|
381
|
+
if (i < 0) return null;
|
|
382
|
+
}
|
|
383
|
+
if (i === n) return major;
|
|
384
|
+
if (_charCodeAt(v, i) === 0x2d) {
|
|
385
|
+
i = _dotSeparatedIds(v, i + 1, n, true);
|
|
386
|
+
if (i < 0) return null;
|
|
387
|
+
if (i === n) return major;
|
|
388
|
+
}
|
|
389
|
+
if (_charCodeAt(v, i) !== 0x2b) return null;
|
|
390
|
+
i = _dotSeparatedIds(v, i + 1, n, false);
|
|
391
|
+
return i === n ? major : null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** @internal The specification version decides whether this build's rules apply to the document at all:
|
|
395
|
+
* "Metadata is written according to version "spec_version" of the specification, and clients MUST verify
|
|
396
|
+
* that "spec_version" matches the expected version number", with adopters free to decide what counts as
|
|
397
|
+
* a match. This build implements 1.x, so the MAJOR version is the match and the minor and patch are not:
|
|
398
|
+
* a 1.x document a later minor version produced is read under rules that are a subset of its own, which
|
|
399
|
+
* is what semantic versioning promises, while a 2.x document is written to rules this build does not
|
|
400
|
+
* have. A document with no `spec_version` at all states no version to match, and TAP 6 makes the field
|
|
401
|
+
* mandatory, so it is refused rather than assumed to be this one.
|
|
402
|
+
*
|
|
403
|
+
* The clause binds metadata rather than root metadata, so it holds for every role, and it is enforced
|
|
404
|
+
* at this one door every document comes through. The verbs a client drives next answer under 1.x rules:
|
|
405
|
+
* a key identifier is a hash of the canonical form this version defines, and an expiry is read in the
|
|
406
|
+
* format this version states. A verdict from either about a document written to another major version
|
|
407
|
+
* is an answer about rules the document does not claim. The validated string is returned so the check
|
|
408
|
+
* and the reported value are one read.
|
|
409
|
+
*
|
|
410
|
+
* It runs as soon as the signed body is reachable, ahead of the field rules below it, because `_type`,
|
|
411
|
+
* `version` and `expires` are themselves rules of this version: reading them first decides a document
|
|
412
|
+
* under rules it may not be written to. What precedes it is the JSON read and the wrapper shape, since
|
|
413
|
+
* the document has to tokenize and the body has to exist before the field inside it can be read. A
|
|
414
|
+
* document carrying a fractional number is therefore `tuf/bad-json` whatever version it declares, the
|
|
415
|
+
* canonical form admitting no floating point at any version this build reads. */
|
|
416
|
+
function _assertSupportedMajor(spec, where, malformedCode) {
|
|
417
|
+
if (typeof spec !== "string") {
|
|
418
|
+
throw _err(malformedCode, where + " names no spec_version, which a client must match against the " +
|
|
419
|
+
"version it implements (TUF specification sec. 4.3, TAP 6)");
|
|
420
|
+
}
|
|
421
|
+
/** @internal A value that is not a semantic version is MALFORMED rather than a version this build
|
|
422
|
+
* does not implement, and the two are different answers: the first says the field is not a version,
|
|
423
|
+
* the second that the version is one this build cannot read. Only a well-formed version reaches the
|
|
424
|
+
* comparison. */
|
|
425
|
+
var major = _specMajor(spec);
|
|
426
|
+
if (major === null) {
|
|
427
|
+
throw _err(malformedCode, where + " states spec_version " + guard.text.showValue(spec) +
|
|
428
|
+
", which is not a Semantic Versioning 2.0.0 value, the format the TUF specification sec. 4.3 " +
|
|
429
|
+
"states for the field");
|
|
430
|
+
}
|
|
431
|
+
if (major !== SUPPORTED_SPEC_MAJOR) {
|
|
432
|
+
throw _err("tuf/unsupported-spec-version", where + " declares spec_version " +
|
|
433
|
+
guard.text.showValue(spec) + ", and this build implements " + SUPPORTED_SPEC_MAJOR +
|
|
434
|
+
".x: a document written to another major version states rules this build does not have");
|
|
435
|
+
}
|
|
436
|
+
return spec;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** @internal The fields the two object-taking verbs read off a metadata object. None may be an
|
|
440
|
+
* accessor: see the refusal in `verifySignatures` for what reading one lets a caller steer. */
|
|
441
|
+
var _METADATA_FIELDS = ["signed", "signedBytes", "specVersion", "expires", "type", "version",
|
|
442
|
+
"signatures"];
|
|
443
|
+
/** @internal The fields a signature record carries, held to the same rule for the same reason. */
|
|
444
|
+
var _SIGNATURE_FIELDS = ["keyid", "sig"];
|
|
445
|
+
|
|
446
|
+
/** @internal The convenience copies a parsed object carries beside the signed body, as
|
|
447
|
+
* [outer field, the body member it copies]. Each is checked against the body rather than trusted,
|
|
448
|
+
* because the body is what the signature covers and a copy that disagrees describes a document the
|
|
449
|
+
* caller does not hold. Checking ONE of them was the defect: `specVersion` was cross-checked while
|
|
450
|
+
* `type` and `version` were not, and a caller reads `version` for a rollback comparison and `type` to
|
|
451
|
+
* choose which role a document belongs to, so both decide something. Absent is accepted, an object
|
|
452
|
+
* assembled by hand being free to carry only what it uses; present and contradicting is refused. */
|
|
453
|
+
var _BODY_COPIES = [["type", "_type"], ["specVersion", "spec_version"], ["version", "version"],
|
|
454
|
+
["expires", "expires"]];
|
|
455
|
+
|
|
456
|
+
function _assertCopiesAgree(metadata, body, label) {
|
|
457
|
+
for (var ci = 0; ci < _BODY_COPIES.length; ci++) {
|
|
458
|
+
var outer = _BODY_COPIES[ci][0], inner = _BODY_COPIES[ci][1];
|
|
459
|
+
var stated = metadata[outer];
|
|
460
|
+
if (stated === undefined || stated === null) continue;
|
|
461
|
+
if (stated !== body[inner]) {
|
|
462
|
+
throw _err("tuf/bad-input", label + " states " + outer + " " + guard.text.showValue(stated) +
|
|
463
|
+
" while the signed body it carries states " + guard.text.showValue(body[inner]));
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** @internal The roles a root states a record for. `mirrors` is deliberately absent: the
|
|
469
|
+
* specification sec. 4.3 calls it OPTIONAL where it calls these four mandatory. */
|
|
470
|
+
var TOP_LEVEL_ROLES = ["root", "targets", "snapshot", "timestamp"];
|
|
471
|
+
/** @internal The same four as text for the refusal, joined through the capture at load so the list has
|
|
472
|
+
* one source. */
|
|
473
|
+
var TOP_LEVEL_ROLES_TEXT = intrinsic.join(TOP_LEVEL_ROLES, ", ");
|
|
474
|
+
|
|
475
|
+
/** @internal The parse's own use of the rule, over the raw field, read once. */
|
|
476
|
+
function _assertSpecVersion(signed) {
|
|
477
|
+
return _assertSupportedMajor(signed.spec_version, "the signed body", "tuf/bad-metadata");
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** @internal The rule carried to the two verbs that take a metadata OBJECT rather than bytes. Both are
|
|
481
|
+
* documented as taking what `parseMetadata` returns, and both are reachable with one a caller
|
|
482
|
+
* assembled, which is why `checkExpiry` already re-enforces the `expires` format rather than trusting
|
|
483
|
+
* it. The version match is the same situation: without it, each answered about a document declaring a
|
|
484
|
+
* version this build does not implement, `verifySignatures` reporting `verified` and `checkExpiry`
|
|
485
|
+
* returning true. Absence is the caller's object being incomplete rather than a document being
|
|
486
|
+
* malformed, so it is `tuf/bad-input` here and `tuf/bad-metadata` at the parse; an unsupported version
|
|
487
|
+
* is the same code on every route. */
|
|
488
|
+
function _assertMetadataSpecVersion(spec) {
|
|
489
|
+
return _assertSupportedMajor(spec, "opts.metadata", "tuf/bad-input");
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* @primitive pki.tuf.parseMetadata
|
|
495
|
+
* @signature pki.tuf.parseMetadata(input) -> { type, specVersion, version, expires, signed, signatures, signedBytes }
|
|
496
|
+
* @since 0.8.41
|
|
497
|
+
* @status stable
|
|
498
|
+
* @spec TUF
|
|
499
|
+
* @related pki.tuf.verifySignatures, pki.tuf.checkExpiry
|
|
500
|
+
*
|
|
501
|
+
* Read a TUF metadata document: the `signatures` wrapper and the `signed` body, with `signedBytes`
|
|
502
|
+
* the canonical form the signatures cover. A duplicate JSON member is refused before any field is
|
|
503
|
+
* read, because the canonical form would carry only one of them and the signature would then cover a
|
|
504
|
+
* document different from the one delivered.
|
|
505
|
+
*
|
|
506
|
+
* `_type`, a `spec_version`, a `version` that is a positive integer, and an `expires` that parses as a
|
|
507
|
+
* date-time are required; a document missing any is `tuf/bad-metadata`.
|
|
508
|
+
*
|
|
509
|
+
* `spec_version` must name a specification version this build implements, which is 1.x, and the major
|
|
510
|
+
* component is the match: a later 1.x minor is read, another major version is `tuf/unsupported-spec-
|
|
511
|
+
* version`. The rule is here rather than in each verb because a key identifier and an expiry are both
|
|
512
|
+
* read under the rules of this version.
|
|
513
|
+
*
|
|
514
|
+
* @example
|
|
515
|
+
* var m = pki.tuf.parseMetadata(Buffer.from(JSON.stringify({ signatures: [],
|
|
516
|
+
* signed: { _type: "root", spec_version: "1.0.31", version: 1, expires: "2030-01-01T00:00:00Z" } })));
|
|
517
|
+
* m.type; // -> "root"
|
|
518
|
+
*/
|
|
519
|
+
function parseMetadata(input) {
|
|
520
|
+
var bytes = guard.bytes.source(input, TufError, "tuf/bad-input", "the TUF metadata");
|
|
521
|
+
var doc = guard.json.parse(bytes, _err, {
|
|
522
|
+
maxBytes: constants.LIMITS.JSON_MAX_BYTES, maxDepth: constants.LIMITS.JSON_MAX_DEPTH,
|
|
523
|
+
badJson: "tuf/bad-json", tooDeep: "tuf/too-deep", duplicateMember: "tuf/duplicate-member",
|
|
524
|
+
tooLarge: "tuf/too-large", badInput: "tuf/bad-input", label: "the TUF metadata",
|
|
525
|
+
/** @internal TUF's canonical JSON admits no floating point, and every number the format carries is a
|
|
526
|
+
* version, a threshold or a length. A fractional token is refused while it is still text: converted
|
|
527
|
+
* first, 1.0000000000000001 becomes 1 and the integer checks below see a conforming document. */
|
|
528
|
+
integersOnly: true,
|
|
529
|
+
});
|
|
530
|
+
if (!doc || typeof doc !== "object" || _isArray(doc)) {
|
|
531
|
+
throw _err("tuf/bad-metadata", "TUF metadata is a JSON object carrying signatures and signed");
|
|
532
|
+
}
|
|
533
|
+
if (!_isArray(doc.signatures)) {
|
|
534
|
+
throw _err("tuf/bad-metadata", "TUF metadata carries a signatures array");
|
|
535
|
+
}
|
|
536
|
+
var signed = doc.signed;
|
|
537
|
+
if (!signed || typeof signed !== "object" || _isArray(signed)) {
|
|
538
|
+
throw _err("tuf/bad-metadata", "TUF metadata carries a signed object");
|
|
539
|
+
}
|
|
540
|
+
var specVersion = _assertSpecVersion(signed);
|
|
541
|
+
if (typeof signed._type !== "string" || signed._type.length === 0) {
|
|
542
|
+
throw _err("tuf/bad-metadata", "the signed body names no _type");
|
|
543
|
+
}
|
|
544
|
+
if (typeof signed.version !== "number" || !_isSafeInteger(signed.version) || signed.version < 1) {
|
|
545
|
+
throw _err("tuf/bad-metadata", "the signed body's version is not a positive integer");
|
|
546
|
+
}
|
|
547
|
+
/** @internal The OFFSET has to denote UTC, which the shared scanner does not decide: RFC 3339 admits
|
|
548
|
+
* every numeric offset, so `2030-01-01T01:00:00+01:00` passes it. Section 4.2.3 says "Time is always
|
|
549
|
+
* in UTC", and such a value names a real instant while not being the form the specification requires,
|
|
550
|
+
* so a conforming client refuses the document and adopting it here would trust metadata that client
|
|
551
|
+
* would not. `Z` and a zero numeric offset denote the same instant and both pass. */
|
|
552
|
+
/** @internal The FORMAT decides, not `Date.parse`. The specification sec. 4.2.3 states it exactly:
|
|
553
|
+
* "Metadata date-time follows the ISO 8601 standard. The expected format of the combined date and time
|
|
554
|
+
* string is "YYYY-MM-DDTHH:MM:SSZ". Time is always in UTC". `Date.parse` is far looser. It ROLLS an
|
|
555
|
+
* impossible date forward rather than refusing it, so an expires of 2026-02-30 read as March 2 and the
|
|
556
|
+
* metadata was usable for two days it does not state; and it accepts a date with no time and a time
|
|
557
|
+
* with no zone, where the instant then depends on the reader's own timezone rather than on the
|
|
558
|
+
* document. The shared RFC 3339 scanner answers the format, calendar validity and leap years included.
|
|
559
|
+
* A zero numeric offset is accepted along with `Z`, both denoting the same instant: the clause states
|
|
560
|
+
* the expected form rather than forbidding an equivalent one. */
|
|
561
|
+
if (typeof signed.expires !== "string" || !rfc3339.isValid(signed.expires) || !_isUtcOffset(signed.expires)) {
|
|
562
|
+
throw _err("tuf/bad-metadata", "the signed body's expires is not an RFC 3339 date-time in the " +
|
|
563
|
+
"form the TUF specification sec. 4.2.3 states, \"YYYY-MM-DDTHH:MM:SSZ\"");
|
|
564
|
+
}
|
|
565
|
+
var sigs = [];
|
|
566
|
+
for (var i = 0; i < doc.signatures.length; i++) {
|
|
567
|
+
var s = doc.signatures[i];
|
|
568
|
+
if (!s || typeof s !== "object" || typeof s.keyid !== "string" || typeof s.sig !== "string") {
|
|
569
|
+
throw _err("tuf/bad-metadata", "signatures[" + i + "] carries a keyid and a sig, both strings");
|
|
570
|
+
}
|
|
571
|
+
_push(sigs, { keyid: s.keyid, sig: s.sig });
|
|
572
|
+
}
|
|
573
|
+
return guard.verdict.of({
|
|
574
|
+
type: signed._type,
|
|
575
|
+
specVersion: specVersion,
|
|
576
|
+
version: signed.version,
|
|
577
|
+
expires: signed.expires,
|
|
578
|
+
signed: signed,
|
|
579
|
+
signatures: sigs,
|
|
580
|
+
signedBytes: canonicalJson(signed),
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function _isFiniteTime(ms) { return typeof ms === "number" && intrinsic.numberIsFinite(ms); }
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* @primitive pki.tuf.checkExpiry
|
|
588
|
+
* @signature pki.tuf.checkExpiry(metadata, now) -> true
|
|
589
|
+
* @since 0.8.41
|
|
590
|
+
* @status stable
|
|
591
|
+
* @spec TUF
|
|
592
|
+
* @related pki.tuf.parseMetadata, pki.tuf.updateRoot
|
|
593
|
+
*
|
|
594
|
+
* Assert that metadata has not expired at `now`, which is the freeze-attack check: a repository that
|
|
595
|
+
* stops publishing leaves a client holding metadata that stays valid forever unless its expiry is
|
|
596
|
+
* read. Returns `true`, or throws `tuf/expired` naming both instants. The instant is a caller value,
|
|
597
|
+
* never the system clock read inside the check, so a verdict is reproducible.
|
|
598
|
+
*
|
|
599
|
+
* The expiry read is `metadata.signed.expires`, the field the signature covers, and not the `expires`
|
|
600
|
+
* beside it: the signature check and this one have to be about one document, and an object carrying a
|
|
601
|
+
* lapsed expiry in the body and a future one in the copy otherwise verified and passed here at the
|
|
602
|
+
* same instant. Every copy the object states beside the body must agree with it, `type`, `version`,
|
|
603
|
+
* `expires` and `specVersion` alike, and so must a `signedBytes` it carries; any of them contradicting
|
|
604
|
+
* the body is `tuf/bad-input`. An object carrying no `signedBytes` is still read, a caller being free
|
|
605
|
+
* to ask only about an expiry.
|
|
606
|
+
*
|
|
607
|
+
* The body's `spec_version` must name a specification version this build implements, the expiry format
|
|
608
|
+
* being one this version states. A document `parseMetadata` returned always carries it; an object
|
|
609
|
+
* assembled by hand without it is `tuf/bad-input`, and one naming another major version is
|
|
610
|
+
* `tuf/unsupported-spec-version`.
|
|
611
|
+
*
|
|
612
|
+
* @example
|
|
613
|
+
* var meta = pki.tuf.parseMetadata(Buffer.from(JSON.stringify({ signatures: [],
|
|
614
|
+
* signed: { _type: "root", spec_version: "1.0.31", version: 1, expires: "2030-01-01T00:00:00Z" } })));
|
|
615
|
+
* pki.tuf.checkExpiry(meta, new Date("2027-01-01T00:00:00Z")); // -> true
|
|
616
|
+
*/
|
|
617
|
+
function checkExpiry(metadata, now) {
|
|
618
|
+
if (!metadata || typeof metadata !== "object") {
|
|
619
|
+
throw _err("tuf/bad-input", "checkExpiry takes a parsed metadata object from pki.tuf.parseMetadata");
|
|
620
|
+
}
|
|
621
|
+
/** @internal ONE read of the signed BODY, and the expiry is taken from it rather than from the
|
|
622
|
+
* `expires` beside it. The signature covers the body, so those are two values: an object carrying a
|
|
623
|
+
* lapsed expiry in the body and a future one in the copy had `verifySignatures` answer about the
|
|
624
|
+
* signed document while this answered about the copy, and metadata correctly signed with an expiry
|
|
625
|
+
* in 2020 then both verified and passed this check at a 2026 instant. Expiry is the freeze-attack
|
|
626
|
+
* check, so the two verbs have to be about one document. A copy the caller also states must agree
|
|
627
|
+
* with the body, and `snapshotDeep` refuses an accessor-backed member outright, so no field can
|
|
628
|
+
* answer the comparison differently from the check. The fields of the object itself are held to the
|
|
629
|
+
* same rule, for the reason the refusal in `verifySignatures` gives, and so is the object itself
|
|
630
|
+
* being a plain record rather than a Proxy. */
|
|
631
|
+
guard.identifier.assertPlainRecord(metadata, _err, "tuf/bad-input", "opts.metadata");
|
|
632
|
+
guard.identifier.refuseAccessorFields(metadata, _METADATA_FIELDS, _err, "tuf/bad-input",
|
|
633
|
+
"opts.metadata");
|
|
634
|
+
var body = guard.bytes.snapshotDeep(metadata.signed, TufError, "tuf/bad-input", "metadata.signed");
|
|
635
|
+
if (!body || typeof body !== "object" || _isArray(body)) {
|
|
636
|
+
throw _err("tuf/bad-input", "checkExpiry takes a parsed metadata object from pki.tuf.parseMetadata");
|
|
637
|
+
}
|
|
638
|
+
var expires = body.expires;
|
|
639
|
+
var type = body._type;
|
|
640
|
+
_assertMetadataSpecVersion(body.spec_version);
|
|
641
|
+
if (typeof expires !== "string") {
|
|
642
|
+
throw _err("tuf/bad-input", "checkExpiry takes a parsed metadata object from pki.tuf.parseMetadata");
|
|
643
|
+
}
|
|
644
|
+
_assertCopiesAgree(metadata, body, "the metadata object");
|
|
645
|
+
/** @internal `signedBytes` is a copy of the body too, in bytes rather than in a field, and the two
|
|
646
|
+
* verbs have to agree about which objects they answer for at all: this read the body while the bytes
|
|
647
|
+
* beside it encoded another document, and returned true for it. This verb does not USE those bytes,
|
|
648
|
+
* which is why it went unnoticed, and that is the point, since an object the signature check refuses
|
|
649
|
+
* should not be one the expiry check answers about. Absent bytes stay acceptable, a caller being
|
|
650
|
+
* free to ask only about an expiry. */
|
|
651
|
+
var statedBytes = metadata.signedBytes;
|
|
652
|
+
if (statedBytes !== undefined && statedBytes !== null) {
|
|
653
|
+
/** @internal Bounded before the copy, as `verifySignatures` bounds the same field: these bytes must
|
|
654
|
+
* equal the canonical form of a body that came from a document no larger than the cap, so anything
|
|
655
|
+
* above it cannot match and copying it first paid an allocation the size of whatever arrived to
|
|
656
|
+
* reach that conclusion. */
|
|
657
|
+
var statedView = guard.bytes.isByteSource(statedBytes)
|
|
658
|
+
? guard.bytes.source(statedBytes, _err, "tuf/bad-input", "metadata.signedBytes") : null;
|
|
659
|
+
if (statedView !== null) {
|
|
660
|
+
guard.limits.byteCap(statedView, constants.LIMITS.JSON_MAX_BYTES, _err, "tuf/too-large",
|
|
661
|
+
"metadata.signedBytes");
|
|
662
|
+
}
|
|
663
|
+
if (statedView === null ||
|
|
664
|
+
!guard.crypto.constantTimeEqual(canonicalJson(body),
|
|
665
|
+
guard.bytes.snapshot(statedView, _err, "tuf/bad-input", "metadata.signedBytes"))) {
|
|
666
|
+
throw _err("tuf/bad-input", "metadata.signedBytes is not the canonical form of metadata.signed, " +
|
|
667
|
+
"so the expiry asked about is not the document the object carries");
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
if (!guard.time.isDate(now)) throw _err("tuf/bad-input", "now must be a Date");
|
|
671
|
+
var atMs = guard.time.instantOf(now);
|
|
672
|
+
/** @internal The same format rule as the parse, the UTC offset included, because this verb is reachable
|
|
673
|
+
* with a metadata object a caller assembled rather than one `parseMetadata` produced: the looser
|
|
674
|
+
* reading is what rolled an impossible date forward into extra days of trust, and a rule enforced at
|
|
675
|
+
* one of two doors is a document this verb accepts and that one refuses. */
|
|
676
|
+
if (!rfc3339.isValid(expires) || !_isUtcOffset(expires)) {
|
|
677
|
+
throw _err("tuf/bad-metadata", "the metadata's expires is not an RFC 3339 date-time in the form the " +
|
|
678
|
+
"TUF specification sec. 4.2.3 states, \"YYYY-MM-DDTHH:MM:SSZ\"");
|
|
679
|
+
}
|
|
680
|
+
var expiresMs = _dateParse(expires);
|
|
681
|
+
if (!_isFiniteTime(expiresMs)) throw _err("tuf/bad-metadata", "the metadata's expires is not a date-time");
|
|
682
|
+
if (!(atMs < expiresMs)) {
|
|
683
|
+
throw _err("tuf/expired", "the " + type + " metadata expired at " + expires +
|
|
684
|
+
", which is at or before the instant it was checked at");
|
|
685
|
+
}
|
|
686
|
+
return true;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/** @internal The SPKI an ed25519 TUF key encodes, with the raw length and the point validated. Shared
|
|
690
|
+
* by the verify and by adoption, so a root naming a key that is the wrong length or not a full-order
|
|
691
|
+
* point is refused in both places rather than in whichever one happens to read it. */
|
|
692
|
+
function _ed25519Spki(key) {
|
|
693
|
+
var raw = guard.encoding.hex(key.keyval.public, 64, _err, "tuf/bad-key", "an ed25519 public key");
|
|
694
|
+
if (raw.length !== 32) {
|
|
695
|
+
throw _err("tuf/bad-key", "an ed25519 public key is 32 bytes, got " + raw.length);
|
|
696
|
+
}
|
|
697
|
+
var spki = _bufferConcat([ED25519_SPKI_PREFIX, raw]);
|
|
698
|
+
edwardsPoint.validateSpki(spki, ED25519_CRV, TufError, "tuf/bad-key");
|
|
699
|
+
return spki;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/** @internal Everything that must hold for a key to verify anything, with no signature involved: the
|
|
703
|
+
* keytype and scheme are ones this build reads, the material imports, the declared type matches what
|
|
704
|
+
* imported, the curve and modulus match the scheme, and an ed25519 point is on the curve. An
|
|
705
|
+
* identifier is a hash of the key's canonical form and says nothing about any of that, so a role
|
|
706
|
+
* could name a key filed under its own identifier that verifies nothing. Only the ROOT role is
|
|
707
|
+
* verified during a rotation, so such a key in any other role was adopted and the client found out at
|
|
708
|
+
* first use. The same checks the verify makes before it looks at a signature, made at adoption.
|
|
709
|
+
* THE ONE IMPORT, AND THE ONE PLACE A SIGNATURE UNDER IT IS CHECKED. The identity a threshold counts
|
|
710
|
+
* and the key a signature is checked under come from this single import, so they cannot be two
|
|
711
|
+
* different keys and `keyval.public` is read once. Derived and verified separately, a key was imported
|
|
712
|
+
* twice per identifier, which doubled the asymmetric work a document can ask for before anything had
|
|
713
|
+
* authenticated it. `check` closes over the imported key and over the algorithm the declaration
|
|
714
|
+
* resolved, so no caller re-reads the record's own `keytype` to decide how to verify, and the
|
|
715
|
+
* low-order point gate on the ed25519 arm sits in the same function as the verify it protects rather
|
|
716
|
+
* than one call away from it. */
|
|
717
|
+
function _usableKey(key) {
|
|
718
|
+
var d = _declarationOf(key);
|
|
719
|
+
var imported = _checkedPublicKey(key, d);
|
|
720
|
+
if (d.nodeType === "ed25519") {
|
|
721
|
+
imported = guard.crypto.publicKey({ key: _ed25519Spki(key), format: "der", type: "spki" });
|
|
722
|
+
return { d: d, key: imported, check: function (sig, preimage) {
|
|
723
|
+
return guard.crypto.verify(null, preimage, imported, sig);
|
|
724
|
+
} };
|
|
725
|
+
}
|
|
726
|
+
if (d.nodeType === "ec") {
|
|
727
|
+
return { d: d, key: imported, check: function (sig, preimage) {
|
|
728
|
+
return guard.crypto.verify("sha256", preimage, { key: imported, dsaEncoding: "der" }, sig);
|
|
729
|
+
} };
|
|
730
|
+
}
|
|
731
|
+
/** @internal The rsa arm. The scheme the specification pairs with an rsa key is RSASSA-PSS over
|
|
732
|
+
* SHA-256, so that is the padding a signature is checked under; reading the padding from the document
|
|
733
|
+
* would let it name the one its forgery happens to satisfy. Every other keytype was refused by
|
|
734
|
+
* `_declarationOf`, whose table is the one list of what this build reads.
|
|
735
|
+
* THE SALT LENGTH IS READ FROM THE SIGNATURE, not pinned, and that is the one part of this the
|
|
736
|
+
* scheme name does not fix. `rsassa-pss-sha256` states the hash and the padding and says nothing
|
|
737
|
+
* about the salt, which is the signer's to choose, and the reference implementation signs with the
|
|
738
|
+
* longest salt the modulus allows. Pinning the digest length rejected those, so metadata that
|
|
739
|
+
* verifies everywhere else failed here and the root rotation it carried stopped. The salt is public
|
|
740
|
+
* and carried in the encoding, so recovering it concedes nothing: what a verifier must not take from
|
|
741
|
+
* the document is the hash or the padding, and both are still fixed here. Elsewhere in the toolkit
|
|
742
|
+
* the salt length IS pinned, because every other format declares it and the declaration is
|
|
743
|
+
* authenticated: `cms-verify` reads it from the RSASSA-PSS AlgorithmIdentifier, `path-validate` from
|
|
744
|
+
* the certificate, `jose` from the RFC 7518 row. TUF declares none, so there is nothing to read. */
|
|
745
|
+
return { d: d, key: imported, check: function (sig, preimage) {
|
|
746
|
+
return guard.crypto.verify("sha256", preimage, {
|
|
747
|
+
key: imported, padding: guard.crypto.padding.RSA_PKCS1_PSS,
|
|
748
|
+
saltLength: guard.crypto.saltLength.AUTO,
|
|
749
|
+
}, sig);
|
|
750
|
+
} };
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/** @internal The keytype and scheme a key declares, and what the key material must then be. A key's
|
|
754
|
+
* identifier is the hash of the whole key object, so `keytype` and `scheme` are as authenticated as the
|
|
755
|
+
* material itself, and verification has to run under the algorithm they name rather than under whatever
|
|
756
|
+
* the material happens to support. Importing a PEM and verifying does the latter: an RSA public key
|
|
757
|
+
* filed under `keytype: "ecdsa"` imports fine and then satisfies an RSA PKCS#1 v1.5 signature, so a
|
|
758
|
+
* forger picks the algorithm by choosing the key they publish. The pairs are the ones the specification
|
|
759
|
+
* lists in section 4.2.2; `ecdsa-sha2-nistp256` is also accepted as a keytype, which is the spelling
|
|
760
|
+
* some published roots use for the same key.
|
|
761
|
+
*
|
|
762
|
+
* `nodeType` is what the imported key must report, and `curve` what an EC key's parameters must be: the
|
|
763
|
+
* scheme names its curve, so a P-384 key under `ecdsa-sha2-nistp256` is a key that does not match its
|
|
764
|
+
* own declaration. `minBits` carries the section 4.2.2 floor, "All RSA keys MUST be at least 2048 bits".
|
|
765
|
+
*/
|
|
766
|
+
var KEY_DECLARATIONS = intrinsic.assign(intrinsic.create(null), {
|
|
767
|
+
"ed25519": { schemes: ["ed25519"], nodeType: "ed25519", curve: null, minBits: 0 },
|
|
768
|
+
"ecdsa": { schemes: ["ecdsa-sha2-nistp256"], nodeType: "ec", curve: "prime256v1", minBits: 0 },
|
|
769
|
+
"ecdsa-sha2-nistp256": { schemes: ["ecdsa-sha2-nistp256"], nodeType: "ec", curve: "prime256v1", minBits: 0 },
|
|
770
|
+
"rsa": { schemes: ["rsassa-pss-sha256"], nodeType: "rsa", curve: null, minBits: 2048 },
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
function _declarationOf(key) {
|
|
774
|
+
var d = KEY_DECLARATIONS[guard.text.keyOf(key.keytype)];
|
|
775
|
+
if (d === undefined) {
|
|
776
|
+
throw _err("tuf/unsupported-key", "this build reads ed25519, ecdsa and rsa keys; the metadata names " +
|
|
777
|
+
guard.text.showValue(key.keytype));
|
|
778
|
+
}
|
|
779
|
+
var scheme = guard.text.keyOf(key.scheme);
|
|
780
|
+
var ok = false;
|
|
781
|
+
for (var i = 0; i < d.schemes.length; i++) if (d.schemes[i] === scheme) ok = true;
|
|
782
|
+
if (!ok) {
|
|
783
|
+
throw _err("tuf/bad-key", "a " + key.keytype + " key pairs with the scheme " +
|
|
784
|
+
_join(d.schemes, " or ") + " (TUF sec. 4.2.2); this one declares " + guard.text.showValue(key.scheme));
|
|
785
|
+
}
|
|
786
|
+
return d;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/** @internal The resolved declaration, checked against the key material before anything verifies under
|
|
790
|
+
* it, and the imported key RETURNED so the caller verifies with the very object that was checked.
|
|
791
|
+
* Importing it again at the call site would read `keyval.public` a second time, and a caller-supplied
|
|
792
|
+
* accessor can answer differently on the second read, which would leave the checked key and the
|
|
793
|
+
* verifying key two
|
|
794
|
+
* different keys. An ed25519 key carries raw bytes rather than a PEM and its own arm holds the length
|
|
795
|
+
* and the point, so there is nothing to import for it. */
|
|
796
|
+
function _checkedPublicKey(key, d) {
|
|
797
|
+
if (d.nodeType === "ed25519") return null;
|
|
798
|
+
var imported = _publicFromPem(key.keyval.public, "a " + key.keytype + " public key");
|
|
799
|
+
if (imported.asymmetricKeyType !== d.nodeType) {
|
|
800
|
+
throw _err("tuf/bad-key", "the key declared as " + key.keytype + " holds a " +
|
|
801
|
+
guard.text.showValue(imported.asymmetricKeyType) + " public key, so the signature would be checked " +
|
|
802
|
+
"under an algorithm the metadata does not name");
|
|
803
|
+
}
|
|
804
|
+
var details = imported.asymmetricKeyDetails || {};
|
|
805
|
+
if (d.curve !== null && details.namedCurve !== d.curve) {
|
|
806
|
+
throw _err("tuf/bad-key", "the scheme " + key.scheme + " names the " + d.curve +
|
|
807
|
+
" curve; the key is on " + guard.text.showValue(details.namedCurve));
|
|
808
|
+
}
|
|
809
|
+
if (d.minBits > 0 && !(details.modulusLength >= d.minBits)) {
|
|
810
|
+
throw _err("tuf/bad-key", "an " + key.keytype + " key must be at least " + d.minBits +
|
|
811
|
+
" bits (TUF sec. 4.2.2); this one is " + guard.text.showValue(details.modulusLength));
|
|
812
|
+
}
|
|
813
|
+
return imported;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
var ED25519_SPKI_PREFIX = _bufferFrom([0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00]);
|
|
817
|
+
var ED25519_CRV = 6;
|
|
818
|
+
var edwardsPoint = require("./edwards-point");
|
|
819
|
+
|
|
820
|
+
function _publicFromPem(pem, label) {
|
|
821
|
+
if (typeof pem !== "string" || pem.length === 0) {
|
|
822
|
+
throw _err("tuf/bad-key", label + " is stated as a non-empty PEM string");
|
|
823
|
+
}
|
|
824
|
+
try { return guard.crypto.publicKey({ key: pem, format: "pem" }); }
|
|
825
|
+
catch (e) { throw _err("tuf/bad-key", label + " did not parse", e); }
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* @primitive pki.tuf.verifySignatures
|
|
830
|
+
* @signature pki.tuf.verifySignatures(opts) -> Promise<{ verified, keyIds, threshold }>
|
|
831
|
+
* @since 0.8.41
|
|
832
|
+
* @status stable
|
|
833
|
+
* @spec TUF
|
|
834
|
+
* @defends signature-bypass (CWE-347)
|
|
835
|
+
* @related pki.tuf.parseMetadata, pki.tuf.updateRoot
|
|
836
|
+
*
|
|
837
|
+
* Verify a role's threshold over parsed metadata. Resolves `verified` and the distinct `keyIds` that
|
|
838
|
+
* counted: whether a threshold was met is a verdict about the metadata, so a shortfall resolves
|
|
839
|
+
* `false` rather than throwing. A KEY the build cannot read throws, the role naming a signer it cannot
|
|
840
|
+
* identify; a SIGNATURE it cannot read counts for nothing, that being one signer short rather than a
|
|
841
|
+
* malformed document.
|
|
842
|
+
*
|
|
843
|
+
* Three rules the specification states, each applied here:
|
|
844
|
+
*
|
|
845
|
+
* - A signature counts only if its `keyid` is one the role names. One from any other key contributes
|
|
846
|
+
* nothing, whatever it verifies over.
|
|
847
|
+
* - One verified signature per DISTINCT key identifier. A key listed twice, or signing twice, counts
|
|
848
|
+
* once, so a threshold cannot be met by repetition. Where an identifier carries more than one
|
|
849
|
+
* signature the FIRST is the one asked: a second cannot raise a count already capped at one, and
|
|
850
|
+
* asking it would let a document choose how many times the signed body is hashed.
|
|
851
|
+
* - Every key's identifier is recomputed from the key itself and must equal the identifier it is
|
|
852
|
+
* listed under. A key filed under another's identifier is `tuf/bad-key`, not a key that fails to
|
|
853
|
+
* verify: the document is malformed rather than unsigned.
|
|
854
|
+
*
|
|
855
|
+
* The document verified is derived from one read of `metadata.signed`, and `metadata.signedBytes` must
|
|
856
|
+
* equal that canonical form, so the bytes the signatures are checked over are the ones the object
|
|
857
|
+
* states. A `signedBytes` holding anything else is `tuf/bad-input`, which is what a parsed object whose
|
|
858
|
+
* Buffer was overwritten after the parse reports.
|
|
859
|
+
*
|
|
860
|
+
* That body's `spec_version` must name a specification version this build implements, a key identifier
|
|
861
|
+
* being a hash of the canonical form this version defines. Another major version is
|
|
862
|
+
* `tuf/unsupported-spec-version`. No `spec_version` at all, or a `metadata.specVersion` that contradicts
|
|
863
|
+
* the body's, is `tuf/bad-input`.
|
|
864
|
+
*
|
|
865
|
+
* @opts
|
|
866
|
+
* metadata: object, // from pki.tuf.parseMetadata
|
|
867
|
+
* keys: object, // the KEYID -> key map the root states
|
|
868
|
+
* role: object, // { keyids, threshold } for the role being checked
|
|
869
|
+
*
|
|
870
|
+
* @example
|
|
871
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
872
|
+
* var raw = pki.asn1.decode(await pki.key.export(pair.publicKey)).children[1].content.subarray(1);
|
|
873
|
+
* var key = { keytype: "ed25519", scheme: "ed25519", keyval: { public: raw.toString("hex") } };
|
|
874
|
+
* var id = pki.tuf.keyId(key), keys = {}; keys[id] = key;
|
|
875
|
+
* var signed = { _type: "root", spec_version: "1.0.31", version: 1, expires: "2030-01-01T00:00:00Z",
|
|
876
|
+
* keys: keys, roles: { root: { keyids: [id], threshold: 1 },
|
|
877
|
+
* targets: { keyids: [id], threshold: 1 }, snapshot: { keyids: [id], threshold: 1 },
|
|
878
|
+
* timestamp: { keyids: [id], threshold: 1 } } };
|
|
879
|
+
* var sig = Buffer.from(await pki.webcrypto.subtle.sign({ name: "Ed25519" }, pair.privateKey,
|
|
880
|
+
* pki.tuf.canonicalJson(signed)));
|
|
881
|
+
* var rootBytes = Buffer.from(JSON.stringify({
|
|
882
|
+
* signatures: [{ keyid: id, sig: sig.toString("hex") }], signed: signed }));
|
|
883
|
+
* var v = await pki.tuf.verifySignatures({ metadata: pki.tuf.parseMetadata(rootBytes),
|
|
884
|
+
* keys: signed.keys, role: signed.roles.root });
|
|
885
|
+
* v.verified; // -> true
|
|
886
|
+
*/
|
|
887
|
+
var _VERIFY_KEYS = intrinsic.assign(_create(null), { metadata: 1, keys: 1, role: 1 });
|
|
888
|
+
async function verifySignatures(opts) {
|
|
889
|
+
opts = opts || {};
|
|
890
|
+
guard.identifier.assertKnownKeys(opts, _VERIFY_KEYS, _err, "tuf/bad-input", "unknown verifySignatures option ");
|
|
891
|
+
/** @internal Every caller-owned input is taken ONCE, at the top, and everything below reads the copy.
|
|
892
|
+
* Validating the caller's object and then reading it again is two reads, and a member reached through an
|
|
893
|
+
* accessor can answer the two differently, so the value that was checked need not be the value that is
|
|
894
|
+
* used. The verification also awaits once per signature, which is the window in which a caller can
|
|
895
|
+
* change what it still holds. So the shape checks below run against the COPIES. */
|
|
896
|
+
var metadata = opts.metadata;
|
|
897
|
+
if (!metadata || typeof metadata !== "object") {
|
|
898
|
+
throw _err("tuf/bad-input", "opts.metadata is a parsed document from pki.tuf.parseMetadata");
|
|
899
|
+
}
|
|
900
|
+
/** @internal No field of the metadata object may be an accessor. Reading one is caller code running
|
|
901
|
+
* mid-verb, and with several fields read in sequence that was a lever rather than a nuisance: a
|
|
902
|
+
* getter on `signedBytes` set a supported version on the body for the duration of the copy while a
|
|
903
|
+
* getter on `specVersion` put the unsupported one back, and the verb reported `verified` about a
|
|
904
|
+
* document the object declared neither before nor after. Ordering the reads moved the lever to
|
|
905
|
+
* whichever field is read first, and the field read first is the subject, so there is no order that
|
|
906
|
+
* closes it. Refusing accessors does, and it costs a conforming caller nothing: a document
|
|
907
|
+
* `parseMetadata` returned carries plain values. This is the door an options bag already goes
|
|
908
|
+
* through, applied to the record. */
|
|
909
|
+
/** @internal A Proxy answers from a trap rather than from a descriptor, so a field check cannot see
|
|
910
|
+
* it: one supplying a supported body on the read and carrying an unsupported one otherwise had both
|
|
911
|
+
* verbs answer positively about a document the object never held. `assertPlainRecord` is the
|
|
912
|
+
* toolkit's own refusal for a caller record, and it covers an inherited Proxy and a built-in exotic
|
|
913
|
+
* along with it. */
|
|
914
|
+
guard.identifier.assertPlainRecord(metadata, _err, "tuf/bad-input", "opts.metadata");
|
|
915
|
+
guard.identifier.refuseAccessorFields(metadata, _METADATA_FIELDS, _err, "tuf/bad-input",
|
|
916
|
+
"opts.metadata");
|
|
917
|
+
/** @internal The BODY is copied first regardless, being the authority every check below reads. */
|
|
918
|
+
var body = guard.bytes.snapshotDeep(metadata.signed, TufError, "tuf/bad-input", "metadata.signed");
|
|
919
|
+
if (!body || typeof body !== "object" || _isArray(body)) {
|
|
920
|
+
throw _err("tuf/bad-input", "opts.metadata is a parsed document from pki.tuf.parseMetadata");
|
|
921
|
+
}
|
|
922
|
+
var rawSignedBytes = metadata.signedBytes;
|
|
923
|
+
if (!guard.bytes.isByteSource(rawSignedBytes)) {
|
|
924
|
+
throw _err("tuf/bad-input", "opts.metadata is a parsed document from pki.tuf.parseMetadata");
|
|
925
|
+
}
|
|
926
|
+
/** @internal Normalized to a byte view before it is copied. The test above admits what
|
|
927
|
+
* `guard.bytes.isByteSource` admits, which includes a DataView and an ArrayBuffer, and
|
|
928
|
+
* `guard.bytes.snapshot` takes neither, so canonical bytes held as the ArrayBuffer a fetch returns
|
|
929
|
+
* were refused for their container by the same verb that had just called them acceptable.
|
|
930
|
+
* `snapshotSource` views and copies in one call; the view shares the caller's store, so the snapshot
|
|
931
|
+
* is still the only copy, which is what the read-once reasoning below depends on.
|
|
932
|
+
* Bounded at the document cap before that copy. These bytes must equal the canonical form of a body
|
|
933
|
+
* that itself came from a document no larger than the cap, so anything above it cannot match and is
|
|
934
|
+
* refused rather than copied: measured, a 32 MiB value allocated 32 MiB before the comparison
|
|
935
|
+
* rejected it. */
|
|
936
|
+
var signedView = guard.bytes.source(rawSignedBytes, _err, "tuf/bad-input", "metadata.signedBytes");
|
|
937
|
+
guard.limits.byteCap(signedView, constants.LIMITS.JSON_MAX_BYTES, _err, "tuf/too-large",
|
|
938
|
+
"metadata.signedBytes");
|
|
939
|
+
var signedBytes = guard.bytes.snapshot(signedView, _err, "tuf/bad-input", "metadata.signedBytes");
|
|
940
|
+
/** @internal ONE read of the signed body, which both the version match and the bytes are taken from.
|
|
941
|
+
* `specVersion` beside the bytes is a SECOND value, filled at parse time, so it names the document
|
|
942
|
+
* that was parsed while `signedBytes` names the document that gets verified, and a parsed object is
|
|
943
|
+
* the caller's once it is returned: its Buffer can be overwritten. Measured, a 1.x document whose
|
|
944
|
+
* bytes were replaced with a validly signed 2.x one reported `verified` while the field still read
|
|
945
|
+
* 1.0.31. Reading the version back out of the bytes is not available, canonical JSON escaping only
|
|
946
|
+
* backslash and quote so that a key held as a PEM puts raw newlines inside a string and the signed
|
|
947
|
+
* form of any real root is not a document a strict JSON reader accepts. So the body is the source:
|
|
948
|
+
* the canonical form derived from it must equal the bytes the caller carries, and the version is read
|
|
949
|
+
* from the same copy. A stated `specVersion` that contradicts it is a caller describing a document it
|
|
950
|
+
* does not hold. This is the `version` swap A16 pins, answered for `spec_version` the same way.
|
|
951
|
+
*
|
|
952
|
+
* Read after both copies are taken, never before: an accessor on this field runs while the bytes and
|
|
953
|
+
* the body would still be the caller's, and the field is only a cross-check. */
|
|
954
|
+
_assertMetadataSpecVersion(body.spec_version);
|
|
955
|
+
if (!guard.crypto.constantTimeEqual(canonicalJson(body), signedBytes)) {
|
|
956
|
+
throw _err("tuf/bad-input", "metadata.signedBytes is not the canonical form of metadata.signed, so " +
|
|
957
|
+
"the document the signatures would be checked over is not the one the object states");
|
|
958
|
+
}
|
|
959
|
+
_assertCopiesAgree(metadata, body, "opts.metadata");
|
|
960
|
+
var keys = opts.keys;
|
|
961
|
+
if (!keys || typeof keys !== "object" || _isArray(keys)) {
|
|
962
|
+
throw _err("tuf/bad-input", "opts.keys is the KEYID to key map the root states");
|
|
963
|
+
}
|
|
964
|
+
/** @internal ONE read of the role, which carries the authorization: which key ids may sign, and how
|
|
965
|
+
* many must. It was read four times, for the presence check, the typeof, and then each field, and an
|
|
966
|
+
* accessor answers every read separately: a getter could present a strict role to the checks and a lax
|
|
967
|
+
* one to the field reads, combining one role's authorized keys with another's threshold. The verdict
|
|
968
|
+
* then reported a document as meeting a threshold no role had stated. */
|
|
969
|
+
var suppliedRole = opts.role;
|
|
970
|
+
if (!suppliedRole || typeof suppliedRole !== "object") {
|
|
971
|
+
throw _err("tuf/bad-input", "opts.role carries a keyids array and a threshold that is a positive integer");
|
|
972
|
+
}
|
|
973
|
+
var role = { keyids: suppliedRole.keyids, threshold: suppliedRole.threshold };
|
|
974
|
+
if (!_isArray(role.keyids) || typeof role.threshold !== "number" ||
|
|
975
|
+
!_isSafeInteger(role.threshold) || role.threshold < 1) {
|
|
976
|
+
throw _err("tuf/bad-input", "opts.role carries a keyids array and a threshold that is a positive integer");
|
|
977
|
+
}
|
|
978
|
+
role.keyids = guard.bytes.snapshotDeep(role.keyids, TufError, "tuf/bad-input", "opts.role.keyids");
|
|
979
|
+
/** @internal The keys MAP is a caller record whose members are read inside a loop that awaits, so it
|
|
980
|
+
* gets the door the other caller records here already have rather than a defensive second read. Held
|
|
981
|
+
* to being a plain object, which refuses a Proxy, and to carrying plain values at every identifier
|
|
982
|
+
* the role names, which refuses an accessor: either would otherwise answer each read separately, and
|
|
983
|
+
* the identifier filed under a key is checked against the copy the first read produced. An
|
|
984
|
+
* identifier the role repeats is resolved once now, so a getter answering correctly first and
|
|
985
|
+
* incorrectly afterwards had its later answer go unread, which is a check on a value nothing used.
|
|
986
|
+
* Closing the door removes the question instead of restoring the read. */
|
|
987
|
+
guard.identifier.assertPlainRecord(keys, _err, "tuf/bad-input", "opts.keys");
|
|
988
|
+
guard.identifier.refuseAccessorFields(keys, role.keyids, _err, "tuf/bad-input", "opts.keys");
|
|
989
|
+
/** @internal The role's identifiers are resolved to keys FIRST, and each key's identifier is
|
|
990
|
+
* recomputed, so the whole key set a role names is held to being correctly filed before any
|
|
991
|
+
* signature is checked. A document that mislabels a key is refused whether or not its signatures
|
|
992
|
+
* would have verified. */
|
|
993
|
+
var authorized = _create(null);
|
|
994
|
+
for (var i = 0; i < role.keyids.length; i++) {
|
|
995
|
+
var id = role.keyids[i];
|
|
996
|
+
if (typeof id !== "string" || id.length !== KEY_ID_HEX_LEN) {
|
|
997
|
+
throw _err("tuf/bad-metadata", "role.keyids[" + i + "] is not a 64-character key identifier");
|
|
998
|
+
}
|
|
999
|
+
if (!_hasOwn(keys, id)) {
|
|
1000
|
+
throw _err("tuf/bad-metadata", "the role names key " + id + ", which the key map does not carry");
|
|
1001
|
+
}
|
|
1002
|
+
/** @internal An identifier the role repeats resolves to the same key, and the resolution below
|
|
1003
|
+
* deep-copies that key and hashes its canonical form. Done per OCCURRENCE, a role could ask for
|
|
1004
|
+
* that work as many times as the size cap admits identifiers: measured, 7500 repetitions of one
|
|
1005
|
+
* identifier beside a 500 KB key took 14.9 seconds inside a one-megabyte document, synchronously,
|
|
1006
|
+
* before the first await and before anything had authenticated a byte of it. The answer is already
|
|
1007
|
+
* filed, so the repeat is a lookup. This is the same per-occurrence shape the signature walk below
|
|
1008
|
+
* and the role walk in `updateRoot` both carry, and the third one found. */
|
|
1009
|
+
if (_hasOwn(authorized, id)) continue;
|
|
1010
|
+
/** @internal The key is COPIED before its identifier is computed, and the COPY is what verifies. The
|
|
1011
|
+
* signature loop below awaits a verification per signature while this map is held, so a caller that
|
|
1012
|
+
* replaces a later key's `keyval.public` in that window would have the identifier checked against one
|
|
1013
|
+
* key and the signature verified under another: a threshold could then be met by material nobody
|
|
1014
|
+
* authorized, the identifier naming one key while the bytes are another's. Deriving the identifier
|
|
1015
|
+
* from the copy makes the two one read of one value. */
|
|
1016
|
+
var key = guard.bytes.snapshotDeep(keys[id], TufError, "tuf/bad-key", "the key filed under " + id);
|
|
1017
|
+
var computed = keyId(key);
|
|
1018
|
+
if (computed !== id) {
|
|
1019
|
+
throw _err("tuf/bad-key", "the key filed under " + id + " has identifier " + computed +
|
|
1020
|
+
"; a key must be listed under its own");
|
|
1021
|
+
}
|
|
1022
|
+
authorized[id] = key;
|
|
1023
|
+
}
|
|
1024
|
+
/** @internal The signature records, one read each. The loop below awaits a verification per signature and
|
|
1025
|
+
* re-read them on every pass, so two signatures over DIFFERENT documents could each verify against the
|
|
1026
|
+
* bytes present on their own turn and together meet a threshold no single document ever met. The signed
|
|
1027
|
+
* bytes were copied at the top for the same reason. */
|
|
1028
|
+
/** @internal Filed by key identifier, because an identifier names at most one signature. The
|
|
1029
|
+
* reference implementation the format points at builds the same map and refuses a document
|
|
1030
|
+
* carrying two entries for one identifier ("Multiple signatures found for keyid"), then verifies by
|
|
1031
|
+
* walking the ROLE's keys and looking up the one signature each names. Reading the document's list
|
|
1032
|
+
* instead let it decide how many verifications ran: distinct signatures for one authorized key
|
|
1033
|
+
* share no memo, and each costs a hash of the whole signed body, so a 500 KB body under 2400
|
|
1034
|
+
* signatures made with a key nobody authorized bought 2400 checks and about a second, synchronously,
|
|
1035
|
+
* inside the one-megabyte size cap and before anything had authenticated a byte. Walking the keys
|
|
1036
|
+
* the ROLE names leaves the verifications at the number of keys that role lists, so the signature
|
|
1037
|
+
* count is no longer a multiplier on the body: the work follows the document's own size rather than
|
|
1038
|
+
* the product of its body and its signature list. The role is the caller's argument, and in a root
|
|
1039
|
+
* rotation a candidate is also verified against the root role it states itself, which is the check
|
|
1040
|
+
* the specification requires of a new root, so that one call's count is the candidate's to state. */
|
|
1041
|
+
var sigOf = _create(null);
|
|
1042
|
+
var rawSigs = metadata.signatures;
|
|
1043
|
+
if (!_isArray(rawSigs)) throw _err("tuf/bad-input", "opts.metadata carries a signatures array");
|
|
1044
|
+
for (var r = 0; r < rawSigs.length; r++) {
|
|
1045
|
+
var rec = rawSigs[r];
|
|
1046
|
+
if (!rec || typeof rec !== "object") throw _err("tuf/bad-metadata", "signatures[" + r + "] is not an object");
|
|
1047
|
+
/** @internal Each record is a plain object with plain fields, held to it before either field is
|
|
1048
|
+
* read. These are copied into the list above so no field is read twice, and reading one at all is
|
|
1049
|
+
* caller code running inside the loop: a getter on `sig` fired while the body was still the
|
|
1050
|
+
* caller's and could change the document mid-verification. The copies make the verdict about one
|
|
1051
|
+
* document, and this makes the reads themselves inert. */
|
|
1052
|
+
guard.identifier.assertPlainRecord(rec, _err, "tuf/bad-metadata", "signatures[" + r + "]");
|
|
1053
|
+
guard.identifier.refuseAccessorFields(rec, _SIGNATURE_FIELDS, _err, "tuf/bad-metadata",
|
|
1054
|
+
"signatures[" + r + "]");
|
|
1055
|
+
var recKeyid = rec.keyid, recSig = rec.sig;
|
|
1056
|
+
/** @internal A keyid that is not a string names no key in the map, so such a record can never be
|
|
1057
|
+
* counted and never costs a verification; it is filed under nothing rather than refused. */
|
|
1058
|
+
if (typeof recKeyid !== "string") continue;
|
|
1059
|
+
/** @internal The FIRST entry an identifier carries is the one asked, and a later entry for that
|
|
1060
|
+
* same identifier is passed over. The two authorities differ on a repeated identifier and this
|
|
1061
|
+
* satisfies both. The specification admits one: "each SIGNATURE which is counted towards the
|
|
1062
|
+
* THRESHOLD MUST have a unique KEYID. Even if a KEYID is listed more than once ... a client MUST
|
|
1063
|
+
* NOT count more than one verified SIGNATURE from that KEYID", which caps the COUNT and leaves
|
|
1064
|
+
* the document readable, so refusing it would be stricter than the document this parser cites.
|
|
1065
|
+
* The reference implementation refuses it outright ("Multiple signatures found for keyid"). A
|
|
1066
|
+
* second entry cannot raise the count the specification caps at one, so the only thing asking it
|
|
1067
|
+
* could do is succeed where the first failed, which takes a document that lists a wrong signature
|
|
1068
|
+
* for a key ahead of a right one. Passing it over keeps the verdict the specification requires and
|
|
1069
|
+
* leaves the number of verifications at the number of keys the ROLE names. */
|
|
1070
|
+
if (_hasOwn(sigOf, recKeyid)) continue;
|
|
1071
|
+
sigOf[recKeyid] = recSig;
|
|
1072
|
+
}
|
|
1073
|
+
/** @internal Counted by key MATERIAL, not by identifier. A threshold is a number of KEYS, and an
|
|
1074
|
+
* identifier is a hash of the whole key object, so one public key can hold two identifiers and be
|
|
1075
|
+
* counted twice: a role naming both with a threshold of two was met by one key, the same signature
|
|
1076
|
+
* bytes verifying under each entry. `keyIds` still reports the identifiers that counted, which is
|
|
1077
|
+
* what names the signers; what the threshold is compared against is how many distinct keys they
|
|
1078
|
+
* are. */
|
|
1079
|
+
var countedMaterial = _create(null);
|
|
1080
|
+
var keyIds = [];
|
|
1081
|
+
/** @internal The keys the ROLE names, each visited once, which is what bounds the work: an identifier
|
|
1082
|
+
* the role repeats was filed once above, and a key the role names and the document signed for is the
|
|
1083
|
+
* only thing that reaches a verification. The signature list decides WHICH signature each key
|
|
1084
|
+
* offers, never how many are asked. */
|
|
1085
|
+
var ids = _keys(authorized);
|
|
1086
|
+
for (var s = 0; s < ids.length; s++) {
|
|
1087
|
+
var signerId = ids[s];
|
|
1088
|
+
if (!_hasOwn(sigOf, signerId)) continue;
|
|
1089
|
+
var use = _usableKey(authorized[signerId]);
|
|
1090
|
+
var materialId = _materialIdOf(use.key);
|
|
1091
|
+
/** @internal Counted material is skipped, since a second identifier over the same key cannot raise
|
|
1092
|
+
* the count. Filed on SUCCESS alone: two identifiers can name one key, and if the first one's
|
|
1093
|
+
* signature does not verify the second's still has to be asked, the key having signed once under
|
|
1094
|
+
* whichever identifier the signer used. */
|
|
1095
|
+
if (_hasOwn(countedMaterial, materialId)) continue;
|
|
1096
|
+
/** @internal A signature this build cannot read is a verdict about the metadata rather than a
|
|
1097
|
+
* fault: the record counts for nothing and the walk goes on, which is what it did when the decode
|
|
1098
|
+
* sat inside the verify and threw. */
|
|
1099
|
+
var sigBytes = null;
|
|
1100
|
+
try {
|
|
1101
|
+
sigBytes = guard.encoding.hex(sigOf[signerId], constants.LIMITS.JSON_MAX_BYTES, _err,
|
|
1102
|
+
"tuf/bad-signature", "a signature");
|
|
1103
|
+
} catch (e) {
|
|
1104
|
+
if (!(e instanceof TufError && e.code === "tuf/bad-signature")) throw e;
|
|
1105
|
+
}
|
|
1106
|
+
if (sigBytes === null) continue;
|
|
1107
|
+
var ok = await use.check(sigBytes, signedBytes);
|
|
1108
|
+
if (ok === true) {
|
|
1109
|
+
countedMaterial[materialId] = 1;
|
|
1110
|
+
_push(keyIds, signerId);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
return guard.verdict.of({ verified: keyIds.length >= role.threshold, keyIds: keyIds,
|
|
1114
|
+
threshold: role.threshold });
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/** @internal The identity of the public KEY, independent of how the metadata spells it. A threshold
|
|
1118
|
+
* counts keys, and a key identifier is a hash of the whole key object, so one public key can hold
|
|
1119
|
+
* many identifiers: a member the hash covers and the cryptography ignores (a `comment`), a PEM with
|
|
1120
|
+
* CRLF line endings or a space before each newline, an EC point written compressed rather than
|
|
1121
|
+
* uncompressed, or `ecdsa-sha2-nistp256` where another entry says `ecdsa`. Each yields a different
|
|
1122
|
+
* identifier for the same key, and a role naming two of them with a threshold of two was met by ONE
|
|
1123
|
+
* key, the same signature bytes verifying under each entry.
|
|
1124
|
+
*
|
|
1125
|
+
* So the fingerprint is taken from the IMPORTED key's JWK, which is the mathematical form: the
|
|
1126
|
+
* coordinates, fixed-width. MEASURED, that is what collapses the four spellings above, where the
|
|
1127
|
+
* exported SPKI does not: Node preserves a compressed point on export, 59 bytes against 91, while the
|
|
1128
|
+
* JWK coordinates of the two are identical. The scheme is deliberately not part of it, one key listed
|
|
1129
|
+
* under two schemes still being one key. */
|
|
1130
|
+
function _materialIdOf(imported) {
|
|
1131
|
+
/** @internal The export goes through the captured operation. Reached off the key's own prototype, a
|
|
1132
|
+
* replacement returning one fixed value gave two distinct authorized keys the same identifier, and a
|
|
1133
|
+
* two-key threshold then counted one signature. */
|
|
1134
|
+
return _bufToString(guard.crypto.digest("sha256",
|
|
1135
|
+
canonicalJson(guard.crypto.exportKey(imported, { format: "jwk" }))), "hex");
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/** @internal A root document, parsed and held to being a root that states who may sign it. */
|
|
1139
|
+
function _asRoot(input, label) {
|
|
1140
|
+
var meta = parseMetadata(input);
|
|
1141
|
+
if (meta.type !== "root") {
|
|
1142
|
+
throw _err("tuf/bad-metadata", label + " has _type " + guard.text.showValue(meta.type) + ", not root");
|
|
1143
|
+
}
|
|
1144
|
+
var signed = meta.signed;
|
|
1145
|
+
if (!signed.keys || typeof signed.keys !== "object" || _isArray(signed.keys)) {
|
|
1146
|
+
throw _err("tuf/bad-metadata", label + " carries no keys map");
|
|
1147
|
+
}
|
|
1148
|
+
if (!signed.roles || typeof signed.roles !== "object" || _isArray(signed.roles)) {
|
|
1149
|
+
throw _err("tuf/bad-metadata", label + " carries no roles map");
|
|
1150
|
+
}
|
|
1151
|
+
/** @internal Every top-level role, not only the one that signs the root. The specification sec. 4.3
|
|
1152
|
+
* states it exactly: "A role for each of "root", "snapshot", "targets", and "timestamp" MUST be
|
|
1153
|
+
* specified in the roles object", with "The role of "mirror" is OPTIONAL". A root carrying only its
|
|
1154
|
+
* own role was adopted, and what that leaves is a trust anchor that cannot authenticate anything
|
|
1155
|
+
* else the repository publishes: no keyids and no threshold to check a targets, snapshot or
|
|
1156
|
+
* timestamp document against. Each record is held to carrying both, since a role that names neither
|
|
1157
|
+
* is present without being usable. */
|
|
1158
|
+
/** @internal One verdict per KEYID for the whole walk, not per occurrence. Each occurrence
|
|
1159
|
+
* canonicalized and hashed the whole key twice, imported it, and derived its material identity, and
|
|
1160
|
+
* all of that runs on UNAUTHENTICATED metadata before any candidate signature is checked: measured,
|
|
1161
|
+
* a 184 KB root carrying a 50 KB key comment and repeating one keyid 500 times per role spent 3.2
|
|
1162
|
+
* seconds inside this walk before answering `tuf/root-unsigned`, with the document well under the
|
|
1163
|
+
* size cap. The work is a pure function of the key filed under the identifier, and the body is
|
|
1164
|
+
* already a snapshot nothing can change under the walk, so the second occurrence of an identifier
|
|
1165
|
+
* reuses the first one's answer. A FAILING identifier throws, so only verdicts that passed are
|
|
1166
|
+
* held. */
|
|
1167
|
+
var keyVerdict = _create(null);
|
|
1168
|
+
for (var r = 0; r < TOP_LEVEL_ROLES.length; r++) {
|
|
1169
|
+
var roleName = TOP_LEVEL_ROLES[r];
|
|
1170
|
+
var record = signed.roles[roleName];
|
|
1171
|
+
if (!record || typeof record !== "object" || _isArray(record)) {
|
|
1172
|
+
throw _err("tuf/bad-metadata", label + " names no " + roleName + " role, and a root states a " +
|
|
1173
|
+
"role for each of " + TOP_LEVEL_ROLES_TEXT + " (TUF specification sec. 4.3)");
|
|
1174
|
+
}
|
|
1175
|
+
/** @internal A record can be present and still name no signer this root carries, which is the same
|
|
1176
|
+
* gap wearing a well-formed shape. Two of these checks are the specification's own words and two
|
|
1177
|
+
* are this build refusing an unusable record with the specification silent, which the messages
|
|
1178
|
+
* distinguish. A KEYID is "a hexdigest of the SHA-256 hash of the canonical form of the key", so
|
|
1179
|
+
* one that is not a string is not a KEYID; a threshold is "A positive integer number of keys
|
|
1180
|
+
* (>=1)". Nothing states that the keyids must appear in this root's own `keys` map or that the
|
|
1181
|
+
* threshold must be reachable, and a role failing either can never be satisfied: it names keys
|
|
1182
|
+
* the root does not carry, or asks for more signatures than it names keys. */
|
|
1183
|
+
if (!_isArray(record.keyids) || record.keyids.length === 0) {
|
|
1184
|
+
throw _err("tuf/bad-metadata", label + "'s " + roleName + " role names no keyids, so it does " +
|
|
1185
|
+
"not state who may sign for it");
|
|
1186
|
+
}
|
|
1187
|
+
/** @internal The threshold is compared against the DISTINCT key ids, not the list length. A
|
|
1188
|
+
* verdict counts each key id once, so a role naming one key twice for a threshold of two reads as
|
|
1189
|
+
* reachable and is not: one signature is all that list can ever produce. */
|
|
1190
|
+
var distinct = _create(null);
|
|
1191
|
+
for (var ki = 0; ki < record.keyids.length; ki++) {
|
|
1192
|
+
var keyid = record.keyids[ki];
|
|
1193
|
+
if (typeof keyid !== "string" || keyid.length === 0) {
|
|
1194
|
+
throw _err("tuf/bad-metadata", label + "'s " + roleName + " role names a keyid that is not a " +
|
|
1195
|
+
"string, where a KEYID is the hexdigest of a hash (TUF specification sec. 4.3)");
|
|
1196
|
+
}
|
|
1197
|
+
if (_hasOwn(keyVerdict, keyid)) { distinct[keyVerdict[keyid]] = 1; continue; }
|
|
1198
|
+
if (!_hasOwn(signed.keys, keyid)) {
|
|
1199
|
+
throw _err("tuf/bad-metadata", label + "'s " + roleName + " role names keyid " +
|
|
1200
|
+
guard.text.showValue(keyid) + ", which this root's keys map does not carry, so the role " +
|
|
1201
|
+
"states a signer the root cannot identify");
|
|
1202
|
+
}
|
|
1203
|
+
/** @internal EXISTENCE IS NOT IDENTITY. A role can name an identifier the keys map carries while
|
|
1204
|
+
* the key filed there hashes to a different one, which is the "key listed under an identifier
|
|
1205
|
+
* that is not its own" `verifySignatures` refuses. Only the ROOT role is verified during a
|
|
1206
|
+
* rotation, so a mis-filed key in any other role was adopted and failed later, where the client
|
|
1207
|
+
* had already trusted the root. Recomputed here, at adoption, for every role. The code is the
|
|
1208
|
+
* one that condition already has, so a caller branching on it sees one answer wherever the
|
|
1209
|
+
* mis-filing is found. */
|
|
1210
|
+
if (keyId(signed.keys[keyid]) !== keyid) {
|
|
1211
|
+
throw _err("tuf/bad-key", label + "'s " + roleName + " role names keyid " +
|
|
1212
|
+
guard.text.showValue(keyid) + ", and the key filed under it derives a different identifier, " +
|
|
1213
|
+
"so the role names a key the root does not carry");
|
|
1214
|
+
}
|
|
1215
|
+
/** @internal Keyed on the MATERIAL, so two identifiers for one public key count once. A role
|
|
1216
|
+
* naming a key under two identifiers for a threshold of two states two signers and has one, and
|
|
1217
|
+
* the verdict counts the same way. The identity comes from the key `_usableKey` imported, so
|
|
1218
|
+
* holding the key to being usable and deriving what it is cost one import between them rather
|
|
1219
|
+
* than one each. */
|
|
1220
|
+
var mid = _materialIdOf(_usableKey(signed.keys[keyid]).key);
|
|
1221
|
+
keyVerdict[keyid] = mid;
|
|
1222
|
+
distinct[mid] = 1;
|
|
1223
|
+
}
|
|
1224
|
+
var distinctCount = intrinsic.keys(distinct).length;
|
|
1225
|
+
if (typeof record.threshold !== "number" || !_isSafeInteger(record.threshold) ||
|
|
1226
|
+
record.threshold < 1) {
|
|
1227
|
+
throw _err("tuf/bad-metadata", label + "'s " + roleName + " role states no threshold that is a " +
|
|
1228
|
+
"positive integer, so it does not state how many signatures it takes");
|
|
1229
|
+
}
|
|
1230
|
+
if (record.threshold > distinctCount) {
|
|
1231
|
+
throw _err("tuf/bad-metadata", label + "'s " + roleName + " role asks for " + record.threshold +
|
|
1232
|
+
" signatures and names " + distinctCount + " distinct key(s), so the role can never be met");
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
return meta;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
/**
|
|
1239
|
+
* @primitive pki.tuf.updateRoot
|
|
1240
|
+
* @signature pki.tuf.updateRoot(opts) -> Promise<{ root, version, updated, walked }>
|
|
1241
|
+
* @since 0.8.41
|
|
1242
|
+
* @status stable
|
|
1243
|
+
* @spec TUF
|
|
1244
|
+
* @defends signature-bypass (CWE-347)
|
|
1245
|
+
* @related pki.tuf.verifySignatures, pki.tuf.checkExpiry
|
|
1246
|
+
*
|
|
1247
|
+
* Walk the root chain from a trusted root to the latest candidate, applying the two rules that make a
|
|
1248
|
+
* key rotation safe. Each new root must be signed by a threshold of the keys the PREVIOUS root names
|
|
1249
|
+
* and a threshold of the keys it names ITSELF: the first says the rotation was authorized by whoever
|
|
1250
|
+
* held the old keys, the second that the new keys can actually sign. A root meeting only one is
|
|
1251
|
+
* `tuf/root-unsigned`.
|
|
1252
|
+
*
|
|
1253
|
+
* Its version must be exactly one more than the trusted one. A candidate that skips a version is
|
|
1254
|
+
* `tuf/bad-root-version`, so no intermediate root can be passed over, and one at or below the trusted
|
|
1255
|
+
* version is refused by the same rule, which is the rollback check.
|
|
1256
|
+
*
|
|
1257
|
+
* The trusted root is itself verified against its own role before any candidate is read, because a chain
|
|
1258
|
+
* anchored in something unchecked proves nothing.
|
|
1259
|
+
*
|
|
1260
|
+
* Expiry is judged once, at the end, on the root the chain finishes on, at the `now` the caller supplies.
|
|
1261
|
+
* That is where the specification puts the freeze-attack check, and it is what lets a client whose pinned
|
|
1262
|
+
* root has lapsed catch up through the successors it is handed instead of needing its trust anchor
|
|
1263
|
+
* replaced by some other means. An intermediate root's own expiry is not checked, and with no candidates
|
|
1264
|
+
* the root the chain finishes on is the trusted root, so a lapsed root with nothing to move to is
|
|
1265
|
+
* `tuf/expired`.
|
|
1266
|
+
*
|
|
1267
|
+
* `candidates` are the intermediate roots in any order; `walked` reports the versions adopted, and
|
|
1268
|
+
* `updated` whether any candidate was.
|
|
1269
|
+
*
|
|
1270
|
+
* @opts
|
|
1271
|
+
* trustedRoot: BufferSource, // the root the caller already trusts
|
|
1272
|
+
* candidates: Array, // the later root documents, each a BufferSource
|
|
1273
|
+
* now: Date, // the instant expiry is judged at
|
|
1274
|
+
*
|
|
1275
|
+
* @example
|
|
1276
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
1277
|
+
* var raw = pki.asn1.decode(await pki.key.export(pair.publicKey)).children[1].content.subarray(1);
|
|
1278
|
+
* var key = { keytype: "ed25519", scheme: "ed25519", keyval: { public: raw.toString("hex") } };
|
|
1279
|
+
* var id = pki.tuf.keyId(key), keys = {}; keys[id] = key;
|
|
1280
|
+
* // `spec_version` is mandatory on a root this verb adopts, and its major version has to be one this
|
|
1281
|
+
* // build implements: another major version states rules these checks do not have, and naming none
|
|
1282
|
+
* // states nothing to match against.
|
|
1283
|
+
* var signed = { _type: "root", spec_version: "1.0.31", version: 1, expires: "2030-01-01T00:00:00Z",
|
|
1284
|
+
* keys: keys, roles: { root: { keyids: [id], threshold: 1 },
|
|
1285
|
+
* targets: { keyids: [id], threshold: 1 }, snapshot: { keyids: [id], threshold: 1 },
|
|
1286
|
+
* timestamp: { keyids: [id], threshold: 1 } } };
|
|
1287
|
+
* var sig = Buffer.from(await pki.webcrypto.subtle.sign({ name: "Ed25519" }, pair.privateKey,
|
|
1288
|
+
* pki.tuf.canonicalJson(signed)));
|
|
1289
|
+
* var rootBytes = Buffer.from(JSON.stringify({
|
|
1290
|
+
* signatures: [{ keyid: id, sig: sig.toString("hex") }], signed: signed }));
|
|
1291
|
+
* var r = await pki.tuf.updateRoot({ trustedRoot: rootBytes, candidates: [],
|
|
1292
|
+
* now: new Date("2027-01-01T00:00:00Z") });
|
|
1293
|
+
* r.updated; // -> false
|
|
1294
|
+
*/
|
|
1295
|
+
var _UPDATE_KEYS = intrinsic.assign(_create(null), { trustedRoot: 1, candidates: 1, now: 1 });
|
|
1296
|
+
async function updateRoot(opts) {
|
|
1297
|
+
opts = opts || {};
|
|
1298
|
+
guard.identifier.assertKnownKeys(opts, _UPDATE_KEYS, _err, "tuf/bad-input", "unknown updateRoot option ");
|
|
1299
|
+
/** @internal The instant is CAPTURED here, at the call, into a Date this function owns. A Date is
|
|
1300
|
+
* mutable and the caller keeps theirs, and the freeze-attack check runs at the END, after a signature
|
|
1301
|
+
* verification has been awaited for every root in the chain: read from the caller's object at that
|
|
1302
|
+
* point, a root that had expired by the instant supplied would pass if that Date were moved back while
|
|
1303
|
+
* the chain was walked. Expiry is judged at the instant validation began at. */
|
|
1304
|
+
var suppliedNow = opts.now;
|
|
1305
|
+
if (!guard.time.isDate(suppliedNow)) throw _err("tuf/bad-input", "opts.now must be a Date");
|
|
1306
|
+
var now = guard.time.toDate(guard.time.instantOf(suppliedNow));
|
|
1307
|
+
/** @internal One read: `opts.candidates === undefined ? [] : opts.candidates` reads it twice, and a
|
|
1308
|
+
* member reached through an accessor can answer the presence test and the value differently, so the
|
|
1309
|
+
* array whose length is checked need not be the array that is walked. */
|
|
1310
|
+
var suppliedCandidates = opts.candidates;
|
|
1311
|
+
var candidates = suppliedCandidates === undefined ? [] : suppliedCandidates;
|
|
1312
|
+
if (!_isArray(candidates)) throw _err("tuf/bad-input", "opts.candidates must be an array of root documents");
|
|
1313
|
+
var candidateCount = candidates.length;
|
|
1314
|
+
if (candidateCount > constants.LIMITS.TUF_MAX_ROOT_CHAIN) {
|
|
1315
|
+
throw _err("tuf/bad-input", "a root chain of " + candidateCount + " exceeds the " +
|
|
1316
|
+
constants.LIMITS.TUF_MAX_ROOT_CHAIN + "-document cap");
|
|
1317
|
+
}
|
|
1318
|
+
var trusted = _asRoot(opts.trustedRoot, "the trusted root");
|
|
1319
|
+
/** @internal Every candidate is read and parsed HERE, before the first await, and the walk below reads
|
|
1320
|
+
* only what this produced. Indexing after the await read the caller's array as it stood then, so a call
|
|
1321
|
+
* made with an empty list could be handed a whole chain while the trusted root was being checked, and
|
|
1322
|
+
* the cap above would have been applied to the empty one. The length is bound for the same reason. */
|
|
1323
|
+
/** @internal The trusted root is verified against its OWN role before it is used to judge
|
|
1324
|
+
* anything. A caller may have been handed it by any means, and a chain anchored in a document
|
|
1325
|
+
* nobody checked carries the whole chain's weight on nothing. */
|
|
1326
|
+
/** @internal Candidates are indexed by version so the walk asks for exactly N+1 at each step. A
|
|
1327
|
+
* list in any order therefore walks in version order, and a gap stops the walk rather than
|
|
1328
|
+
* letting a later root be adopted without the one before it. */
|
|
1329
|
+
var byVersion = _create(null);
|
|
1330
|
+
for (var i = 0; i < candidateCount; i++) {
|
|
1331
|
+
var cand = _asRoot(candidates[i], "candidates[" + i + "]");
|
|
1332
|
+
if (_hasOwn(byVersion, cand.version)) {
|
|
1333
|
+
throw _err("tuf/bad-root-version", "two candidate roots state version " + cand.version);
|
|
1334
|
+
}
|
|
1335
|
+
byVersion[cand.version] = cand;
|
|
1336
|
+
}
|
|
1337
|
+
var self = await verifySignatures({ metadata: trusted, keys: trusted.signed.keys, role: trusted.signed.roles.root });
|
|
1338
|
+
if (self.verified !== true) {
|
|
1339
|
+
throw _err("tuf/root-unsigned", "the trusted root does not meet its own root-role threshold of " +
|
|
1340
|
+
self.threshold + "; " + self.keyIds.length + " of its own keys signed it");
|
|
1341
|
+
}
|
|
1342
|
+
var walked = [];
|
|
1343
|
+
var current = trusted;
|
|
1344
|
+
for (;;) {
|
|
1345
|
+
var next = byVersion[current.version + 1];
|
|
1346
|
+
if (next === undefined) break;
|
|
1347
|
+
var byOld = await verifySignatures({ metadata: next, keys: current.signed.keys, role: current.signed.roles.root });
|
|
1348
|
+
if (byOld.verified !== true) {
|
|
1349
|
+
throw _err("tuf/root-unsigned", "root version " + next.version + " is not signed by a threshold of " +
|
|
1350
|
+
"the keys the trusted root version " + current.version + " names");
|
|
1351
|
+
}
|
|
1352
|
+
var byNew = await verifySignatures({ metadata: next, keys: next.signed.keys, role: next.signed.roles.root });
|
|
1353
|
+
if (byNew.verified !== true) {
|
|
1354
|
+
throw _err("tuf/root-unsigned", "root version " + next.version + " is not signed by a threshold of " +
|
|
1355
|
+
"the keys it names itself, so its own keys could not sign for it");
|
|
1356
|
+
}
|
|
1357
|
+
_push(walked, next.version);
|
|
1358
|
+
current = next;
|
|
1359
|
+
}
|
|
1360
|
+
/** @internal A candidate the walk never reached is a gap or a rollback, and is reported rather
|
|
1361
|
+
* than ignored: a client that silently stops at a gap would keep trusting an older root while a
|
|
1362
|
+
* newer one it was handed says otherwise. */
|
|
1363
|
+
var versions = _keys(byVersion);
|
|
1364
|
+
for (var v = 0; v < versions.length; v++) {
|
|
1365
|
+
var num = +versions[v];
|
|
1366
|
+
if (num > current.version) {
|
|
1367
|
+
throw _err("tuf/bad-root-version", "candidate root version " + num + " was not reached: the chain " +
|
|
1368
|
+
"stops at version " + current.version + ", so a root between them is missing");
|
|
1369
|
+
}
|
|
1370
|
+
if (num <= trusted.version) {
|
|
1371
|
+
throw _err("tuf/bad-root-version", "candidate root version " + num + " is not newer than the " +
|
|
1372
|
+
"trusted version " + trusted.version + "; a root may only move forward");
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
/** @internal The freeze-attack check, LAST, and on the root the walk ended on. The specification puts
|
|
1376
|
+
* it at step 5.3.10, after the walk of steps 5.3.2 to 5.3.9, and no step checks an intermediate
|
|
1377
|
+
* root's expiry: a client that has been offline long enough for its pinned root to lapse is meant to
|
|
1378
|
+
* catch up through the successors it is handed, and checking the anchor first is what stops it,
|
|
1379
|
+
* leaving the only way forward a replacement of the trust anchor by some other means. With no
|
|
1380
|
+
* candidates the root the walk ends on is the trusted root itself, so a lapsed root with nothing to
|
|
1381
|
+
* move to is still refused here. */
|
|
1382
|
+
checkExpiry(current, now);
|
|
1383
|
+
return guard.verdict.of({ root: current.signed, version: current.version,
|
|
1384
|
+
updated: walked.length > 0, walked: walked });
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
module.exports = {
|
|
1388
|
+
canonicalJson: canonicalJson,
|
|
1389
|
+
keyId: keyId,
|
|
1390
|
+
parseMetadata: parseMetadata,
|
|
1391
|
+
checkExpiry: checkExpiry,
|
|
1392
|
+
verifySignatures: verifySignatures,
|
|
1393
|
+
updateRoot: updateRoot,
|
|
1394
|
+
ROLES: TUF_ROLES,
|
|
1395
|
+
};
|