@blamejs/pki 0.6.54 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/README.md +3 -3
- package/lib/asn1-der.js +27 -3
- package/lib/constants.js +4 -0
- package/lib/crmf-sign.js +821 -18
- package/lib/guard-identifier.js +10 -0
- package/lib/oid.js +8 -0
- package/lib/schema-pkix.js +15 -0
- package/lib/sigstore.js +731 -80
- package/lib/tls-cert-compress.js +7 -6
- package/lib/vendor/MANIFEST.json +1 -1
- package/lib/vendor/README.md +1 -1
- package/lib/webauthn-mds.js +16 -8
- package/package.json +4 -3
- package/sbom.cdx.json +7 -7
package/lib/sigstore.js
CHANGED
|
@@ -8,19 +8,22 @@
|
|
|
8
8
|
* @fullname Sigstore bundles: keyless signing and provenance
|
|
9
9
|
* @intro Zero-dependency verifier for a Sigstore bundle, the exact artifact
|
|
10
10
|
* `npm publish --provenance` produces and the npm registry serves at its
|
|
11
|
-
* attestations API. A bundle is a keyless (Fulcio) signature
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* attestations API. A bundle is a keyless (Fulcio) signature with a Rekor
|
|
12
|
+
* transparency-log inclusion proof, over one of two content arms: a DSSE-wrapped
|
|
13
|
+
* in-toto attestation, which `cosign attest` and npm provenance produce, or a
|
|
14
|
+
* message signature over an artifact's own bytes, which `cosign sign-blob`
|
|
15
|
+
* produces. `verifyBundle` composes five fail-closed legs against caller-supplied
|
|
14
16
|
* trust material (the Fulcio CA roots + Rekor log keys, never trusted from the
|
|
15
|
-
* bundle): the
|
|
16
|
-
* the
|
|
17
|
-
*
|
|
18
|
-
* tree root; the log entry binding
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
17
|
+
* bundle): the signature under the Fulcio leaf key, over the DSSE PAE preimage or
|
|
18
|
+
* over the artifact `opts.artifact` supplies; the Fulcio certificate chain,
|
|
19
|
+
* validated as of the Rekor log time (the cert is ephemeral, ~10 minutes); the
|
|
20
|
+
* Rekor inclusion proof folded to a Rekor-signed tree root; the log entry binding
|
|
21
|
+
* to this exact signature and certificate; and the artifact binding, which is the
|
|
22
|
+
* in-toto subject digest the caller confirms for a DSSE bundle and the entry's own
|
|
23
|
+
* authenticated hash for a message signature. Verify-only and offline: every input
|
|
24
|
+
* is in the bundle or a caller argument. Reuses the shipped X.509 parser, RFC 5280
|
|
25
|
+
* path validator, RFC 9162 Merkle verifier, and native crypto engine; the net-new
|
|
26
|
+
* codecs are the DSSE PAE byte-builder and a fail-closed JSON bundle reader.
|
|
24
27
|
* @spec DSSE, Sigstore bundle v0.3, RFC 9162, SLSA provenance v1
|
|
25
28
|
* @card Verify an npm --provenance Sigstore bundle offline (DSSE + Fulcio + Rekor + SLSA).
|
|
26
29
|
*/
|
|
@@ -42,7 +45,99 @@ var C = constants;
|
|
|
42
45
|
var SigstoreError = frameworkError.SigstoreError;
|
|
43
46
|
function _err(code, message, cause) { return new SigstoreError(code, message, cause); }
|
|
44
47
|
|
|
48
|
+
/** @internal Whether a oneof arm is PRESENT, which is not the same as whether it is truthy. A bundle
|
|
49
|
+
* setting an arm to false or to an empty string has set it: reading presence as truthiness lets a
|
|
50
|
+
* second arm sit beside the one being read, which is the state the oneof rule exists to refuse. Own
|
|
51
|
+
* properties only, so a polluted prototype cannot conjure an arm the bundle does not carry. */
|
|
52
|
+
function _armPresent(obj, name) {
|
|
53
|
+
return !!obj && typeof obj === "object" &&
|
|
54
|
+
_hasOwn(obj, name) && obj[name] !== undefined && obj[name] !== null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** @internal The HashAlgorithm enum members a message signature may name its digest under, keyed by
|
|
58
|
+
* the name protojson writes. All five members that name an algorithm are here, the two SHA-3
|
|
59
|
+
* members included: they
|
|
60
|
+
* are marked deprecated, which says a producer should not choose one, not that a bundle carrying one
|
|
61
|
+
* is malformed, and this digest names its own algorithm independently of the one the log entry
|
|
62
|
+
* records. A member this table does not carry is refused rather than mapped by guessing at its name,
|
|
63
|
+
* so a member added to the enum later cannot be read as one of these.
|
|
64
|
+
* HASH_ALGORITHM_UNSPECIFIED is absent for the reason it exists: it names no algorithm. */
|
|
65
|
+
var _HASH_BY_ENUM = Object.freeze(Object.assign(Object.create(null), {
|
|
66
|
+
SHA2_256: { node: "sha256", bytes: 32 },
|
|
67
|
+
SHA2_384: { node: "sha384", bytes: 48 },
|
|
68
|
+
SHA2_512: { node: "sha512", bytes: 64 },
|
|
69
|
+
SHA3_256: { node: "sha3-256", bytes: 32 },
|
|
70
|
+
SHA3_384: { node: "sha3-384", bytes: 48 },
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
/** @internal The same members under the numbers the enum assigns them. The JSON mapping states that
|
|
74
|
+
* a parser "accept[s] both enum names and integer values", so a bundle naming its digest algorithm as
|
|
75
|
+
* 1 is one a conforming producer may write and this reads it as the name it stands for. Zero is
|
|
76
|
+
* absent for the reason the name it belongs to is: HASH_ALGORITHM_UNSPECIFIED names no algorithm. */
|
|
77
|
+
var _HASH_BY_NUMBER = Object.freeze(Object.assign(Object.create(null), {
|
|
78
|
+
1: _HASH_BY_ENUM.SHA2_256,
|
|
79
|
+
2: _HASH_BY_ENUM.SHA2_384,
|
|
80
|
+
3: _HASH_BY_ENUM.SHA2_512,
|
|
81
|
+
4: _HASH_BY_ENUM.SHA3_256,
|
|
82
|
+
5: _HASH_BY_ENUM.SHA3_384,
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
/** @internal The algorithm a HashAlgorithm field names, in either encoding, or null when it names
|
|
86
|
+
* none this build verifies under. A number that is not one of the members is refused rather than
|
|
87
|
+
* rounded or coerced into one. */
|
|
88
|
+
function _hashOf(algorithm) {
|
|
89
|
+
if (typeof algorithm === "string") return _HASH_BY_ENUM[algorithm] || null;
|
|
90
|
+
if (typeof algorithm === "number" && _isSafeInt(algorithm)) {
|
|
91
|
+
return _HASH_BY_NUMBER[algorithm] || null;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** @internal The same three algorithms keyed the way a hashedrekord names them. The entry is the
|
|
97
|
+
* authenticated statement of what was signed, so its algorithm is the one the artifact is digested
|
|
98
|
+
* under; the schema enumerates exactly these three. */
|
|
99
|
+
var _HASH_BY_REKOR = Object.freeze(Object.assign(Object.create(null), {
|
|
100
|
+
sha256: { node: "sha256", bytes: 32 },
|
|
101
|
+
sha384: { node: "sha384", bytes: 48 },
|
|
102
|
+
sha512: { node: "sha512", bytes: 64 },
|
|
103
|
+
}));
|
|
104
|
+
|
|
45
105
|
var JSON_MAX = C.LIMITS.JSON_MAX_BYTES;
|
|
106
|
+
|
|
107
|
+
/** @internal How deeply a bundle may nest, stated once and read by both routes into it. A bundle
|
|
108
|
+
* given as text and the same bundle given as an object are the same document, so a limit either one
|
|
109
|
+
* applied alone would admit it one way and refuse it the other. */
|
|
110
|
+
var BUNDLE_MAX_DEPTH = 64;
|
|
111
|
+
|
|
112
|
+
/** @internal The operations the bundle copy decides with, taken at load. That copy is what every
|
|
113
|
+
* later check runs on, so which function answers "what does this object own" and "what does it hold"
|
|
114
|
+
* settles what is verified; one replaced after this module loads would otherwise be asked at the
|
|
115
|
+
* moment of the call. An emptied key list alone turns a bundle into one carrying no content arm. */
|
|
116
|
+
var _ownNames = Object.getOwnPropertyNames;
|
|
117
|
+
var _ownDescriptor = Object.getOwnPropertyDescriptor;
|
|
118
|
+
var _isArray = Array.isArray;
|
|
119
|
+
var _create = Object.create;
|
|
120
|
+
var _stringOf = String;
|
|
121
|
+
/** @internal Whether an object OWNS a name. The oneof rules are written on this question, so a
|
|
122
|
+
* replacement answering no for one arm hides it and a bundle setting two is read as setting one. */
|
|
123
|
+
var _hasOwn = Function.prototype.call.bind(Object.prototype.hasOwnProperty);
|
|
124
|
+
var _isFiniteNumber = Number.isFinite;
|
|
125
|
+
var _isSafeInt = Number.isSafeInteger;
|
|
126
|
+
var _isBuffer = Buffer.isBuffer;
|
|
127
|
+
var _bufferFrom = Buffer.from;
|
|
128
|
+
/** @internal The canonical form a Rekor signed entry timestamp is verified over. A replacement
|
|
129
|
+
* writing different bytes decides what that signature is checked against. */
|
|
130
|
+
var _jsonStringify = JSON.stringify;
|
|
131
|
+
/** @internal Every field the copy holds is DEFINED as its own, never assigned. An assignment consults
|
|
132
|
+
* the target's prototype for a setter, and something that installs one sees each field the copy
|
|
133
|
+
* writes and can hand back a different one, so a malformed value arrives valid and the checks read
|
|
134
|
+
* what the caller never sent. Defining reaches no prototype. */
|
|
135
|
+
var _defineProp = Object.defineProperty;
|
|
136
|
+
var _isFiniteNum = isFinite;
|
|
137
|
+
/** @internal Uncurried, so the copy reads a character's code through the operation taken at load
|
|
138
|
+
* rather than through whatever the string it is walking offers under that name. What a character
|
|
139
|
+
* costs decides whether a bundle passes the size limit. */
|
|
140
|
+
var _charCodeAt = Function.prototype.call.bind(String.prototype.charCodeAt);
|
|
46
141
|
var SP = Buffer.from(" ", "ascii");
|
|
47
142
|
var DSSEV1 = Buffer.from("DSSEv1", "ascii");
|
|
48
143
|
var _EM_DASH = String.fromCharCode(0x2014);
|
|
@@ -80,12 +175,12 @@ var _BUNDLE_MEDIA_TYPES = Object.assign(Object.create(null), {
|
|
|
80
175
|
"application/vnd.dev.sigstore.bundle+json;version=0.3": 1
|
|
81
176
|
});
|
|
82
177
|
function _isBundleMediaType(mt) {
|
|
83
|
-
return typeof mt === "string" &&
|
|
178
|
+
return typeof mt === "string" && _hasOwn(_BUNDLE_MEDIA_TYPES, mt);
|
|
84
179
|
}
|
|
85
180
|
|
|
86
181
|
function _jsonParse(input, code, label) {
|
|
87
182
|
return guard.json.parse(input, _err, {
|
|
88
|
-
maxBytes: JSON_MAX, maxDepth:
|
|
183
|
+
maxBytes: JSON_MAX, maxDepth: BUNDLE_MAX_DEPTH,
|
|
89
184
|
badJson: code, tooDeep: code, duplicateMember: code, tooLarge: code, badInput: code, label: label,
|
|
90
185
|
});
|
|
91
186
|
}
|
|
@@ -112,7 +207,7 @@ function _jsonParse(input, code, label) {
|
|
|
112
207
|
function pae(payloadType, payloadBytes) {
|
|
113
208
|
if (typeof payloadType !== "string") throw new TypeError("pae: payloadType must be a string");
|
|
114
209
|
var type = Buffer.from(payloadType, "utf8");
|
|
115
|
-
var body =
|
|
210
|
+
var body = _isBuffer(payloadBytes) ? payloadBytes : _bufferFrom(payloadBytes || []);
|
|
116
211
|
return Buffer.concat([
|
|
117
212
|
DSSEV1, SP,
|
|
118
213
|
Buffer.from(String(type.length), "ascii"), SP, type, SP,
|
|
@@ -145,7 +240,14 @@ function _b64(s, label) {
|
|
|
145
240
|
* Buffer) fail-closed: a non-object, malformed JSON, an oversize input, an
|
|
146
241
|
* unknown `mediaType`, or a missing required member throws a typed
|
|
147
242
|
* `sigstore/bad-bundle` / `sigstore/bad-bundle-version`. Returns the validated
|
|
148
|
-
* bundle
|
|
243
|
+
* bundle (structure only, no cryptographic verification).
|
|
244
|
+
*
|
|
245
|
+
* An object is copied into plain data and read back before any rule runs on it,
|
|
246
|
+
* and the copy is what comes back, so a caller reads what was checked. A field
|
|
247
|
+
* reached through an accessor is refused rather than called, and so is a value
|
|
248
|
+
* JSON does not carry: a bundle is data, and a field that computes its value can
|
|
249
|
+
* remove another the walk has already counted. `pki.sigstore.verifyBundle` takes
|
|
250
|
+
* the same copy, so the two verbs cannot answer differently about one bundle.
|
|
149
251
|
*
|
|
150
252
|
* @example
|
|
151
253
|
* // requires: `bundle` -- a Sigstore bundle as cosign or npm provenance emits it
|
|
@@ -155,14 +257,18 @@ function _b64(s, label) {
|
|
|
155
257
|
*/
|
|
156
258
|
function parseBundle(input) {
|
|
157
259
|
var obj;
|
|
158
|
-
if (
|
|
260
|
+
if (_isBuffer(input) || typeof input === "string") {
|
|
159
261
|
obj = _jsonParse(input, "sigstore/bad-bundle", "bundle");
|
|
160
262
|
} else if (input && typeof input === "object") {
|
|
161
|
-
|
|
263
|
+
/** @internal An object is copied and read back before a single rule runs on it, so this verb
|
|
264
|
+
* decides what the verifying one decides. Checking the caller's object in place would run its
|
|
265
|
+
* accessors, and one of those can remove a field a check has already counted: a bundle owning
|
|
266
|
+
* two content arms passes the oneof rule when reading the first deletes the second. */
|
|
267
|
+
obj = _snapshotBundle(input);
|
|
162
268
|
} else {
|
|
163
269
|
throw _err("sigstore/bad-bundle", "bundle must be a JSON object, string, or Buffer");
|
|
164
270
|
}
|
|
165
|
-
if (!obj || typeof obj !== "object" ||
|
|
271
|
+
if (!obj || typeof obj !== "object" || _isArray(obj)) throw _err("sigstore/bad-bundle", "bundle must be a JSON object");
|
|
166
272
|
var mt = obj.mediaType;
|
|
167
273
|
if (!_isBundleMediaType(mt)) {
|
|
168
274
|
throw _err("sigstore/bad-bundle-version", "unsupported or unknown bundle media type: " + (mt === undefined ? "(none)" : guard.text.showValue(mt)));
|
|
@@ -170,20 +276,292 @@ function parseBundle(input) {
|
|
|
170
276
|
if (!obj.verificationMaterial || typeof obj.verificationMaterial !== "object") {
|
|
171
277
|
throw _err("sigstore/bad-bundle", "bundle is missing verificationMaterial");
|
|
172
278
|
}
|
|
173
|
-
|
|
174
|
-
|
|
279
|
+
_assertMaterialOneOf(obj.verificationMaterial);
|
|
280
|
+
_assertContentOneOf(obj);
|
|
281
|
+
if (_armPresent(obj, "messageSignature")) {
|
|
282
|
+
_parseMessageSignature(obj.messageSignature);
|
|
283
|
+
return obj;
|
|
175
284
|
}
|
|
176
285
|
if (!obj.dsseEnvelope || typeof obj.dsseEnvelope !== "object") {
|
|
177
286
|
throw _err("sigstore/bad-bundle", "bundle is missing a dsseEnvelope");
|
|
178
287
|
}
|
|
179
288
|
var d = obj.dsseEnvelope;
|
|
180
|
-
if (typeof d.payload !== "string" || typeof d.payloadType !== "string" || !
|
|
289
|
+
if (typeof d.payload !== "string" || typeof d.payloadType !== "string" || !_isArray(d.signatures) || !d.signatures.length ||
|
|
181
290
|
!d.signatures[0] || typeof d.signatures[0] !== "object" || typeof d.signatures[0].sig !== "string") {
|
|
182
291
|
throw _err("sigstore/bad-dsse", "the DSSE envelope is missing a required field (payload / payloadType / signatures[].sig)");
|
|
183
292
|
}
|
|
184
293
|
return obj;
|
|
185
294
|
}
|
|
186
295
|
|
|
296
|
+
/** @internal The bundle's content is a protobuf oneof, so a bundle setting both arms is malformed
|
|
297
|
+
* and is refused rather than read as whichever arm this build happens to support: verifying the
|
|
298
|
+
* envelope and passing over the other arm reports a verdict that says nothing about content the
|
|
299
|
+
* bundle carries. */
|
|
300
|
+
function _assertContentOneOf(obj) {
|
|
301
|
+
if (_armPresent(obj, "messageSignature") && _armPresent(obj, "dsseEnvelope")) {
|
|
302
|
+
throw _err("sigstore/bad-bundle", "bundle carries both dsse_envelope and message_signature; the bundle content is a oneof and exactly one arm may be present");
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** @internal The verification material's certificate is a oneof of the same kind. Both arms set draws
|
|
307
|
+
* the leaf from one and the chain it is validated through from the other, so the certificate whose
|
|
308
|
+
* key is checked and the certificates it is checked through come from different statements about the
|
|
309
|
+
* same signer. The two refusals are separate because a caller can act on one and not the other. */
|
|
310
|
+
function _assertMaterialOneOf(vm) {
|
|
311
|
+
var arms = 0;
|
|
312
|
+
if (_armPresent(vm, "certificate")) arms++;
|
|
313
|
+
if (_armPresent(vm, "x509CertificateChain")) arms++;
|
|
314
|
+
if (_armPresent(vm, "publicKey")) arms++;
|
|
315
|
+
if (arms > 1) {
|
|
316
|
+
throw _err("sigstore/bad-bundle", "verificationMaterial carries more than one of certificate, x509CertificateChain and publicKey; it is a oneof and exactly one arm may be present");
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** @internal The bytes a string costs in the JSON a document would be written as, charged while it is
|
|
321
|
+
* scanned so a long one is refused part-way rather than measured whole. Counted as JSON writes it: an
|
|
322
|
+
* escape costs the characters it takes, a character outside ASCII costs its UTF-8 bytes, a surrogate
|
|
323
|
+
* pair costs the four its code point takes, and an unpaired surrogate costs the six a \u escape
|
|
324
|
+
* takes. Charging the string's own length instead charges UTF-16 units, which is fewer than the text
|
|
325
|
+
* form for anything escaped or non-ASCII, and would admit an object the same document as text is
|
|
326
|
+
* refused for. Scanned by character code, since a pattern here would be matched through a replaceable
|
|
327
|
+
* protocol. */
|
|
328
|
+
function _chargeString(budget, s) {
|
|
329
|
+
_charge(budget, 2);
|
|
330
|
+
for (var i = 0; i < s.length; i++) {
|
|
331
|
+
var c = _charCodeAt(s, i);
|
|
332
|
+
var cost;
|
|
333
|
+
if (c === 0x22 || c === 0x5c) cost = 2;
|
|
334
|
+
else if (c === 0x08 || c === 0x09 || c === 0x0a || c === 0x0c || c === 0x0d) cost = 2;
|
|
335
|
+
else if (c < 0x20) cost = 6;
|
|
336
|
+
else if (c < 0x80) cost = 1;
|
|
337
|
+
else if (c < 0x800) cost = 2;
|
|
338
|
+
else if (c >= 0xd800 && c <= 0xdbff) {
|
|
339
|
+
var next = i + 1 < s.length ? _charCodeAt(s, i + 1) : 0;
|
|
340
|
+
if (next >= 0xdc00 && next <= 0xdfff) { cost = 4; i++; } else { cost = 6; }
|
|
341
|
+
} else if (c >= 0xdc00 && c <= 0xdfff) { cost = 6; } else { cost = 3; }
|
|
342
|
+
_charge(budget, cost);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** @internal A bundle field read as a number, without asking the value to convert itself. A log entry
|
|
347
|
+
* carries its time and index as JSON numbers, and a document may write either as a string, so both
|
|
348
|
+
* are read; anything else is not a number and reads as one that is not finite, which the caller of
|
|
349
|
+
* this already refuses. Converting an arbitrary value would ask it for a primitive, and a value that
|
|
350
|
+
* has no way to give one throws where this module's contract is a typed refusal. */
|
|
351
|
+
function _numOf(v) {
|
|
352
|
+
var t = typeof v;
|
|
353
|
+
if (t === "number") return v;
|
|
354
|
+
if (t === "string" || t === "boolean") return Number(v);
|
|
355
|
+
return NaN;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** @internal Charge the copy's running size, refusing the moment it passes what the reader admits.
|
|
359
|
+
* What is counted is the size the same document costs as text, exactly: the punctuation JSON writes
|
|
360
|
+
* around a value, the digits a number takes, and the bytes a string takes once escaped. Counting
|
|
361
|
+
* less would admit an object the text reader refuses; counting more would refuse one it admits. */
|
|
362
|
+
function _charge(budget, cost) {
|
|
363
|
+
budget.n += cost;
|
|
364
|
+
if (budget.n > JSON_MAX) {
|
|
365
|
+
throw _err("sigstore/bad-bundle", "the bundle is larger than the " + JSON_MAX + " byte limit");
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** @internal Bound the WALK, which is a separate question from the size. A property whose value is
|
|
370
|
+
* skipped costs the walk a step while costing the document nothing, so a structure made of them is
|
|
371
|
+
* bounded here rather than by the byte count, which would have to overstate the document to do it. */
|
|
372
|
+
function _step(budget) {
|
|
373
|
+
budget.steps++;
|
|
374
|
+
if (budget.steps > JSON_MAX) {
|
|
375
|
+
throw _err("sigstore/bad-bundle", "the bundle holds more values than a bundle of the maximum size can");
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** @internal One property of the caller's structure, copied. Taken from the property's DESCRIPTOR,
|
|
380
|
+
* which does not run an accessor, and an accessor is refused rather than called. A bundle is data; a
|
|
381
|
+
* field that computes its value is not one. Reading through the object would run the caller's code in
|
|
382
|
+
* the middle of the walk, and that code can change the object being walked: a getter on one property
|
|
383
|
+
* deleting another leaves a key this already listed reading as absent, so an object carrying two
|
|
384
|
+
* content arms would be copied with one. Not calling anything is what makes that unreachable, rather
|
|
385
|
+
* than another rule about what a second read may say. Named properties and array elements both come
|
|
386
|
+
* through here, because an indexed accessor runs the same code a named one does.
|
|
387
|
+
* Returns `undefined` for a property that is absent or holds `undefined`, which is what a bundle
|
|
388
|
+
* without that field means. */
|
|
389
|
+
function _valueOf(obj, key, depth, budget) {
|
|
390
|
+
var d = _ownDescriptor(obj, key);
|
|
391
|
+
if (d === undefined) return undefined;
|
|
392
|
+
if (typeof d.get === "function" || typeof d.set === "function") {
|
|
393
|
+
throw _err("sigstore/bad-bundle", "the bundle reaches " + guard.text.showValue(key) +
|
|
394
|
+
" through an accessor rather than holding a value; a bundle is data");
|
|
395
|
+
}
|
|
396
|
+
if (d.value === undefined) return undefined;
|
|
397
|
+
return _plainCopy(d.value, depth + 1, budget);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** @internal A bundle object copied into plain data, reading each own enumerable property EXACTLY
|
|
401
|
+
* ONCE. That single read is the whole guarantee: an accessor has no second answer to give, so no two
|
|
402
|
+
* observations of the same field can disagree and no check can be shown one value while another is
|
|
403
|
+
* used. Serializing the object instead would read it a second time, which is how a field could be
|
|
404
|
+
* checked in one place and used in another.
|
|
405
|
+
* A value JSON does not carry is refused rather than skipped. Skipping is what serializing does, and
|
|
406
|
+
* a dropped value turns a bundle setting two content arms into one setting a single arm: a refusal
|
|
407
|
+
* sanitized into an accept. A bundle is JSON data, so a function, a symbol or a non-finite number
|
|
408
|
+
* anywhere in it means the object is not one. `undefined` is the exception and is skipped, since that
|
|
409
|
+
* is what a bundle absent a field means and what the presence test already reads it as. */
|
|
410
|
+
function _plainCopy(v, depth, budget) {
|
|
411
|
+
/** @internal The size the copy would serialize to, charged AS IT IS BUILT so the refusal comes
|
|
412
|
+
* before the allocation rather than after it. Two shapes both need this and neither is bounded by
|
|
413
|
+
* a depth limit: one sub-object shared between two properties is copied twice, so a few hundred
|
|
414
|
+
* bytes describes hundreds of millions of values, and a modest array of long strings is a handful
|
|
415
|
+
* of values holding tens of megabytes. Each value is charged what JSON writes it as, so the object
|
|
416
|
+
* path admits and refuses exactly what the text path does. */
|
|
417
|
+
_step(budget);
|
|
418
|
+
if (depth > BUNDLE_MAX_DEPTH) {
|
|
419
|
+
throw _err("sigstore/bad-bundle", "the bundle nests deeper than " + BUNDLE_MAX_DEPTH + " levels");
|
|
420
|
+
}
|
|
421
|
+
if (v === null) { _charge(budget, 4); return null; }
|
|
422
|
+
var t = typeof v;
|
|
423
|
+
if (t === "string") { _chargeString(budget, v); return v; }
|
|
424
|
+
if (t === "boolean") { _charge(budget, v ? 4 : 5); return v; }
|
|
425
|
+
if (t === "number") {
|
|
426
|
+
if (!_isFiniteNum(v)) throw _err("sigstore/bad-bundle", "the bundle carries a number JSON cannot represent");
|
|
427
|
+
/** @internal A finite number is written by JSON exactly as the language writes it, so the digits
|
|
428
|
+
* it takes are the bytes it costs. One in exponent form is long and would otherwise be counted
|
|
429
|
+
* as a single byte. */
|
|
430
|
+
_charge(budget, _stringOf(v).length);
|
|
431
|
+
return v;
|
|
432
|
+
}
|
|
433
|
+
if (t !== "object") {
|
|
434
|
+
throw _err("sigstore/bad-bundle", "the bundle carries a " + t + " where JSON carries only objects, arrays, strings, numbers, booleans and null");
|
|
435
|
+
}
|
|
436
|
+
/** @internal The two brackets or braces this value is written between. */
|
|
437
|
+
_charge(budget, 2);
|
|
438
|
+
var out, i;
|
|
439
|
+
if (_isArray(v)) {
|
|
440
|
+
out = [];
|
|
441
|
+
for (i = 0; i < v.length; i++) {
|
|
442
|
+
if (i > 0) _charge(budget, 1);
|
|
443
|
+
/** @internal An element is a property, and is taken the same way a named one is. An indexed
|
|
444
|
+
* accessor runs the caller's code exactly as a named accessor does, and can do the same thing
|
|
445
|
+
* with it: deleting a content arm the walk has already listed. A hole is written as null, and
|
|
446
|
+
* costs what null costs. */
|
|
447
|
+
var el = _valueOf(v, _stringOf(i), depth, budget);
|
|
448
|
+
if (el === undefined) _charge(budget, 4);
|
|
449
|
+
_defineProp(out, _stringOf(out.length), { value: el, writable: true, enumerable: true, configurable: true });
|
|
450
|
+
}
|
|
451
|
+
return out;
|
|
452
|
+
}
|
|
453
|
+
/** @internal Built with no prototype, so a property named __proto__ is copied as a field of that
|
|
454
|
+
* name. Assigning it onto an ordinary object runs the inherited setter instead, which sets the
|
|
455
|
+
* copy's prototype rather than one of its fields: an object owning nothing but __proto__ would
|
|
456
|
+
* then read as whatever it holds, and the same document as JSON text is refused. The reader this
|
|
457
|
+
* module parses text with states the same rule, so both routes agree. */
|
|
458
|
+
out = _create(null);
|
|
459
|
+
/** @internal EVERY own string-keyed property, not only the enumerable ones, because that is what
|
|
460
|
+
* the presence test the oneof rules are built on reads. Enumerating a narrower set than the test
|
|
461
|
+
* does is how a second content arm gets counted where the bundle is parsed and dropped where it is
|
|
462
|
+
* copied, which is a refusal turned into an acceptance. */
|
|
463
|
+
var keys = _ownNames(v);
|
|
464
|
+
var kept = 0;
|
|
465
|
+
for (i = 0; i < keys.length; i++) {
|
|
466
|
+
/** @internal Every enumerated property costs the walk a step, whether or not its value is kept.
|
|
467
|
+
* Its SIZE is charged only if it is kept, since JSON omits a property whose value is not
|
|
468
|
+
* written and charging one would refuse a document the text reader admits. */
|
|
469
|
+
_step(budget);
|
|
470
|
+
/** @internal Taken from the property's DESCRIPTOR, which does not run an accessor, and an
|
|
471
|
+
* accessor is refused rather than called. A bundle is data; a field that computes its value is
|
|
472
|
+
* not one. Reading through the object would run the caller's code in the middle of the walk,
|
|
473
|
+
* and that code can change the object being walked: a getter on one property deleting another
|
|
474
|
+
* leaves a key this already listed reading as absent, so an object carrying two content arms
|
|
475
|
+
* would be copied with one. Not calling anything is what makes that unreachable, rather than
|
|
476
|
+
* another rule about what a second read may say. */
|
|
477
|
+
var val = _valueOf(v, keys[i], depth, budget);
|
|
478
|
+
if (val === undefined) continue;
|
|
479
|
+
/** @internal What JSON writes around a kept property: the comma before it once one has been
|
|
480
|
+
* written, its name in quotes, and the colon. */
|
|
481
|
+
if (kept > 0) _charge(budget, 1);
|
|
482
|
+
_chargeString(budget, keys[i]);
|
|
483
|
+
_charge(budget, 1);
|
|
484
|
+
kept++;
|
|
485
|
+
_defineProp(out, keys[i], { value: val, writable: true, enumerable: true, configurable: true });
|
|
486
|
+
}
|
|
487
|
+
return out;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** @internal The bundle a verdict is computed from: JSON text as it came, or an object copied once
|
|
491
|
+
* into plain data and re-read through the same reader, so the caps that bound a text bundle bound an
|
|
492
|
+
* object one too. Every check below runs on that copy and none of them consults the original again,
|
|
493
|
+
* which is what makes the verdict a statement about one fixed set of bytes.
|
|
494
|
+
* The scope this cannot reach, stated so it is not mistaken for an oversight: an exotic object whose
|
|
495
|
+
* own descriptors say one thing while a direct property read says another, which a Proxy can be
|
|
496
|
+
* written to do. There is no truer answer to prefer between the two, and the verdict describes the
|
|
497
|
+
* one the copy took, so it remains a statement about what was actually verified. A caller comparing
|
|
498
|
+
* a separate parseBundle of the same object may see the other answer. */
|
|
499
|
+
function _snapshotBundle(bundle) {
|
|
500
|
+
/** @internal The copy IS the snapshot. It is plain data already, and the size and depth the reader
|
|
501
|
+
* bounds a text bundle by were charged while it was built, so serializing it and reading it back
|
|
502
|
+
* would repeat work that is already done. It would also put a step between the copy and the checks
|
|
503
|
+
* that is not this module's: a serializer replaced after this module loaded is read at the moment
|
|
504
|
+
* it is called, and one that drops a field would hand the checks a bundle carrying one content arm
|
|
505
|
+
* where the copy holds two. Nothing here converts the copy to text, so there is no such step.
|
|
506
|
+
* Reading a caller's object runs the caller's own accessors, and one that throws must arrive as
|
|
507
|
+
* this module's refusal rather than as whatever it threw: a bundle this cannot read is refused with
|
|
508
|
+
* a typed error like any other it cannot read. This module's own refusals pass through unchanged,
|
|
509
|
+
* so a size or depth verdict keeps its own message. */
|
|
510
|
+
try {
|
|
511
|
+
return _plainCopy(bundle, 0, { n: 0, steps: 0 });
|
|
512
|
+
} catch (e) {
|
|
513
|
+
if (e instanceof SigstoreError) throw e;
|
|
514
|
+
throw _err("sigstore/bad-bundle", "the bundle object could not be read", e);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** @internal Which content arm a parsed bundle carries. parseBundle has already refused a bundle
|
|
519
|
+
* setting both or neither, so this reads the one that is there. */
|
|
520
|
+
function _contentTypeOf(obj) {
|
|
521
|
+
return _armPresent(obj, "messageSignature") ? "messageSignature" : "dsseEnvelope";
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** @internal The shape of a MessageSignature (sigstore_common.proto). `signature` is REQUIRED;
|
|
525
|
+
* `message_digest` is not, and the bundle format states why: a verifier is given the artifact, so the
|
|
526
|
+
* digest is a hint rather than an input. The field comment says so outright, "Clients MUST NOT attempt
|
|
527
|
+
* to use this digest to verify the associated signature; it is intended solely for identification",
|
|
528
|
+
* which is why nothing here derives a verification input from it. What it IS held to is describing
|
|
529
|
+
* itself: an algorithm this build knows, and a digest of that algorithm's own length. */
|
|
530
|
+
function _parseMessageSignature(ms) {
|
|
531
|
+
if (!ms || typeof ms !== "object" || _isArray(ms)) {
|
|
532
|
+
throw _err("sigstore/bad-message-signature", "messageSignature is not an object");
|
|
533
|
+
}
|
|
534
|
+
var sig = ms.signature;
|
|
535
|
+
if (typeof sig !== "string" || sig === "") {
|
|
536
|
+
throw _err("sigstore/bad-message-signature", "messageSignature is missing its signature");
|
|
537
|
+
}
|
|
538
|
+
if (!_armPresent(ms, "messageDigest")) return { signature: sig, digestAlg: null, digest: null };
|
|
539
|
+
var md = ms.messageDigest;
|
|
540
|
+
if (typeof md !== "object" || _isArray(md)) {
|
|
541
|
+
throw _err("sigstore/bad-message-signature", "messageSignature.messageDigest is not an object");
|
|
542
|
+
}
|
|
543
|
+
/** @internal Read for being a string or an integer BEFORE it is used as a key. A value whose own
|
|
544
|
+
* conversion throws would otherwise escape as an untyped error out of a verb whose contract is that
|
|
545
|
+
* malformed input is refused with a typed one, and a bundle is caller-supplied JSON. */
|
|
546
|
+
var alg = _hashOf(md.algorithm);
|
|
547
|
+
if (!alg) {
|
|
548
|
+
throw _err("sigstore/bad-message-signature", "messageSignature.messageDigest names a hash algorithm this build does not verify under: " +
|
|
549
|
+
(md.algorithm === undefined ? "(none)" : guard.text.showValue(md.algorithm)));
|
|
550
|
+
}
|
|
551
|
+
if (typeof md.digest !== "string") {
|
|
552
|
+
throw _err("sigstore/bad-message-signature", "messageSignature.messageDigest is missing its digest");
|
|
553
|
+
}
|
|
554
|
+
var raw = _b64(md.digest, "messageSignature.messageDigest.digest");
|
|
555
|
+
if (raw.length !== alg.bytes) {
|
|
556
|
+
throw _err("sigstore/bad-message-signature", "messageSignature.messageDigest states " + md.algorithm +
|
|
557
|
+
" and carries " + raw.length + " bytes, which that algorithm does not produce");
|
|
558
|
+
}
|
|
559
|
+
/** @internal The checked values are RETURNED rather than left to be read again. A bundle is a
|
|
560
|
+
* caller-supplied object, so a second read of the same field is a second chance to answer
|
|
561
|
+
* differently; everything downstream uses what was validated here. */
|
|
562
|
+
return { signature: sig, digestAlg: alg, digest: raw };
|
|
563
|
+
}
|
|
564
|
+
|
|
187
565
|
|
|
188
566
|
function _certBytes(c, label) {
|
|
189
567
|
if (!c || typeof c !== "object" || typeof c.rawBytes !== "string") throw _err("sigstore/bad-bundle", label + " is not a { rawBytes } certificate");
|
|
@@ -191,13 +569,13 @@ function _certBytes(c, label) {
|
|
|
191
569
|
}
|
|
192
570
|
function _leafCertDer(vm) {
|
|
193
571
|
if (vm.certificate) return _certBytes(vm.certificate, "verificationMaterial.certificate");
|
|
194
|
-
if (vm.x509CertificateChain &&
|
|
572
|
+
if (vm.x509CertificateChain && _isArray(vm.x509CertificateChain.certificates) && vm.x509CertificateChain.certificates.length) {
|
|
195
573
|
return _certBytes(vm.x509CertificateChain.certificates[0], "verificationMaterial.x509CertificateChain[0]");
|
|
196
574
|
}
|
|
197
575
|
throw _err("sigstore/bad-bundle", "bundle has no Fulcio certificate (public_key bundles are not supported)");
|
|
198
576
|
}
|
|
199
577
|
function _chainDers(vm) {
|
|
200
|
-
if (vm.x509CertificateChain &&
|
|
578
|
+
if (vm.x509CertificateChain && _isArray(vm.x509CertificateChain.certificates)) {
|
|
201
579
|
return vm.x509CertificateChain.certificates.map(function (c, i) { return _certBytes(c, "x509CertificateChain[" + i + "]"); });
|
|
202
580
|
}
|
|
203
581
|
return [_leafCertDer(vm)];
|
|
@@ -233,7 +611,7 @@ function _sha256(buf) { return nodeCrypto.createHash("sha256").update(buf).diges
|
|
|
233
611
|
|
|
234
612
|
function _toMs(x) {
|
|
235
613
|
if (x == null) return null;
|
|
236
|
-
if (typeof x === "number") return
|
|
614
|
+
if (typeof x === "number") return _isFiniteNumber(x) ? x : null;
|
|
237
615
|
if (guard.time.isDate(x)) { var d = guard.time.instantOf(x); return isNaN(d) ? null : d; }
|
|
238
616
|
if (typeof x === "string") { var t = Date.parse(x); return isNaN(t) ? null : t; }
|
|
239
617
|
return null;
|
|
@@ -277,29 +655,64 @@ function _verifyCheckpoint(envelope, ip, rekorKeys, timeMs) {
|
|
|
277
655
|
return false;
|
|
278
656
|
}
|
|
279
657
|
|
|
280
|
-
function _verifySet(te, rekorKeys, timeMs) {
|
|
658
|
+
function _verifySet(te, rekorKeys, timeMs, canonBody) {
|
|
281
659
|
var promise = te.inclusionPromise;
|
|
282
660
|
if (!promise || typeof promise.signedEntryTimestamp !== "string") return false;
|
|
283
661
|
var keyId = te.logId && te.logId.keyId ? _b64(te.logId.keyId, "logId.keyId") : null;
|
|
284
662
|
var k = _rekorKey(rekorKeys, keyId, null, timeMs);
|
|
285
663
|
if (!k) return false;
|
|
286
|
-
var canonical =
|
|
287
|
-
body:
|
|
288
|
-
integratedTime:
|
|
664
|
+
var canonical = _jsonStringify({
|
|
665
|
+
body: canonBody,
|
|
666
|
+
integratedTime: _numOf(te.integratedTime),
|
|
289
667
|
logID: keyId.toString("hex"),
|
|
290
|
-
logIndex:
|
|
668
|
+
logIndex: _numOf(te.logIndex),
|
|
291
669
|
});
|
|
292
670
|
var sig = _b64(promise.signedEntryTimestamp, "signedEntryTimestamp");
|
|
293
671
|
return _rawVerify(_pubFromSpki(k.spki, "Rekor log"), Buffer.from(canonical, "utf8"), sig);
|
|
294
672
|
}
|
|
295
673
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
674
|
+
/** @internal The entry rows this build reads, keyed on the entry's OWN kind and apiVersion together.
|
|
675
|
+
* A version is not a detail of a kind here: a hashedrekord v0.0.2 body names its fields differently
|
|
676
|
+
* (`spec.hashedRekordV002`, a raw certificate in place of a PEM), so dispatching on the kind alone
|
|
677
|
+
* would read one shape's field names out of the other and report a mismatch rather than an
|
|
678
|
+
* unsupported entry. Each row states which content arm it may accompany, because an entry describing
|
|
679
|
+
* a DSSE envelope says nothing about a message signature and the two must agree. */
|
|
680
|
+
var _ENTRY_ROWS = Object.freeze(Object.assign(Object.create(null), {
|
|
681
|
+
"dsse@0.0.1": { content: "dsseEnvelope", bind: _bindDsseEntry },
|
|
682
|
+
"hashedrekord@0.0.1": { content: "messageSignature", bind: _bindHashedRekordEntry },
|
|
683
|
+
}));
|
|
684
|
+
|
|
685
|
+
function _entryBody(canonBytes) {
|
|
686
|
+
var body = _jsonParse(canonBytes, "sigstore/bad-tlog-entry", "Rekor entry body");
|
|
687
|
+
if (!body || typeof body !== "object" || _isArray(body)) throw _err("sigstore/bad-tlog-entry", "the Rekor entry body is not a JSON object");
|
|
688
|
+
return body;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/** @internal The row for an entry, refused when the body names a kind and version this build does not
|
|
692
|
+
* read or one that does not go with the arm the bundle carries. */
|
|
693
|
+
function _entryRow(body, contentType) {
|
|
694
|
+
/** @internal Both halves are read for being strings before either is joined into a key, since the
|
|
695
|
+
* body is caller-supplied JSON and a value whose own conversion throws would escape untyped. */
|
|
696
|
+
if (typeof body.kind !== "string" || typeof body.apiVersion !== "string") {
|
|
697
|
+
throw _err("sigstore/unsupported-content", "the Rekor entry does not name its kind and apiVersion as strings");
|
|
698
|
+
}
|
|
699
|
+
var key = body.kind + "@" + body.apiVersion;
|
|
700
|
+
var row = _ENTRY_ROWS[key];
|
|
701
|
+
if (!row) {
|
|
702
|
+
throw _err("sigstore/unsupported-content", "unsupported Rekor entry kind and version: " + guard.text.showValue(key));
|
|
703
|
+
}
|
|
704
|
+
if (row.content !== contentType) {
|
|
705
|
+
throw _err("sigstore/unsupported-content", "the Rekor entry is a " + guard.text.showValue(key) +
|
|
706
|
+
" while the bundle carries a " + contentType + "; the entry and the content arm must describe the same signature");
|
|
707
|
+
}
|
|
708
|
+
return row;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function _bindDsseEntry(body, envelope, leafDer) {
|
|
299
712
|
var spec = body.spec;
|
|
300
|
-
if (
|
|
713
|
+
if (!spec || typeof spec !== "object" || !_isArray(spec.signatures) || !spec.signatures.length ||
|
|
301
714
|
!spec.signatures[0] || typeof spec.signatures[0] !== "object" || typeof spec.signatures[0].signature !== "string") {
|
|
302
|
-
throw _err("sigstore/
|
|
715
|
+
throw _err("sigstore/bad-tlog-entry", "the Rekor dsse entry is missing its signature");
|
|
303
716
|
}
|
|
304
717
|
var envSig = _b64(envelope.signatures[0].sig, "dsseEnvelope.signatures[0].sig");
|
|
305
718
|
var bodySig = _b64(spec.signatures[0].signature, "Rekor entry signature");
|
|
@@ -317,20 +730,127 @@ function _bindEntry(te, envelope, leafDer) {
|
|
|
317
730
|
if (!vDer.equals(leafDer)) throw _err("sigstore/entry-mismatch", "the Rekor entry verifier certificate does not match the bundle leaf certificate");
|
|
318
731
|
}
|
|
319
732
|
|
|
733
|
+
/** @internal The entry's verifier, bound to the bundle's leaf. The Rekor field is "the public key
|
|
734
|
+
* that can verify the signature; this can also be an X509 code signing certificate that contains the
|
|
735
|
+
* raw public key information", so an entry states one or the other and both are read. A certificate
|
|
736
|
+
* binds as the same certificate; a bare key binds as the same key, which is what the entry claims
|
|
737
|
+
* about it, and the certificate's own identity is settled by the chain leg rather than here. Neither
|
|
738
|
+
* form is guessed at from the bytes: the armor names which one the entry wrote. */
|
|
739
|
+
function _bindVerifier(content, leafDer) {
|
|
740
|
+
var text = _b64(content, "Rekor entry verifier").toString("utf8");
|
|
741
|
+
var vDer;
|
|
742
|
+
try { vDer = x509.pemDecode(text, "CERTIFICATE"); } catch (_e) { vDer = null; } // allow:swallow-unverified the other armor is tried next and a value carrying neither is refused below
|
|
743
|
+
if (vDer !== null) {
|
|
744
|
+
if (!vDer.equals(leafDer)) {
|
|
745
|
+
throw _err("sigstore/entry-mismatch", "the Rekor entry verifier certificate does not match the bundle leaf certificate");
|
|
746
|
+
}
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
var spki;
|
|
750
|
+
try { spki = x509.pemDecode(text, "PUBLIC KEY"); }
|
|
751
|
+
catch (e) { throw _err("sigstore/bad-tlog-entry", "the Rekor entry verifier is neither a certificate nor a public key", e); }
|
|
752
|
+
var leafSpki = _parseCert(leafDer, "Fulcio leaf certificate").subjectPublicKeyInfo.bytes;
|
|
753
|
+
if (!spki.equals(Buffer.from(leafSpki))) {
|
|
754
|
+
throw _err("sigstore/entry-mismatch", "the Rekor entry verifier public key does not match the bundle leaf certificate's key");
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/** @internal Whether a string is a run of lowercase hexadecimal digits. The Rekor schema states the
|
|
759
|
+
* artifact hash that way, and the comparison it feeds comes from a hex encoder that emits lowercase,
|
|
760
|
+
* so an uppercase or non-hex value would compare unequal and be reported as a mismatched artifact
|
|
761
|
+
* rather than as the malformed entry it is. Scanned by character code, since a pattern here would be
|
|
762
|
+
* matched through a replaceable protocol. */
|
|
763
|
+
function _isLowerHex(s) {
|
|
764
|
+
for (var i = 0; i < s.length; i++) {
|
|
765
|
+
var c = s.charCodeAt(i);
|
|
766
|
+
if (!((c >= 0x30 && c <= 0x39) || (c >= 0x61 && c <= 0x66))) return false;
|
|
767
|
+
}
|
|
768
|
+
return s.length > 0;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/** @internal The hashedrekord v0.0.1 sibling of the row above. It answers the same three questions
|
|
772
|
+
* the client specification states for binding an entry to the material being verified: the signature
|
|
773
|
+
* in the body is the one the bundle carries, the certificate in the body is the bundle's leaf, and
|
|
774
|
+
* the body names the artifact. The third is what makes this entry worth having, because
|
|
775
|
+
* `data.hash` sits inside the canonicalized body that the inclusion proof folds to a signed root,
|
|
776
|
+
* while the bundle's own messageDigest is covered by no signature at all. Returns that authenticated
|
|
777
|
+
* hash, which is the statement the artifact is then held to. */
|
|
778
|
+
function _bindHashedRekordEntry(body, ms, leafDer) {
|
|
779
|
+
var spec = body.spec;
|
|
780
|
+
if (!spec || typeof spec !== "object" || !spec.signature || typeof spec.signature !== "object") {
|
|
781
|
+
throw _err("sigstore/bad-tlog-entry", "the Rekor hashedrekord entry has no signature object");
|
|
782
|
+
}
|
|
783
|
+
if (typeof spec.signature.content !== "string") {
|
|
784
|
+
throw _err("sigstore/bad-tlog-entry", "the Rekor hashedrekord entry is missing its signature content");
|
|
785
|
+
}
|
|
786
|
+
var bodySig = _b64(spec.signature.content, "Rekor entry signature");
|
|
787
|
+
var armSig = _b64(ms.signature, "messageSignature.signature");
|
|
788
|
+
if (!armSig.equals(bodySig)) {
|
|
789
|
+
throw _err("sigstore/entry-mismatch", "the Rekor entry signature does not match the bundle signature");
|
|
790
|
+
}
|
|
791
|
+
var pk = spec.signature.publicKey;
|
|
792
|
+
if (!pk || typeof pk !== "object" || typeof pk.content !== "string") {
|
|
793
|
+
throw _err("sigstore/bad-tlog-entry", "the Rekor hashedrekord entry is missing its verifier public key");
|
|
794
|
+
}
|
|
795
|
+
_bindVerifier(pk.content, leafDer);
|
|
796
|
+
var data = spec.data;
|
|
797
|
+
if (!data || typeof data !== "object" || !data.hash || typeof data.hash !== "object") {
|
|
798
|
+
throw _err("sigstore/bad-tlog-entry", "the Rekor hashedrekord entry has no data hash");
|
|
799
|
+
}
|
|
800
|
+
var alg = typeof data.hash.algorithm === "string" ? _HASH_BY_REKOR[data.hash.algorithm] : null;
|
|
801
|
+
if (!alg) {
|
|
802
|
+
throw _err("sigstore/bad-tlog-entry", "the Rekor entry names a hash algorithm outside the schema's own three: " +
|
|
803
|
+
(data.hash.algorithm === undefined ? "(none)" : guard.text.showValue(data.hash.algorithm)));
|
|
804
|
+
}
|
|
805
|
+
if (typeof data.hash.value !== "string" || data.hash.value.length !== alg.bytes * 2 ||
|
|
806
|
+
!_isLowerHex(data.hash.value)) {
|
|
807
|
+
throw _err("sigstore/bad-tlog-entry", "the Rekor entry's artifact hash is not a lowercase hex digest of its own algorithm");
|
|
808
|
+
}
|
|
809
|
+
return { algorithm: data.hash.algorithm, node: alg.node, value: data.hash.value };
|
|
810
|
+
}
|
|
811
|
+
|
|
320
812
|
function _verifyRekor(te, envelope, rekorKeys, leafDer) {
|
|
813
|
+
return _verifyRekorEntry(te, rekorKeys, leafDer, "dsseEnvelope", function (body) {
|
|
814
|
+
_bindDsseEntry(body, envelope, leafDer);
|
|
815
|
+
return null;
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/** @internal The message-signature route through the same entry verification. Only the binding
|
|
820
|
+
* differs, so the inclusion proof, the checkpoint and the SET are checked in one place for both arms
|
|
821
|
+
* rather than in a copy per arm. */
|
|
822
|
+
function _verifyRekorMessage(te, ms, rekorKeys, leafDer) {
|
|
823
|
+
return _verifyRekorEntry(te, rekorKeys, leafDer, "messageSignature", function (body) {
|
|
824
|
+
return _bindHashedRekordEntry(body, ms, leafDer);
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function _verifyRekorEntry(te, rekorKeys, leafDer, contentType, bind) {
|
|
321
829
|
var ip = te.inclusionProof;
|
|
322
|
-
if (!ip || typeof ip.rootHash !== "string" || !
|
|
830
|
+
if (!ip || typeof ip.rootHash !== "string" || !_isArray(ip.hashes)) {
|
|
323
831
|
throw _err("sigstore/bad-inclusion-proof", "the transparency-log entry has no inclusion proof");
|
|
324
832
|
}
|
|
325
|
-
var claimedSec =
|
|
326
|
-
var claimedMs =
|
|
327
|
-
|
|
833
|
+
var claimedSec = _numOf(te.integratedTime);
|
|
834
|
+
var claimedMs = _isFiniteNumber(claimedSec) ? C.TIME.seconds(claimedSec) : NaN;
|
|
835
|
+
/** @internal The canonicalized body is read ONCE and every check below runs on that one copy: the
|
|
836
|
+
* signature and certificate it binds, the leaf hash the inclusion proof folds, and the bytes the
|
|
837
|
+
* signed entry timestamp covers. A transparency-log entry is a caller-supplied object, so reading
|
|
838
|
+
* the field again would let one body be bound while a different one is proven, and the verdict
|
|
839
|
+
* would then attest an entry that never recorded what was verified. */
|
|
840
|
+
var canonB64 = te.canonicalizedBody;
|
|
841
|
+
if (typeof canonB64 !== "string") {
|
|
842
|
+
throw _err("sigstore/bad-tlog-entry", "the transparency-log entry has no canonicalizedBody");
|
|
843
|
+
}
|
|
844
|
+
var canonBytes = _b64(canonB64, "canonicalizedBody");
|
|
845
|
+
var body = _entryBody(canonBytes);
|
|
846
|
+
_entryRow(body, contentType);
|
|
847
|
+
var artifactHash = bind(body);
|
|
328
848
|
var included;
|
|
329
849
|
try {
|
|
330
850
|
included = merkle.verifyInclusion({
|
|
331
851
|
leafIndex: BigInt(ip.logIndex),
|
|
332
852
|
treeSize: BigInt(ip.treeSize),
|
|
333
|
-
leafHash: merkle.leafHash(
|
|
853
|
+
leafHash: merkle.leafHash(canonBytes),
|
|
334
854
|
proof: ip.hashes.map(function (h, i) { return _b64(h, "inclusionProof.hashes[" + i + "]"); }),
|
|
335
855
|
rootHash: _b64(ip.rootHash, "inclusionProof.rootHash"),
|
|
336
856
|
});
|
|
@@ -340,12 +860,16 @@ function _verifyRekor(te, envelope, rekorKeys, leafDer) {
|
|
|
340
860
|
if (!included) throw _err("sigstore/inclusion-proof-mismatch", "the inclusion proof does not reconstruct the tree root");
|
|
341
861
|
var checkpointOk = ip.checkpoint && typeof ip.checkpoint.envelope === "string" && _verifyCheckpoint(ip.checkpoint.envelope, ip, rekorKeys, claimedMs);
|
|
342
862
|
if (!checkpointOk) throw _err("sigstore/unsigned-root", "the Rekor checkpoint (the signed tree root) did not verify under the caller Rekor key -- the inclusion-proof root is not attested");
|
|
343
|
-
var setOk = _verifySet(te, rekorKeys, claimedMs);
|
|
863
|
+
var setOk = _verifySet(te, rekorKeys, claimedMs, canonB64);
|
|
344
864
|
if (!setOk) throw _err("sigstore/unattested-time", "the Rekor SET (the signed source of integratedTime) did not verify -- the log time is not attested and cannot date the Fulcio certificate");
|
|
345
|
-
var t =
|
|
346
|
-
if (!
|
|
347
|
-
var li =
|
|
348
|
-
return {
|
|
865
|
+
var t = _numOf(te.integratedTime);
|
|
866
|
+
if (!_isFiniteNumber(t) || t < 0) throw _err("sigstore/bad-tlog-entry", "the transparency-log entry has a malformed integratedTime");
|
|
867
|
+
var li = _numOf(te.logIndex);
|
|
868
|
+
return {
|
|
869
|
+
integratedTime: t, logIndex: _isFiniteNumber(li) ? li : null,
|
|
870
|
+
logId: te.logId && te.logId.keyId ? _b64(te.logId.keyId, "logId.keyId").toString("hex") : null,
|
|
871
|
+
artifactHash: artifactHash,
|
|
872
|
+
};
|
|
349
873
|
}
|
|
350
874
|
|
|
351
875
|
|
|
@@ -353,8 +877,8 @@ function _dn(cert, which) { return cert[which] && cert[which].dn; }
|
|
|
353
877
|
|
|
354
878
|
function _normRoots(fulcioRoots) {
|
|
355
879
|
return fulcioRoots.map(function (r, i) {
|
|
356
|
-
var der =
|
|
357
|
-
if (!
|
|
880
|
+
var der = _isBuffer(r) ? r : (r && (r.der || r.rawBytes));
|
|
881
|
+
if (!_isBuffer(der)) throw _err("sigstore/bad-input", "fulcioRoots[" + i + "] must be a DER Buffer or { der, validFor }");
|
|
358
882
|
return { cert: _parseCert(der, "Fulcio CA root [" + i + "]"), validFor: (r && r.validFor) || null };
|
|
359
883
|
});
|
|
360
884
|
}
|
|
@@ -394,12 +918,12 @@ var OID_SCT_LIST = oid.byName("signedCertificateTimestampList");
|
|
|
394
918
|
* operator builds both from the one trusted-root document. An empty array is refused rather than read
|
|
395
919
|
* as "no logs to check against", which would turn an opted-in check into a silent pass. */
|
|
396
920
|
function _normCtLogs(ctLogs) {
|
|
397
|
-
if (!
|
|
921
|
+
if (!_isArray(ctLogs)) throw _err("sigstore/bad-input", "ctLogs must be an array of { keyId, spki, validFor? }");
|
|
398
922
|
if (ctLogs.length === 0) throw _err("sigstore/bad-input", "ctLogs is empty; omit the option to skip the certificate-transparency check rather than supplying no logs");
|
|
399
923
|
return ctLogs.map(function (l, i) {
|
|
400
924
|
if (l === null || typeof l !== "object") throw _err("sigstore/bad-input", "ctLogs[" + i + "] must be { keyId, spki, validFor? }");
|
|
401
|
-
if (!
|
|
402
|
-
if (!
|
|
925
|
+
if (!_isBuffer(l.keyId)) throw _err("sigstore/bad-input", "ctLogs[" + i + "].keyId must be the log id as a Buffer");
|
|
926
|
+
if (!_isBuffer(l.spki)) throw _err("sigstore/bad-input", "ctLogs[" + i + "].spki must be the log's SubjectPublicKeyInfo DER");
|
|
403
927
|
return { keyId: l.keyId, spki: l.spki, validFor: l.validFor || null };
|
|
404
928
|
});
|
|
405
929
|
}
|
|
@@ -456,7 +980,7 @@ function _sctVerifies(entry, sct, spki) {
|
|
|
456
980
|
function _sctMs(sct) { return Number(sct.timestamp || 0); }
|
|
457
981
|
|
|
458
982
|
function _ctLogFor(logs, logIdBytes, timeMs) {
|
|
459
|
-
var want =
|
|
983
|
+
var want = _isBuffer(logIdBytes) ? logIdBytes : Buffer.from(logIdBytes || []);
|
|
460
984
|
for (var i = 0; i < logs.length; i++) {
|
|
461
985
|
if (logs[i].keyId.equals(want) && _inWindow(timeMs, logs[i].validFor)) return logs[i];
|
|
462
986
|
}
|
|
@@ -541,20 +1065,35 @@ function _sanValue(ext) {
|
|
|
541
1065
|
return names[0] || null;
|
|
542
1066
|
}
|
|
543
1067
|
function _fulcioExtValue(ext, leafArc) {
|
|
544
|
-
if (!
|
|
1068
|
+
if (!_isBuffer(ext.value)) throw _err("sigstore/bad-certificate", "Fulcio extension " + ext.oid + " has no value");
|
|
545
1069
|
if (leafArc >= 1 && leafArc <= 6) return ext.value.toString("utf8");
|
|
546
1070
|
return asn1.read.string(asn1.decode(ext.value));
|
|
547
1071
|
}
|
|
548
1072
|
|
|
549
1073
|
var IDENTITY_FIELDS = ["san", "issuer", "sourceRepositoryURI"];
|
|
550
1074
|
var IDENTITY_KEYS = Object.assign(Object.create(null), { san: 1, issuer: 1, sourceRepositoryURI: 1 });
|
|
551
|
-
var _VERIFY_BUNDLE_OPTS = Object.assign(Object.create(null), { fulcioRoots: 1, rekorKeys: 1, ctLogs: 1, identity: 1, predicateType: 1, time: 1 });
|
|
1075
|
+
var _VERIFY_BUNDLE_OPTS = Object.assign(Object.create(null), { fulcioRoots: 1, rekorKeys: 1, ctLogs: 1, identity: 1, predicateType: 1, time: 1, artifact: 1 });
|
|
1076
|
+
|
|
1077
|
+
/** @internal The bytes a message signature covers, taken once. There is no shape here in which a
|
|
1078
|
+
* caller hands over a precomputed digest: the arm's own field comment forbids using one as a
|
|
1079
|
+
* verification input, and the client specification asks a verifier to accept the artifact and compute
|
|
1080
|
+
* the digest itself so the two cannot be confused. So this takes a byte source and nothing else, and
|
|
1081
|
+
* a hex string or a digest-shaped object is refused at the door rather than hashed as if it were the
|
|
1082
|
+
* artifact. Snapshotted, because both the digest and the signature read these bytes and a view whose
|
|
1083
|
+
* backing store changes between the two would be verified in one form and reported in the other. */
|
|
1084
|
+
function _artifactBytes(v) {
|
|
1085
|
+
if (!guard.bytes.isByteSource(v)) {
|
|
1086
|
+
throw _err("sigstore/bad-input", "opts.artifact must be the artifact's bytes (a Buffer, TypedArray, DataView or ArrayBuffer); " +
|
|
1087
|
+
"a digest is not accepted in its place, since the signature covers the artifact itself");
|
|
1088
|
+
}
|
|
1089
|
+
return guard.bytes.snapshotSource(v, SigstoreError, "sigstore/bad-input", "opts.artifact");
|
|
1090
|
+
}
|
|
552
1091
|
|
|
553
1092
|
function _checkIdentity(id, policy) {
|
|
554
1093
|
// allow:registry-table-inherits-object-prototype -- the identityChecked record an operator reads by field name, whose three keys are the fixed IDENTITY_FIELDS rather than anything the bundle supplies
|
|
555
1094
|
var ran = { san: false, issuer: false, sourceRepositoryURI: false };
|
|
556
1095
|
if (policy === undefined || policy === null) return ran;
|
|
557
|
-
if (typeof policy !== "object" ||
|
|
1096
|
+
if (typeof policy !== "object" || _isArray(policy)) {
|
|
558
1097
|
throw _err("sigstore/bad-input", "opts.identity must be an object naming the identity fields to pin (" + IDENTITY_FIELDS.join(", ") + ")");
|
|
559
1098
|
}
|
|
560
1099
|
guard.identifier.assertKnownKeys(policy, IDENTITY_KEYS, _err, "sigstore/bad-input", "opts.identity has an unknown key ");
|
|
@@ -582,7 +1121,7 @@ function _statement(payload, payloadType, expectedPredicate) {
|
|
|
582
1121
|
}
|
|
583
1122
|
var st = _jsonParse(payload, "sigstore/bad-statement", "in-toto statement");
|
|
584
1123
|
if (!st || typeof st !== "object" || st._type !== "https://in-toto.io/Statement/v1") throw _err("sigstore/bad-statement", "the payload is not an in-toto Statement v1");
|
|
585
|
-
if (!
|
|
1124
|
+
if (!_isArray(st.subject) || !st.subject.length) throw _err("sigstore/bad-statement", "the in-toto statement has no subject");
|
|
586
1125
|
if (expectedPredicate && st.predicateType !== expectedPredicate) {
|
|
587
1126
|
throw _err("sigstore/predicate-mismatch", "the statement predicateType " + JSON.stringify(st.predicateType) + " does not match the expected " + JSON.stringify(expectedPredicate));
|
|
588
1127
|
}
|
|
@@ -638,12 +1177,28 @@ function _statement(payload, payloadType, expectedPredicate) {
|
|
|
638
1177
|
* while reading as a policy; so is an unrecognized field name, which would otherwise
|
|
639
1178
|
* pin nothing under a spelling the operator believes constrains the signer.
|
|
640
1179
|
*
|
|
1180
|
+
* A bundle carrying a `message_signature` is verified against the artifact itself,
|
|
1181
|
+
* which `opts.artifact` supplies as bytes and which this hashes. It is required for
|
|
1182
|
+
* that arm: the `messageDigest` the bundle carries is covered by no signature, and
|
|
1183
|
+
* the arm's own definition says a client must not use it to verify the signature, so
|
|
1184
|
+
* there is no shape here in which a caller hands over a digest instead. The artifact
|
|
1185
|
+
* is held to the hash the Rekor entry records, which the inclusion proof covers, and
|
|
1186
|
+
* then the signature is checked over its bytes. `artifactDigest` reports the digest
|
|
1187
|
+
* this computed and `digestAlgorithm` the algorithm the entry named;
|
|
1188
|
+
* `messageDigestChecked` says whether the bundle's own digest was there to agree with.
|
|
1189
|
+
* `opts.predicateType` is refused for that arm, and `opts.artifact` for a DSSE one,
|
|
1190
|
+
* rather than being read and ignored.
|
|
1191
|
+
*
|
|
1192
|
+
* The verdict has one shape for both arms. `contentType` names the arm, and the
|
|
1193
|
+
* fields the other arm has nothing to report are present and `null`.
|
|
1194
|
+
*
|
|
641
1195
|
* @opts
|
|
642
1196
|
* fulcioRoots: Array, // the Fulcio CA anchors: a DER Buffer or { der, validFor } each
|
|
643
1197
|
* rekorKeys: Array, // [{ keyId, spki, validFor? }] the Rekor log public keys
|
|
644
1198
|
* ctLogs: Array, // optional [{ keyId, spki, validFor? }] certificate-transparency logs; when given, the certificate's embedded receipt is checked
|
|
645
1199
|
* identity: object, // optional policy: { san, issuer, sourceRepositoryURI }; at least one required when present
|
|
646
|
-
* predicateType: string, // optional: require this in-toto predicateType (e.g. the SLSA URI)
|
|
1200
|
+
* predicateType: string, // optional: require this in-toto predicateType (e.g. the SLSA URI); dsse_envelope bundles only
|
|
1201
|
+
* artifact: BufferSource, // the bytes a message_signature covers; required for that arm, refused for a dsse_envelope
|
|
647
1202
|
* time: Date, // optional check-date override (default: the Rekor integratedTime)
|
|
648
1203
|
*
|
|
649
1204
|
* @example
|
|
@@ -654,59 +1209,155 @@ function _statement(payload, payloadType, expectedPredicate) {
|
|
|
654
1209
|
* out.subjects[0].digest; // { sha512: "..." } -- confirm against your tarball
|
|
655
1210
|
*/
|
|
656
1211
|
async function verifyBundle(bundle, opts) {
|
|
657
|
-
if (bundle === null || typeof bundle !== "object" && typeof bundle !== "string" && !
|
|
1212
|
+
if (bundle === null || typeof bundle !== "object" && typeof bundle !== "string" && !_isBuffer(bundle)) {
|
|
658
1213
|
throw new TypeError("verifyBundle: bundle must be an object, JSON string, or Buffer");
|
|
659
1214
|
}
|
|
660
1215
|
opts = opts || {};
|
|
661
1216
|
guard.identifier.assertKnownKeys(opts, _VERIFY_BUNDLE_OPTS, _err, "sigstore/bad-input", "pki.sigstore.verifyBundle has an unknown option ");
|
|
1217
|
+
/** @internal parseBundle takes the copy, so every check below runs on plain data that cannot change
|
|
1218
|
+
* under it, whichever field it reads and however many times. */
|
|
662
1219
|
var b = parseBundle(bundle);
|
|
663
1220
|
var vm = b.verificationMaterial;
|
|
1221
|
+
var contentType = _contentTypeOf(b);
|
|
1222
|
+
var isMsg = contentType === "messageSignature";
|
|
664
1223
|
var env = b.dsseEnvelope;
|
|
665
|
-
|
|
666
|
-
|
|
1224
|
+
/** @internal Every option is read once, into a snapshot the checks and the verdict both read. An
|
|
1225
|
+
* option reached through an accessor answers each read separately, so a second read is a second
|
|
1226
|
+
* answer: one that passed a refusal could be reported by the verdict as a different value, and a
|
|
1227
|
+
* verdict that names an option is naming the value the check ran on. */
|
|
1228
|
+
var o = guard.identifier.snapshotOptions(opts, _VERIFY_BUNDLE_OPTS);
|
|
1229
|
+
var rekorKeys = o.rekorKeys || [];
|
|
1230
|
+
var fulcioRoots = o.fulcioRoots || [];
|
|
1231
|
+
|
|
1232
|
+
/** @internal Each option belongs to one arm, and one supplied for the other is refused rather than
|
|
1233
|
+
* passed over. An ignored option reads as a check that ran: opts.artifact on a DSSE bundle names
|
|
1234
|
+
* bytes nothing here compares, and opts.predicateType on a message signature names a statement
|
|
1235
|
+
* there is none of. */
|
|
1236
|
+
if (!isMsg && o.artifact != null) {
|
|
1237
|
+
throw _err("sigstore/bad-input", "opts.artifact is only read for a message_signature bundle; this bundle carries a dsse_envelope, whose artifact binding is the statement's own subject digest");
|
|
1238
|
+
}
|
|
1239
|
+
if (isMsg && o.predicateType != null) {
|
|
1240
|
+
throw _err("sigstore/bad-input", "opts.predicateType is only read for a dsse_envelope bundle; a message_signature carries no statement to pin a predicate type on");
|
|
1241
|
+
}
|
|
1242
|
+
var artifact = null;
|
|
1243
|
+
if (isMsg) {
|
|
1244
|
+
if (o.artifact == null) {
|
|
1245
|
+
throw _err("sigstore/artifact-required", "verifying a message_signature bundle requires opts.artifact, the bytes the signature covers; the bundle's own messageDigest is unauthenticated and is not a substitute");
|
|
1246
|
+
}
|
|
1247
|
+
artifact = _artifactBytes(o.artifact);
|
|
1248
|
+
}
|
|
667
1249
|
|
|
668
1250
|
var leafDer = _leafCertDer(vm);
|
|
669
1251
|
var leaf = _parseCert(leafDer, "Fulcio leaf certificate");
|
|
670
|
-
var payload = _b64(env.payload, "dsseEnvelope.payload");
|
|
671
|
-
var preimage = pae(env.payloadType, payload);
|
|
672
|
-
var derSig = _b64(env.signatures[0].sig, "dsseEnvelope.signatures[0].sig");
|
|
673
1252
|
var leafKey = _pubFromSpki(leaf.subjectPublicKeyInfo.bytes, "Fulcio leaf");
|
|
674
|
-
|
|
1253
|
+
/** @internal Read once, validated once, used from the validated copy everywhere below. */
|
|
1254
|
+
var msParsed = isMsg ? _parseMessageSignature(b.messageSignature) : null;
|
|
1255
|
+
var payload = null, derSig;
|
|
1256
|
+
if (isMsg) {
|
|
1257
|
+
derSig = _b64(msParsed.signature, "messageSignature.signature");
|
|
1258
|
+
} else {
|
|
1259
|
+
payload = _b64(env.payload, "dsseEnvelope.payload");
|
|
1260
|
+
derSig = _b64(env.signatures[0].sig, "dsseEnvelope.signatures[0].sig");
|
|
1261
|
+
if (!_rawVerify(leafKey, pae(env.payloadType, payload), derSig)) throw _err("sigstore/dsse-verify-failed", "the DSSE signature does not verify under the Fulcio leaf key");
|
|
1262
|
+
}
|
|
675
1263
|
|
|
676
|
-
var tlogs =
|
|
1264
|
+
var tlogs = _isArray(vm.tlogEntries) ? vm.tlogEntries : [];
|
|
677
1265
|
if (!tlogs.length) throw _err("sigstore/bad-bundle", "a keyless bundle requires at least one transparency-log entry");
|
|
678
|
-
|
|
1266
|
+
/** @internal Selecting an entry means finding one that passes EVERY check deciding WHICH entry it
|
|
1267
|
+
* is: that it binds to this signature and certificate, that it records this artifact, and that the
|
|
1268
|
+
* certificate chain and the embedded timestamps hold at the instant it attests. Any of those
|
|
1269
|
+
* failing is a reason to try the next entry rather than to refuse the bundle, because a log can
|
|
1270
|
+
* hold several entries for one signature. Each check left after the loop refused a bundle whose
|
|
1271
|
+
* later entry would have passed it, once for the artifact hash and again for the chain instant.
|
|
1272
|
+
* Passing over an entry is not accepting a weaker one: the selected entry has passed all of them,
|
|
1273
|
+
* and when none does the last failure is what the caller is told. */
|
|
1274
|
+
var haveCallerTime = guard.time.isDate(o.time);
|
|
1275
|
+
var callerTime = haveCallerTime ? guard.time.instantOf(o.time) : null;
|
|
1276
|
+
var chainDers = _chainDers(vm);
|
|
1277
|
+
/** @internal A caller-pinned instant is the same for every candidate, so the chain it decides is
|
|
1278
|
+
* computed on the first candidate and reused rather than rebuilt per entry. Without one the instant
|
|
1279
|
+
* comes from the entry and every candidate is its own question. The timestamps are not cached
|
|
1280
|
+
* beside it: a candidate that reaches them and passes is selected immediately, so no later
|
|
1281
|
+
* candidate could read the result. */
|
|
1282
|
+
var pinnedChain = null;
|
|
1283
|
+
var rekor = null, lastErr = null, artifactComputed = null, sctResult = null;
|
|
679
1284
|
for (var ti = 0; ti < tlogs.length; ti++) {
|
|
680
1285
|
if (!tlogs[ti] || typeof tlogs[ti] !== "object") { lastErr = _err("sigstore/bad-bundle", "a transparency-log entry is not an object"); continue; }
|
|
681
|
-
try {
|
|
682
|
-
|
|
1286
|
+
try {
|
|
1287
|
+
var candidate = isMsg ? _verifyRekorMessage(tlogs[ti], msParsed, rekorKeys, leafDer)
|
|
1288
|
+
: _verifyRekor(tlogs[ti], env, rekorKeys, leafDer);
|
|
1289
|
+
var candidateDigest = null;
|
|
1290
|
+
if (isMsg) {
|
|
1291
|
+
candidateDigest = nodeCrypto.createHash(candidate.artifactHash.node).update(artifact).digest();
|
|
1292
|
+
if (!guard.crypto.constantTimeEqual(candidateDigest, Buffer.from(candidate.artifactHash.value, "hex"))) {
|
|
1293
|
+
throw _err("sigstore/artifact-mismatch", "the artifact does not match the one the transparency-log entry records; its " +
|
|
1294
|
+
candidate.artifactHash.algorithm + " digest is " + candidateDigest.toString("hex") + " and the entry names " + candidate.artifactHash.value);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
var candidateTime = haveCallerTime ? callerTime : C.TIME.seconds(candidate.integratedTime);
|
|
1298
|
+
var candidateChain = pinnedChain !== null ? pinnedChain
|
|
1299
|
+
: await _verifyChain(leaf, chainDers, fulcioRoots, candidateTime);
|
|
1300
|
+
if (haveCallerTime) pinnedChain = candidateChain;
|
|
1301
|
+
/** @internal With no caller instant the reference is the authenticated log-entry time, whose
|
|
1302
|
+
* whole second is inclusive: a certificate logged in the same second as its entry is not dated
|
|
1303
|
+
* after it, and nothing beyond that second is accepted. */
|
|
1304
|
+
var candidateBound = haveCallerTime ? candidateTime : C.TIME.seconds(candidate.integratedTime) + 999;
|
|
1305
|
+
var candidateScts = await _verifyEmbeddedScts(leaf, leafDer, candidateChain.issuer, o.ctLogs, candidateTime, candidateBound);
|
|
1306
|
+
rekor = candidate;
|
|
1307
|
+
artifactComputed = candidateDigest;
|
|
1308
|
+
sctResult = candidateScts;
|
|
1309
|
+
break;
|
|
1310
|
+
} catch (e) { lastErr = e; }
|
|
683
1311
|
}
|
|
684
1312
|
if (rekor === null) throw lastErr;
|
|
685
1313
|
var integratedTime = rekor.integratedTime;
|
|
686
1314
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
var
|
|
693
|
-
|
|
1315
|
+
/** @internal The order the client specification states: the entry is verified first, because it is
|
|
1316
|
+
* what supplies both the time the certificate is checked at and the authenticated statement of what
|
|
1317
|
+
* was signed. The artifact is held to THAT hash rather than to the digest the bundle carries, which
|
|
1318
|
+
* no signature covers, and that comparison is what selected the entry above. Only after the
|
|
1319
|
+
* artifact is the one the log recorded is the signature checked over its bytes. */
|
|
1320
|
+
var artifactDigest = null, digestAlgorithm = null, messageDigestChecked = false;
|
|
1321
|
+
if (isMsg) {
|
|
1322
|
+
digestAlgorithm = rekor.artifactHash.algorithm;
|
|
1323
|
+
var computed = artifactComputed;
|
|
1324
|
+
artifactDigest = computed.toString("hex");
|
|
1325
|
+
if (msParsed.digest !== null) {
|
|
1326
|
+
var againstClaim = msParsed.digestAlg.node === rekor.artifactHash.node ? computed
|
|
1327
|
+
: nodeCrypto.createHash(msParsed.digestAlg.node).update(artifact).digest();
|
|
1328
|
+
if (!guard.crypto.constantTimeEqual(againstClaim, msParsed.digest)) {
|
|
1329
|
+
throw _err("sigstore/artifact-mismatch", "the artifact does not match the messageDigest the bundle carries");
|
|
1330
|
+
}
|
|
1331
|
+
messageDigestChecked = true;
|
|
1332
|
+
}
|
|
1333
|
+
if (!_rawVerify(leafKey, artifact, derSig)) {
|
|
1334
|
+
throw _err("sigstore/signature-verify-failed", "the message signature does not verify over the artifact under the Fulcio leaf key");
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
694
1338
|
var identity = _identity(leaf);
|
|
695
|
-
var identityChecked = _checkIdentity(identity,
|
|
1339
|
+
var identityChecked = _checkIdentity(identity, o.identity);
|
|
696
1340
|
|
|
697
|
-
|
|
1341
|
+
/** @internal One verdict shape for both arms, so a caller reads the same fields whichever a bundle
|
|
1342
|
+
* carries and a missing key never has to be told apart from a false one. The fields the other arm
|
|
1343
|
+
* has nothing to report are present and null. */
|
|
1344
|
+
var st = isMsg ? null : _statement(payload, env.payloadType, o.predicateType);
|
|
698
1345
|
|
|
699
1346
|
return guard.verdict.of({
|
|
700
1347
|
valid: true,
|
|
701
1348
|
verified: true,
|
|
1349
|
+
contentType: contentType,
|
|
702
1350
|
payload: payload,
|
|
703
1351
|
statement: st,
|
|
704
|
-
subjects: st.subject,
|
|
705
|
-
predicateType: st.predicateType,
|
|
706
|
-
predicate: st.predicate,
|
|
1352
|
+
subjects: st === null ? null : st.subject,
|
|
1353
|
+
predicateType: st === null ? null : st.predicateType,
|
|
1354
|
+
predicate: st === null ? null : st.predicate,
|
|
1355
|
+
artifactDigest: artifactDigest,
|
|
1356
|
+
digestAlgorithm: digestAlgorithm,
|
|
1357
|
+
messageDigestChecked: messageDigestChecked,
|
|
707
1358
|
identity: identity,
|
|
708
1359
|
identityChecked: identityChecked,
|
|
709
|
-
predicateTypeChecked:
|
|
1360
|
+
predicateTypeChecked: o.predicateType != null,
|
|
710
1361
|
sctChecked: sctResult.checked,
|
|
711
1362
|
validScts: sctResult.validScts,
|
|
712
1363
|
integratedTime: integratedTime,
|