@blamejs/pki 0.6.55 → 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/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 over a DSSE-wrapped
12
- * in-toto SLSA provenance attestation, with a Rekor transparency-log inclusion
13
- * proof. `verifyBundle` composes five fail-closed legs against caller-supplied
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 DSSE signature over its PAE preimage under the Fulcio leaf key;
16
- * the Fulcio certificate chain, validated as of the Rekor log time (the cert is
17
- * ephemeral, ~10 minutes); the Rekor inclusion proof folded to a Rekor-signed
18
- * tree root; the log entry binding to this exact signature; and the in-toto
19
- * subject digest the caller confirms against the published artifact. Verify-only
20
- * and offline: every input is in the bundle or a caller argument. Reuses the
21
- * shipped X.509 parser, RFC 5280 path validator, RFC 9162 Merkle verifier, and
22
- * native crypto engine; the net-new codecs are the DSSE PAE byte-builder and a
23
- * fail-closed JSON bundle reader.
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
  */
@@ -48,10 +51,93 @@ function _err(code, message, cause) { return new SigstoreError(code, message, ca
48
51
  * properties only, so a polluted prototype cannot conjure an arm the bundle does not carry. */
49
52
  function _armPresent(obj, name) {
50
53
  return !!obj && typeof obj === "object" &&
51
- Object.prototype.hasOwnProperty.call(obj, name) && obj[name] !== undefined && obj[name] !== null;
54
+ _hasOwn(obj, name) && obj[name] !== undefined && obj[name] !== null;
52
55
  }
53
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
+
54
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);
55
141
  var SP = Buffer.from(" ", "ascii");
56
142
  var DSSEV1 = Buffer.from("DSSEv1", "ascii");
57
143
  var _EM_DASH = String.fromCharCode(0x2014);
@@ -89,12 +175,12 @@ var _BUNDLE_MEDIA_TYPES = Object.assign(Object.create(null), {
89
175
  "application/vnd.dev.sigstore.bundle+json;version=0.3": 1
90
176
  });
91
177
  function _isBundleMediaType(mt) {
92
- return typeof mt === "string" && Object.prototype.hasOwnProperty.call(_BUNDLE_MEDIA_TYPES, mt);
178
+ return typeof mt === "string" && _hasOwn(_BUNDLE_MEDIA_TYPES, mt);
93
179
  }
94
180
 
95
181
  function _jsonParse(input, code, label) {
96
182
  return guard.json.parse(input, _err, {
97
- maxBytes: JSON_MAX, maxDepth: 64,
183
+ maxBytes: JSON_MAX, maxDepth: BUNDLE_MAX_DEPTH,
98
184
  badJson: code, tooDeep: code, duplicateMember: code, tooLarge: code, badInput: code, label: label,
99
185
  });
100
186
  }
@@ -121,7 +207,7 @@ function _jsonParse(input, code, label) {
121
207
  function pae(payloadType, payloadBytes) {
122
208
  if (typeof payloadType !== "string") throw new TypeError("pae: payloadType must be a string");
123
209
  var type = Buffer.from(payloadType, "utf8");
124
- var body = Buffer.isBuffer(payloadBytes) ? payloadBytes : Buffer.from(payloadBytes || []);
210
+ var body = _isBuffer(payloadBytes) ? payloadBytes : _bufferFrom(payloadBytes || []);
125
211
  return Buffer.concat([
126
212
  DSSEV1, SP,
127
213
  Buffer.from(String(type.length), "ascii"), SP, type, SP,
@@ -154,7 +240,14 @@ function _b64(s, label) {
154
240
  * Buffer) fail-closed: a non-object, malformed JSON, an oversize input, an
155
241
  * unknown `mediaType`, or a missing required member throws a typed
156
242
  * `sigstore/bad-bundle` / `sigstore/bad-bundle-version`. Returns the validated
157
- * bundle object (structure only, no cryptographic verification).
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.
158
251
  *
159
252
  * @example
160
253
  * // requires: `bundle` -- a Sigstore bundle as cosign or npm provenance emits it
@@ -164,14 +257,18 @@ function _b64(s, label) {
164
257
  */
165
258
  function parseBundle(input) {
166
259
  var obj;
167
- if (Buffer.isBuffer(input) || typeof input === "string") {
260
+ if (_isBuffer(input) || typeof input === "string") {
168
261
  obj = _jsonParse(input, "sigstore/bad-bundle", "bundle");
169
262
  } else if (input && typeof input === "object") {
170
- obj = input;
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);
171
268
  } else {
172
269
  throw _err("sigstore/bad-bundle", "bundle must be a JSON object, string, or Buffer");
173
270
  }
174
- if (!obj || typeof obj !== "object" || Array.isArray(obj)) throw _err("sigstore/bad-bundle", "bundle must be a JSON object");
271
+ if (!obj || typeof obj !== "object" || _isArray(obj)) throw _err("sigstore/bad-bundle", "bundle must be a JSON object");
175
272
  var mt = obj.mediaType;
176
273
  if (!_isBundleMediaType(mt)) {
177
274
  throw _err("sigstore/bad-bundle-version", "unsupported or unknown bundle media type: " + (mt === undefined ? "(none)" : guard.text.showValue(mt)));
@@ -179,37 +276,292 @@ function parseBundle(input) {
179
276
  if (!obj.verificationMaterial || typeof obj.verificationMaterial !== "object") {
180
277
  throw _err("sigstore/bad-bundle", "bundle is missing verificationMaterial");
181
278
  }
182
- /** @internal The verification material's certificate is a oneof of the same kind. Both arms set
183
- * draws the leaf from one and the chain from the other, so the certificate whose key is checked and
184
- * the certificates it is checked through come from different statements about the same signer. */
185
- var vmArms = 0;
186
- if (_armPresent(obj.verificationMaterial, "certificate")) vmArms++;
187
- if (_armPresent(obj.verificationMaterial, "x509CertificateChain")) vmArms++;
188
- if (_armPresent(obj.verificationMaterial, "publicKey")) vmArms++;
189
- if (vmArms > 1) {
190
- 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");
191
- }
192
- /** @internal The bundle's content is a protobuf oneof, so a bundle setting both arms is malformed
193
- * and is refused rather than read as whichever arm this build happens to support: verifying the
194
- * envelope and passing over the other arm reports a verdict that says nothing about content the
195
- * bundle carries. The two refusals are separate because a caller can act on one and not the other. */
196
- if (_armPresent(obj, "messageSignature") && _armPresent(obj, "dsseEnvelope")) {
197
- 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");
198
- }
279
+ _assertMaterialOneOf(obj.verificationMaterial);
280
+ _assertContentOneOf(obj);
199
281
  if (_armPresent(obj, "messageSignature")) {
200
- throw _err("sigstore/unsupported-content", "a message_signature bundle is not supported (only dsse_envelope)");
282
+ _parseMessageSignature(obj.messageSignature);
283
+ return obj;
201
284
  }
202
285
  if (!obj.dsseEnvelope || typeof obj.dsseEnvelope !== "object") {
203
286
  throw _err("sigstore/bad-bundle", "bundle is missing a dsseEnvelope");
204
287
  }
205
288
  var d = obj.dsseEnvelope;
206
- if (typeof d.payload !== "string" || typeof d.payloadType !== "string" || !Array.isArray(d.signatures) || !d.signatures.length ||
289
+ if (typeof d.payload !== "string" || typeof d.payloadType !== "string" || !_isArray(d.signatures) || !d.signatures.length ||
207
290
  !d.signatures[0] || typeof d.signatures[0] !== "object" || typeof d.signatures[0].sig !== "string") {
208
291
  throw _err("sigstore/bad-dsse", "the DSSE envelope is missing a required field (payload / payloadType / signatures[].sig)");
209
292
  }
210
293
  return obj;
211
294
  }
212
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
+
213
565
 
214
566
  function _certBytes(c, label) {
215
567
  if (!c || typeof c !== "object" || typeof c.rawBytes !== "string") throw _err("sigstore/bad-bundle", label + " is not a { rawBytes } certificate");
@@ -217,13 +569,13 @@ function _certBytes(c, label) {
217
569
  }
218
570
  function _leafCertDer(vm) {
219
571
  if (vm.certificate) return _certBytes(vm.certificate, "verificationMaterial.certificate");
220
- if (vm.x509CertificateChain && Array.isArray(vm.x509CertificateChain.certificates) && vm.x509CertificateChain.certificates.length) {
572
+ if (vm.x509CertificateChain && _isArray(vm.x509CertificateChain.certificates) && vm.x509CertificateChain.certificates.length) {
221
573
  return _certBytes(vm.x509CertificateChain.certificates[0], "verificationMaterial.x509CertificateChain[0]");
222
574
  }
223
575
  throw _err("sigstore/bad-bundle", "bundle has no Fulcio certificate (public_key bundles are not supported)");
224
576
  }
225
577
  function _chainDers(vm) {
226
- if (vm.x509CertificateChain && Array.isArray(vm.x509CertificateChain.certificates)) {
578
+ if (vm.x509CertificateChain && _isArray(vm.x509CertificateChain.certificates)) {
227
579
  return vm.x509CertificateChain.certificates.map(function (c, i) { return _certBytes(c, "x509CertificateChain[" + i + "]"); });
228
580
  }
229
581
  return [_leafCertDer(vm)];
@@ -259,7 +611,7 @@ function _sha256(buf) { return nodeCrypto.createHash("sha256").update(buf).diges
259
611
 
260
612
  function _toMs(x) {
261
613
  if (x == null) return null;
262
- if (typeof x === "number") return Number.isFinite(x) ? x : null;
614
+ if (typeof x === "number") return _isFiniteNumber(x) ? x : null;
263
615
  if (guard.time.isDate(x)) { var d = guard.time.instantOf(x); return isNaN(d) ? null : d; }
264
616
  if (typeof x === "string") { var t = Date.parse(x); return isNaN(t) ? null : t; }
265
617
  return null;
@@ -303,29 +655,64 @@ function _verifyCheckpoint(envelope, ip, rekorKeys, timeMs) {
303
655
  return false;
304
656
  }
305
657
 
306
- function _verifySet(te, rekorKeys, timeMs) {
658
+ function _verifySet(te, rekorKeys, timeMs, canonBody) {
307
659
  var promise = te.inclusionPromise;
308
660
  if (!promise || typeof promise.signedEntryTimestamp !== "string") return false;
309
661
  var keyId = te.logId && te.logId.keyId ? _b64(te.logId.keyId, "logId.keyId") : null;
310
662
  var k = _rekorKey(rekorKeys, keyId, null, timeMs);
311
663
  if (!k) return false;
312
- var canonical = JSON.stringify({
313
- body: te.canonicalizedBody,
314
- integratedTime: Number(te.integratedTime),
664
+ var canonical = _jsonStringify({
665
+ body: canonBody,
666
+ integratedTime: _numOf(te.integratedTime),
315
667
  logID: keyId.toString("hex"),
316
- logIndex: Number(te.logIndex),
668
+ logIndex: _numOf(te.logIndex),
317
669
  });
318
670
  var sig = _b64(promise.signedEntryTimestamp, "signedEntryTimestamp");
319
671
  return _rawVerify(_pubFromSpki(k.spki, "Rekor log"), Buffer.from(canonical, "utf8"), sig);
320
672
  }
321
673
 
322
- function _bindEntry(te, envelope, leafDer) {
323
- var body = _jsonParse(_b64(te.canonicalizedBody, "canonicalizedBody"), "sigstore/bad-tlog-entry", "Rekor entry body");
324
- if (!body || typeof body !== "object" || Array.isArray(body)) throw _err("sigstore/bad-tlog-entry", "the Rekor entry body is not a JSON object");
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) {
325
712
  var spec = body.spec;
326
- if (body.kind !== "dsse" || !spec || typeof spec !== "object" || !Array.isArray(spec.signatures) || !spec.signatures.length ||
713
+ if (!spec || typeof spec !== "object" || !_isArray(spec.signatures) || !spec.signatures.length ||
327
714
  !spec.signatures[0] || typeof spec.signatures[0] !== "object" || typeof spec.signatures[0].signature !== "string") {
328
- throw _err("sigstore/unsupported-content", "unsupported or malformed Rekor entry: " + body.kind);
715
+ throw _err("sigstore/bad-tlog-entry", "the Rekor dsse entry is missing its signature");
329
716
  }
330
717
  var envSig = _b64(envelope.signatures[0].sig, "dsseEnvelope.signatures[0].sig");
331
718
  var bodySig = _b64(spec.signatures[0].signature, "Rekor entry signature");
@@ -343,20 +730,127 @@ function _bindEntry(te, envelope, leafDer) {
343
730
  if (!vDer.equals(leafDer)) throw _err("sigstore/entry-mismatch", "the Rekor entry verifier certificate does not match the bundle leaf certificate");
344
731
  }
345
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
+
346
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) {
347
829
  var ip = te.inclusionProof;
348
- if (!ip || typeof ip.rootHash !== "string" || !Array.isArray(ip.hashes)) {
830
+ if (!ip || typeof ip.rootHash !== "string" || !_isArray(ip.hashes)) {
349
831
  throw _err("sigstore/bad-inclusion-proof", "the transparency-log entry has no inclusion proof");
350
832
  }
351
- var claimedSec = Number(te.integratedTime);
352
- var claimedMs = Number.isFinite(claimedSec) ? C.TIME.seconds(claimedSec) : NaN;
353
- _bindEntry(te, envelope, leafDer);
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);
354
848
  var included;
355
849
  try {
356
850
  included = merkle.verifyInclusion({
357
851
  leafIndex: BigInt(ip.logIndex),
358
852
  treeSize: BigInt(ip.treeSize),
359
- leafHash: merkle.leafHash(_b64(te.canonicalizedBody, "canonicalizedBody")),
853
+ leafHash: merkle.leafHash(canonBytes),
360
854
  proof: ip.hashes.map(function (h, i) { return _b64(h, "inclusionProof.hashes[" + i + "]"); }),
361
855
  rootHash: _b64(ip.rootHash, "inclusionProof.rootHash"),
362
856
  });
@@ -366,12 +860,16 @@ function _verifyRekor(te, envelope, rekorKeys, leafDer) {
366
860
  if (!included) throw _err("sigstore/inclusion-proof-mismatch", "the inclusion proof does not reconstruct the tree root");
367
861
  var checkpointOk = ip.checkpoint && typeof ip.checkpoint.envelope === "string" && _verifyCheckpoint(ip.checkpoint.envelope, ip, rekorKeys, claimedMs);
368
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");
369
- var setOk = _verifySet(te, rekorKeys, claimedMs);
863
+ var setOk = _verifySet(te, rekorKeys, claimedMs, canonB64);
370
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");
371
- var t = Number(te.integratedTime);
372
- if (!Number.isFinite(t) || t < 0) throw _err("sigstore/bad-tlog-entry", "the transparency-log entry has a malformed integratedTime");
373
- var li = Number(te.logIndex);
374
- return { integratedTime: t, logIndex: Number.isFinite(li) ? li : null, logId: te.logId && te.logId.keyId ? _b64(te.logId.keyId, "logId.keyId").toString("hex") : null };
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
+ };
375
873
  }
376
874
 
377
875
 
@@ -379,8 +877,8 @@ function _dn(cert, which) { return cert[which] && cert[which].dn; }
379
877
 
380
878
  function _normRoots(fulcioRoots) {
381
879
  return fulcioRoots.map(function (r, i) {
382
- var der = Buffer.isBuffer(r) ? r : (r && (r.der || r.rawBytes));
383
- if (!Buffer.isBuffer(der)) throw _err("sigstore/bad-input", "fulcioRoots[" + i + "] must be a DER Buffer or { der, validFor }");
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 }");
384
882
  return { cert: _parseCert(der, "Fulcio CA root [" + i + "]"), validFor: (r && r.validFor) || null };
385
883
  });
386
884
  }
@@ -420,12 +918,12 @@ var OID_SCT_LIST = oid.byName("signedCertificateTimestampList");
420
918
  * operator builds both from the one trusted-root document. An empty array is refused rather than read
421
919
  * as "no logs to check against", which would turn an opted-in check into a silent pass. */
422
920
  function _normCtLogs(ctLogs) {
423
- if (!Array.isArray(ctLogs)) throw _err("sigstore/bad-input", "ctLogs must be an array of { keyId, spki, validFor? }");
921
+ if (!_isArray(ctLogs)) throw _err("sigstore/bad-input", "ctLogs must be an array of { keyId, spki, validFor? }");
424
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");
425
923
  return ctLogs.map(function (l, i) {
426
924
  if (l === null || typeof l !== "object") throw _err("sigstore/bad-input", "ctLogs[" + i + "] must be { keyId, spki, validFor? }");
427
- if (!Buffer.isBuffer(l.keyId)) throw _err("sigstore/bad-input", "ctLogs[" + i + "].keyId must be the log id as a Buffer");
428
- if (!Buffer.isBuffer(l.spki)) throw _err("sigstore/bad-input", "ctLogs[" + i + "].spki must be the log's SubjectPublicKeyInfo DER");
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");
429
927
  return { keyId: l.keyId, spki: l.spki, validFor: l.validFor || null };
430
928
  });
431
929
  }
@@ -482,7 +980,7 @@ function _sctVerifies(entry, sct, spki) {
482
980
  function _sctMs(sct) { return Number(sct.timestamp || 0); }
483
981
 
484
982
  function _ctLogFor(logs, logIdBytes, timeMs) {
485
- var want = Buffer.isBuffer(logIdBytes) ? logIdBytes : Buffer.from(logIdBytes || []);
983
+ var want = _isBuffer(logIdBytes) ? logIdBytes : Buffer.from(logIdBytes || []);
486
984
  for (var i = 0; i < logs.length; i++) {
487
985
  if (logs[i].keyId.equals(want) && _inWindow(timeMs, logs[i].validFor)) return logs[i];
488
986
  }
@@ -567,20 +1065,35 @@ function _sanValue(ext) {
567
1065
  return names[0] || null;
568
1066
  }
569
1067
  function _fulcioExtValue(ext, leafArc) {
570
- if (!Buffer.isBuffer(ext.value)) throw _err("sigstore/bad-certificate", "Fulcio extension " + ext.oid + " has no value");
1068
+ if (!_isBuffer(ext.value)) throw _err("sigstore/bad-certificate", "Fulcio extension " + ext.oid + " has no value");
571
1069
  if (leafArc >= 1 && leafArc <= 6) return ext.value.toString("utf8");
572
1070
  return asn1.read.string(asn1.decode(ext.value));
573
1071
  }
574
1072
 
575
1073
  var IDENTITY_FIELDS = ["san", "issuer", "sourceRepositoryURI"];
576
1074
  var IDENTITY_KEYS = Object.assign(Object.create(null), { san: 1, issuer: 1, sourceRepositoryURI: 1 });
577
- 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
+ }
578
1091
 
579
1092
  function _checkIdentity(id, policy) {
580
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
581
1094
  var ran = { san: false, issuer: false, sourceRepositoryURI: false };
582
1095
  if (policy === undefined || policy === null) return ran;
583
- if (typeof policy !== "object" || Array.isArray(policy)) {
1096
+ if (typeof policy !== "object" || _isArray(policy)) {
584
1097
  throw _err("sigstore/bad-input", "opts.identity must be an object naming the identity fields to pin (" + IDENTITY_FIELDS.join(", ") + ")");
585
1098
  }
586
1099
  guard.identifier.assertKnownKeys(policy, IDENTITY_KEYS, _err, "sigstore/bad-input", "opts.identity has an unknown key ");
@@ -608,7 +1121,7 @@ function _statement(payload, payloadType, expectedPredicate) {
608
1121
  }
609
1122
  var st = _jsonParse(payload, "sigstore/bad-statement", "in-toto statement");
610
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");
611
- if (!Array.isArray(st.subject) || !st.subject.length) throw _err("sigstore/bad-statement", "the in-toto statement has no subject");
1124
+ if (!_isArray(st.subject) || !st.subject.length) throw _err("sigstore/bad-statement", "the in-toto statement has no subject");
612
1125
  if (expectedPredicate && st.predicateType !== expectedPredicate) {
613
1126
  throw _err("sigstore/predicate-mismatch", "the statement predicateType " + JSON.stringify(st.predicateType) + " does not match the expected " + JSON.stringify(expectedPredicate));
614
1127
  }
@@ -664,12 +1177,28 @@ function _statement(payload, payloadType, expectedPredicate) {
664
1177
  * while reading as a policy; so is an unrecognized field name, which would otherwise
665
1178
  * pin nothing under a spelling the operator believes constrains the signer.
666
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
+ *
667
1195
  * @opts
668
1196
  * fulcioRoots: Array, // the Fulcio CA anchors: a DER Buffer or { der, validFor } each
669
1197
  * rekorKeys: Array, // [{ keyId, spki, validFor? }] the Rekor log public keys
670
1198
  * ctLogs: Array, // optional [{ keyId, spki, validFor? }] certificate-transparency logs; when given, the certificate's embedded receipt is checked
671
1199
  * identity: object, // optional policy: { san, issuer, sourceRepositoryURI }; at least one required when present
672
- * 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
673
1202
  * time: Date, // optional check-date override (default: the Rekor integratedTime)
674
1203
  *
675
1204
  * @example
@@ -680,59 +1209,155 @@ function _statement(payload, payloadType, expectedPredicate) {
680
1209
  * out.subjects[0].digest; // { sha512: "..." } -- confirm against your tarball
681
1210
  */
682
1211
  async function verifyBundle(bundle, opts) {
683
- if (bundle === null || typeof bundle !== "object" && typeof bundle !== "string" && !Buffer.isBuffer(bundle)) {
1212
+ if (bundle === null || typeof bundle !== "object" && typeof bundle !== "string" && !_isBuffer(bundle)) {
684
1213
  throw new TypeError("verifyBundle: bundle must be an object, JSON string, or Buffer");
685
1214
  }
686
1215
  opts = opts || {};
687
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. */
688
1219
  var b = parseBundle(bundle);
689
1220
  var vm = b.verificationMaterial;
1221
+ var contentType = _contentTypeOf(b);
1222
+ var isMsg = contentType === "messageSignature";
690
1223
  var env = b.dsseEnvelope;
691
- var rekorKeys = opts.rekorKeys || [];
692
- var fulcioRoots = opts.fulcioRoots || [];
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
+ }
693
1249
 
694
1250
  var leafDer = _leafCertDer(vm);
695
1251
  var leaf = _parseCert(leafDer, "Fulcio leaf certificate");
696
- var payload = _b64(env.payload, "dsseEnvelope.payload");
697
- var preimage = pae(env.payloadType, payload);
698
- var derSig = _b64(env.signatures[0].sig, "dsseEnvelope.signatures[0].sig");
699
1252
  var leafKey = _pubFromSpki(leaf.subjectPublicKeyInfo.bytes, "Fulcio leaf");
700
- if (!_rawVerify(leafKey, preimage, derSig)) throw _err("sigstore/dsse-verify-failed", "the DSSE signature does not verify under the Fulcio leaf key");
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
+ }
701
1263
 
702
- var tlogs = Array.isArray(vm.tlogEntries) ? vm.tlogEntries : [];
1264
+ var tlogs = _isArray(vm.tlogEntries) ? vm.tlogEntries : [];
703
1265
  if (!tlogs.length) throw _err("sigstore/bad-bundle", "a keyless bundle requires at least one transparency-log entry");
704
- var rekor = null, lastErr = null;
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;
705
1284
  for (var ti = 0; ti < tlogs.length; ti++) {
706
1285
  if (!tlogs[ti] || typeof tlogs[ti] !== "object") { lastErr = _err("sigstore/bad-bundle", "a transparency-log entry is not an object"); continue; }
707
- try { rekor = _verifyRekor(tlogs[ti], env, rekorKeys, leafDer); break; }
708
- catch (e) { lastErr = e; }
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; }
709
1311
  }
710
1312
  if (rekor === null) throw lastErr;
711
1313
  var integratedTime = rekor.integratedTime;
712
1314
 
713
- var checkTime = guard.time.isDate(opts.time) ? guard.time.instantOf(opts.time) : C.TIME.seconds(integratedTime);
714
- var chain = await _verifyChain(leaf, _chainDers(vm), fulcioRoots, checkTime);
715
- /** @internal With no caller instant the reference is the authenticated log-entry time, whose whole
716
- * second is inclusive: a certificate logged in the same second as its entry is not dated after it,
717
- * and nothing beyond that second is accepted. */
718
- var sctBound = guard.time.isDate(opts.time) ? checkTime : C.TIME.seconds(integratedTime) + 999;
719
- var sctResult = await _verifyEmbeddedScts(leaf, leafDer, chain.issuer, opts.ctLogs, checkTime, sctBound);
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
+
720
1338
  var identity = _identity(leaf);
721
- var identityChecked = _checkIdentity(identity, opts.identity);
1339
+ var identityChecked = _checkIdentity(identity, o.identity);
722
1340
 
723
- var st = _statement(payload, env.payloadType, opts.predicateType);
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);
724
1345
 
725
1346
  return guard.verdict.of({
726
1347
  valid: true,
727
1348
  verified: true,
1349
+ contentType: contentType,
728
1350
  payload: payload,
729
1351
  statement: st,
730
- subjects: st.subject,
731
- predicateType: st.predicateType,
732
- 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,
733
1358
  identity: identity,
734
1359
  identityChecked: identityChecked,
735
- predicateTypeChecked: opts.predicateType != null,
1360
+ predicateTypeChecked: o.predicateType != null,
736
1361
  sctChecked: sctResult.checked,
737
1362
  validScts: sctResult.validScts,
738
1363
  integratedTime: integratedTime,