@blamejs/pki 0.8.31 → 0.8.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +401 -0
  2. package/README.md +66 -15
  3. package/bin/pki.js +716 -30
  4. package/examples/issue-a-certificate.js +90 -0
  5. package/examples/post-quantum.js +66 -0
  6. package/examples/read-a-certificate.js +56 -0
  7. package/examples/revoke-and-check.js +86 -0
  8. package/examples/sign-and-verify-cms.js +63 -0
  9. package/index.js +18 -5
  10. package/lib/acme.js +223 -74
  11. package/lib/alt-sig.js +441 -0
  12. package/lib/asn1-der.js +48 -25
  13. package/lib/attrcert-sign.js +27 -19
  14. package/lib/cbor-det.js +24 -14
  15. package/lib/cmc-build.js +9 -2
  16. package/lib/cmc-verify.js +14 -3
  17. package/lib/cmp-build.js +17 -5
  18. package/lib/cmp-session.js +24 -11
  19. package/lib/cmp-verify.js +28 -19
  20. package/lib/cms-compress.js +3 -1
  21. package/lib/cms-decrypt.js +102 -7
  22. package/lib/cms-encrypt.js +59 -7
  23. package/lib/cms-sign.js +31 -9
  24. package/lib/cms-verify.js +22 -18
  25. package/lib/composite-kem.js +30 -18
  26. package/lib/composite-sig.js +29 -14
  27. package/lib/constants.js +26 -0
  28. package/lib/crl-sign.js +111 -19
  29. package/lib/crmf-sign.js +167 -15
  30. package/lib/csr-sign.js +117 -6
  31. package/lib/ct.js +1008 -58
  32. package/lib/est.js +29 -16
  33. package/lib/framework-error.js +12 -0
  34. package/lib/guard-async.js +1 -1
  35. package/lib/guard-bytes.js +103 -20
  36. package/lib/guard-crypto.js +205 -3
  37. package/lib/guard-encoding.js +6 -1
  38. package/lib/guard-identifier.js +55 -8
  39. package/lib/guard-intrinsic.js +43 -0
  40. package/lib/guard-json.js +122 -9
  41. package/lib/guard-limits.js +12 -1
  42. package/lib/guard-list.js +63 -8
  43. package/lib/guard-name.js +167 -20
  44. package/lib/guard-range.js +43 -5
  45. package/lib/guard-text.js +41 -0
  46. package/lib/guard-verdict.js +39 -8
  47. package/lib/hpke.js +688 -42
  48. package/lib/http-transport.js +156 -4
  49. package/lib/identity-match.js +4 -4
  50. package/lib/inspect.js +33 -6
  51. package/lib/jose.js +93 -25
  52. package/lib/key.js +18 -13
  53. package/lib/lint.js +165 -1
  54. package/lib/merkle.js +233 -27
  55. package/lib/ocsp.js +27 -14
  56. package/lib/oid.js +14 -4
  57. package/lib/path-validate.js +143 -61
  58. package/lib/pkcs12-build.js +7 -2
  59. package/lib/pki-build.js +31 -7
  60. package/lib/possession.js +575 -0
  61. package/lib/related-cert.js +708 -0
  62. package/lib/revocation-fetch.js +6 -6
  63. package/lib/scep.js +15 -5
  64. package/lib/schema-c509.js +5 -2
  65. package/lib/schema-cmc.js +1 -1
  66. package/lib/schema-cmp.js +8 -8
  67. package/lib/schema-cms.js +2 -15
  68. package/lib/schema-csr.js +69 -0
  69. package/lib/schema-engine.js +10 -3
  70. package/lib/schema-pkix.js +228 -2
  71. package/lib/schema-tsp.js +7 -6
  72. package/lib/sign-scheme.js +22 -9
  73. package/lib/sigstore.js +684 -168
  74. package/lib/sleep.js +8 -1
  75. package/lib/smime.js +22 -1
  76. package/lib/tlog.js +1235 -0
  77. package/lib/tls-cert-compress.js +25 -8
  78. package/lib/tsp-sign.js +120 -35
  79. package/lib/tuf.js +1395 -0
  80. package/lib/validator-tpm.js +4 -1
  81. package/lib/verify-seam.js +7 -1
  82. package/lib/webauthn-mds.js +41 -4
  83. package/lib/webauthn.js +43 -25
  84. package/lib/webcrypto.js +48 -46
  85. package/lib/x509-sign.js +225 -19
  86. package/package.json +2 -1
  87. package/sbom.cdx.json +6 -6
package/lib/sigstore.js CHANGED
@@ -38,9 +38,20 @@ var x509 = require("./schema-x509");
38
38
  var pkix = require("./schema-pkix");
39
39
  var pathValidate = require("./path-validate");
40
40
  var merkle = require("./merkle");
41
+ var tlog = require("./tlog");
42
+ var tsp = require("./tsp-sign");
41
43
  var oid = require("./oid");
42
44
  var ct = require("./ct");
43
45
  var edwardsPoint = require("./edwards-point");
46
+ /** @internal The four verdicts this verifier trusts another module for, captured at load. Each module
47
+ * exports an ordinary object, so a property fetched at the moment of the call is replaceable: one
48
+ * answering `verified: true` passes a checkpoint nothing signed, and the same holds for the timestamp,
49
+ * the embedded SCT and the certification path. */
50
+ var _tlogParseNote = tlog.parseNote;
51
+ var _tlogVerifyCheckpoint = tlog.verifyCheckpoint;
52
+ var _tspVerify = tsp.verify;
53
+ var _ctVerifySct = ct.verifySct;
54
+ var _pathBuild = pathValidate.build;
44
55
 
45
56
  /** @internal The operations this module decides with, taken at load. The bundle copy every later
46
57
  * check runs on settles what is verified, so which function answers "what does this object own" and
@@ -56,6 +67,10 @@ var _freeze = intrinsic.freeze;
56
67
  var _stringOf = intrinsic.String;
57
68
  var _numberOf = intrinsic.Number;
58
69
  var _bigIntOf = intrinsic.BigInt;
70
+ /** @internal The largest log index the verdict's `logIndex` can carry without losing precision, since
71
+ * that field is a number while a tree index is a BigInt. One above it is reported as no index rather
72
+ * than as a value a caller would read as exact. */
73
+ var _MAX_SAFE_BIGINT = _bigIntOf(Number.MAX_SAFE_INTEGER);
59
74
  var _Date = intrinsic.Date;
60
75
  var _dateParse = intrinsic.dateParse;
61
76
  var _isNaN = intrinsic.numberIsNaN;
@@ -150,6 +165,79 @@ function _hashOf(algorithm) {
150
165
  /** @internal The same three algorithms keyed the way a hashedrekord names them. The entry is the
151
166
  * authenticated statement of what was signed, so its algorithm is the one the artifact is digested
152
167
  * under; the schema enumerates exactly these three. */
168
+ /** @internal The signing-key algorithm a Rekor v0.0.2 entry's `keyDetails` names, as the OID the
169
+ * leaf's SubjectPublicKeyInfo would carry for it. The entry's statement about the key is held to the
170
+ * key rather than read in its place, since a verifier that took the declared algorithm could be told
171
+ * one thing while the signature was made with another. A name outside this table is refused rather
172
+ * than passed over, because passing over it would accept the statement unchecked. */
173
+ /** @internal What a Rekor `keyDetails` name STATES about the signing key. The name carries more than a
174
+ * key family: an ECDSA one names its curve and an RSA one its modulus size, and every NIST curve shares
175
+ * the `ecPublicKey` OID while every RSA size shares `rsaEncryption`. So holding the entry to the family
176
+ * alone leaves the rest of its own statement unchecked, and an entry can claim P-384 with SHA-384 over a
177
+ * P-256 key that signed with SHA-256. `curve` is the named-curve OID the SubjectPublicKeyInfo parameters
178
+ * must carry, and `bits` the exact modulus size the name states. */
179
+ var _KEY_DETAILS_ALG = _freeze(_assign(_create(null), {
180
+ PKIX_ECDSA_P256_SHA_256: { oid: "ecPublicKey", curve: "prime256v1" },
181
+ PKIX_ECDSA_P384_SHA_384: { oid: "ecPublicKey", curve: "secp384r1" },
182
+ PKIX_ECDSA_P521_SHA_512: { oid: "ecPublicKey", curve: "secp521r1" },
183
+ PKIX_ED25519: { oid: "Ed25519" },
184
+ PKIX_RSA_PKCS1V15_2048_SHA256: { oid: "rsaEncryption", bits: 2048 },
185
+ PKIX_RSA_PKCS1V15_3072_SHA256: { oid: "rsaEncryption", bits: 3072 },
186
+ PKIX_RSA_PKCS1V15_4096_SHA256: { oid: "rsaEncryption", bits: 4096 },
187
+ }));
188
+
189
+ /** @internal Names this build RECOGNIZES but cannot verify a signature under, mapped to the scheme it
190
+ * would otherwise have used. A `keyDetails` name states a signature scheme as well as a key, and
191
+ * signatures here are checked with RSA PKCS#1 v1.5 and ordinary Ed25519. So an entry declaring RSA-PSS
192
+ * would have had a PKCS#1 v1.5 signature accepted for it, and one declaring Ed25519ph an ordinary
193
+ * Ed25519 signature: the declaration would be read and then not honored, which is the one outcome a
194
+ * declaration must never have. They are refused instead, and refused by NAME so the message says which
195
+ * scheme is missing rather than reporting an unknown algorithm.
196
+ *
197
+ * Re-open condition: each becomes a row in the table above once its scheme is carried into signature
198
+ * verification. RSA-PSS needs the padding and salt length threaded from the entry to the verify call;
199
+ * Ed25519ph needs a pre-hashed Ed25519 the runtime does not currently offer. */
200
+ var _KEY_DETAILS_UNVERIFIABLE = _freeze(_assign(_create(null), {
201
+ PKIX_ED25519_PH: "Ed25519ph, a pre-hashed Ed25519",
202
+ PKIX_RSA_PSS_2048_SHA256: "RSASSA-PSS",
203
+ PKIX_RSA_PSS_3072_SHA256: "RSASSA-PSS",
204
+ PKIX_RSA_PSS_4096_SHA256: "RSASSA-PSS",
205
+ }));
206
+
207
+ /** @internal The rest of what a `keyDetails` name states, held to the key. The family OID is compared by
208
+ * the caller; this is the curve an ECDSA name carries and the modulus size an RSA name carries, neither of
209
+ * which the OID distinguishes. An Ed25519 name states nothing further, its OID being the whole algorithm.
210
+ * A name whose parameters the key does not match is `entry-mismatch`, the same verdict as a family that
211
+ * does not match: the entry describes a key other than the one in the bundle either way. */
212
+ function _assertKeyDetailsParameters(want, leafSpki, declared) {
213
+ if (want.curve !== undefined) {
214
+ var params = leafSpki.algorithm.parameters;
215
+ var named = null;
216
+ if (params !== null && params !== undefined) {
217
+ try { named = asn1.read.oid(asn1.decode(params)); }
218
+ catch (_e) { named = null; } // allow:swallow-unverified -- parameters this build cannot read name no curve, and the mismatch below is then the verdict
219
+ }
220
+ if (named !== oid.byName(want.curve)) {
221
+ throw _err("sigstore/entry-mismatch", "the Rekor entry states the signing key is " +
222
+ guard.text.showValue(declared) + ", whose curve is " + want.curve +
223
+ ", while the bundle's leaf key is on " +
224
+ (named === null ? "no curve this build reads" : guard.text.showValue(oid.name(named) || named)));
225
+ }
226
+ }
227
+ if (want.bits !== undefined) {
228
+ var bits;
229
+ try {
230
+ bits = guard.crypto.publicKey({ key: leafSpki.bytes, format: "der", type: "spki" })
231
+ .asymmetricKeyDetails.modulusLength;
232
+ } catch (_e) { bits = null; } // allow:swallow-unverified -- a key that will not import has no modulus size to match, and the mismatch below is the verdict
233
+ if (bits !== want.bits) {
234
+ throw _err("sigstore/entry-mismatch", "the Rekor entry states the signing key is " +
235
+ guard.text.showValue(declared) + ", which is " + want.bits + " bits, while the bundle's leaf key is " +
236
+ (bits === null ? "not an RSA key this build reads" : bits + " bits"));
237
+ }
238
+ }
239
+ }
240
+
153
241
  var _HASH_BY_REKOR = _freeze(_assign(_create(null), {
154
242
  sha256: { node: "sha256", bytes: 32 },
155
243
  sha384: { node: "sha384", bytes: 48 },
@@ -163,28 +251,21 @@ var JSON_MAX = C.LIMITS.JSON_MAX_BYTES;
163
251
  * applied alone would admit it one way and refuse it the other. */
164
252
  var BUNDLE_MAX_DEPTH = 64;
165
253
 
254
+ /** @internal The bundle members read as integers, and so the members whose number tokens must denote
255
+ * exactly the integer they are read as. Each goes into the canonical form a signed entry timestamp
256
+ * covers, so a rounding spelling verifies under the log's signature while naming another value.
257
+ *
258
+ * Named individually because this reader also parses the in-toto statement, whose `predicate` is
259
+ * arbitrary JSON by specification and may legitimately carry fractional numbers. A rule over every
260
+ * number in the document would refuse a conforming attestation. */
261
+ var EXACT_INTEGER_BUNDLE_MEMBERS = intrinsic.assign(intrinsic.create(null), {
262
+ logIndex: 1, integratedTime: 1, treeSize: 1,
263
+ });
264
+
166
265
  var SP = _bufferFrom(" ", "ascii");
167
266
  var DSSEV1 = _bufferFrom("DSSEv1", "ascii");
168
267
  var _EM_DASH = _fromCharCode(0x2014);
169
268
 
170
- function _parseNoteSig(line) {
171
- var prefix = _EM_DASH + " ";
172
- if (_strSlice(line, 0, prefix.length) !== prefix) return null;
173
- var p = prefix.length, n = line.length;
174
- var t1s = p;
175
- while (p < n && !pkix.isJsWhitespace(_charCodeAt(line, p))) p += 1;
176
- if (p === t1s) return null;
177
- var keyId = _strSlice(line, t1s, p);
178
- if (_charAt(line, p) !== " ") return null;
179
- p += 1;
180
- var t2s = p;
181
- while (p < n && !pkix.isJsWhitespace(_charCodeAt(line, p))) p += 1;
182
- if (p === t2s) return null;
183
- var sig = _strSlice(line, t2s, p);
184
- if (p !== n) return null;
185
- return { keyId: keyId, sig: sig };
186
- }
187
-
188
269
  function _stripTrailingEquals(s) {
189
270
  var i = s.length;
190
271
  while (i > 0 && _charAt(s, i - 1) === "=") i -= 1;
@@ -203,9 +284,23 @@ function _isBundleMediaType(mt) {
203
284
  return typeof mt === "string" && _hasOwn(_BUNDLE_MEDIA_TYPES, mt);
204
285
  }
205
286
 
206
- function _jsonParse(input, code, label) {
287
+ /** @internal `exactIntegerMembers` is passed by the CALLER, because this helper reads two different
288
+ * documents and only one of them has members whose numbers mean an integer.
289
+ *
290
+ * The bundle's `logIndex`, `integratedTime` and `treeSize` enter the canonical form a signed entry
291
+ * timestamp covers, so a spelling that rounds to a different value than it denotes verifies while the
292
+ * value reported is not the one the log signed, and `1.0000000000000001` converts to the Number 1 so no
293
+ * check made after conversion can tell.
294
+ *
295
+ * The in-toto STATEMENT carries none: its `predicate` is arbitrary JSON by specification, so a predicate
296
+ * is passed no policy at all. Naming members here instead would constrain a predicate field for merely
297
+ * sharing a name with log metadata, and `predicate: { integratedTime: 0.5 }` is as conforming as
298
+ * `predicate: { score: 0.5 }`. A document-wide rule and a name-matched one fail the same way; what the
299
+ * rule has to follow is which DOCUMENT is being read. */
300
+ function _jsonParse(input, code, label, exactMembers) {
207
301
  return guard.json.parse(input, _err, {
208
302
  maxBytes: JSON_MAX, maxDepth: BUNDLE_MAX_DEPTH,
303
+ exactIntegerMembers: exactMembers,
209
304
  badJson: code, tooDeep: code, duplicateMember: code, tooLarge: code, badInput: code, label: label,
210
305
  });
211
306
  }
@@ -283,7 +378,7 @@ function _b64(s, label) {
283
378
  function parseBundle(input) {
284
379
  var obj;
285
380
  if (_isBuffer(input) || typeof input === "string") {
286
- obj = _jsonParse(input, "sigstore/bad-bundle", "bundle");
381
+ obj = _jsonParse(input, "sigstore/bad-bundle", "bundle", EXACT_INTEGER_BUNDLE_MEMBERS);
287
382
  } else if (input && typeof input === "object") {
288
383
  /** @internal An object is copied and read back before a single rule runs on it, so this verb
289
384
  * decides what the verifying one decides. Checking the caller's object in place would run its
@@ -616,16 +711,17 @@ function _chainDers(vm) {
616
711
  function _rawVerify(keyObj, data, derSig) {
617
712
  var t = keyObj.asymmetricKeyType;
618
713
  if (t === "ed25519" || t === "ed448") {
619
- edwardsPoint.validateSpki(keyObj.export({ type: "spki", format: "der" }), t === "ed25519" ? 6 : 7, SigstoreError, "sigstore/bad-key");
620
- return nodeCrypto.verify(null, data, keyObj, derSig);
714
+ var edSpki = guard.crypto.exportKey(keyObj, { type: "spki", format: "der" });
715
+ edwardsPoint.validateSpki(edSpki, t === "ed25519" ? 6 : 7, SigstoreError, "sigstore/bad-key");
716
+ return guard.crypto.verify(null, data, keyObj, derSig);
621
717
  }
622
718
  var crv = keyObj.asymmetricKeyDetails && keyObj.asymmetricKeyDetails.namedCurve;
623
719
  var hash = crv === "secp384r1" ? "sha384" : (crv === "secp521r1" ? "sha512" : "sha256");
624
- return nodeCrypto.verify(hash, data, { key: keyObj, dsaEncoding: "der" }, derSig);
720
+ return guard.crypto.verify(hash, data, { key: keyObj, dsaEncoding: "der" }, derSig);
625
721
  }
626
722
  function _pubFromSpki(spkiDer, label) {
627
723
  var key;
628
- try { key = nodeCrypto.createPublicKey({ key: spkiDer, format: "der", type: "spki" }); }
724
+ try { key = guard.crypto.publicKey({ key: spkiDer, format: "der", type: "spki" }); }
629
725
  catch (e) { throw _err("sigstore/bad-key", "invalid " + label + " public key", e); }
630
726
  var t = key.asymmetricKeyType;
631
727
  if (t === "ed25519" || t === "ed448") {
@@ -648,51 +744,107 @@ function _toMs(x) {
648
744
  if (typeof x === "string") { var t = _dateParse(x); return _isNaN(t) ? null : t; }
649
745
  return null;
650
746
  }
747
+ /** @internal A pinned validity window, resolved to milliseconds ONCE and copied off the caller's object.
748
+ * `_inWindow` runs on both sides of an await, so a window read from the caller at each check could widen
749
+ * between two of them. A bound that is present but not a time becomes NaN, which no comparison satisfies,
750
+ * so an unreadable window keeps the key out exactly as reading it late did. A `validFor` that is not an
751
+ * object imposes nothing, which is what a missing one does. */
752
+ function _normWindow(vf) {
753
+ if (vf == null || typeof vf !== "object") return null;
754
+ /** @internal Each bound is read from the caller ONCE, into a local, and the presence test, the conversion
755
+ * and the stored value all read that local. A presence test plus a conversion plus an assignment is
756
+ * three reads of one member, and an accessor answering the first two with a bound and the third with
757
+ * null would have the window discarded after it had already been parsed. */
758
+ var rawStart = vf.start, rawEnd = vf.end;
759
+ var s = rawStart == null ? null : _toMs(rawStart);
760
+ var e = rawEnd == null ? null : _toMs(rawEnd);
761
+ var out = _create(null);
762
+ out.start = rawStart == null ? null : (s === null ? NaN : s);
763
+ out.end = rawEnd == null ? null : (e === null ? NaN : e);
764
+ return out;
765
+ }
766
+ /** @internal Copies a caller's bytes when they are bytes, and passes anything else through so the check
767
+ * that owns the field still reports it. */
768
+ function _maybeSnapshot(v, label) {
769
+ if (v == null || !guard.bytes.isByteSource(v)) return v;
770
+ /** @internal Normalized to a byte view before it is copied. The test above admits what
771
+ * `guard.bytes.isByteSource` admits, which includes a DataView and an ArrayBuffer, and
772
+ * `guard.bytes.snapshot` takes neither, so a pinned key held as the ArrayBuffer
773
+ * `Response.arrayBuffer()` returns had the bundle refused for its container rather than its content.
774
+ * `snapshotSource` views and copies in one call, and the view shares the caller's store, so the copy
775
+ * is still the only allocation. */
776
+ return guard.bytes.snapshotSource(v, _err, "sigstore/bad-input", label);
777
+ }
651
778
  function _inWindow(timeMs, vf) {
652
779
  if (!vf) return true;
653
- if (vf.start != null) { var s = _toMs(vf.start); if (s === null || !(timeMs >= s)) return false; }
654
- if (vf.end != null) { var e = _toMs(vf.end); if (e === null || !(timeMs <= e)) return false; }
780
+ if (vf.start !== null && !(timeMs >= vf.start)) return false;
781
+ if (vf.end !== null && !(timeMs <= vf.end)) return false;
655
782
  return true;
656
783
  }
657
- function _rekorKey(rekorKeys, keyIdBuf, hint, timeMs) {
784
+ /** @internal The signed tree head, verified through `pki.tlog` rather than re-implemented here. The
785
+ * one implementation is what holds the note to the shape its specification fixes: the text ends at
786
+ * the LAST blank line, the body is three non-empty lines, the tree size carries no leading zero, the
787
+ * root is 32 bytes, no ASCII control byte appears outside a newline, and the signature lines are
788
+ * capped.
789
+ *
790
+ * Which log signed it is the load-bearing part. The entry names its log in `logId.keyId`, and only
791
+ * that log's key is offered to the verify, because a valid signature from a second pinned log over
792
+ * the same tree root is a statement about that log's tree and not about this entry's. Resolving on
793
+ * the four-byte hint alone accepted one for the other. With no `logId.keyId` the entry claims no log,
794
+ * so there is nothing to bind and every key valid at the instant is offered.
795
+ *
796
+ * The key name comes from the note's own signature lines. The identifier derivation binds the name to
797
+ * the key material for the algorithms where the name is in it, so a line that renames a key no longer
798
+ * matches its identifier; for ECDSA the identifier is the key material alone and the binding is the
799
+ * pinned key itself. Returns the verified checkpoint, whose size and root the fold then uses, or
800
+ * `null` when no pinned key signed it. */
801
+ async function _verifyCheckpoint(envelope, te, rekorKeys, timeMs) {
802
+ var claimed = te.logId && te.logId.keyId ? _b64(te.logId.keyId, "logId.keyId") : null;
803
+ var usable = [];
658
804
  for (var i = 0; i < rekorKeys.length; i++) {
659
805
  var k = rekorKeys[i];
660
- var match = (keyIdBuf && k.keyId && _bufferEquals(k.keyId, keyIdBuf)) ||
661
- (hint && k.keyId && k.keyId.length >= 4 && _bufferEquals(_subarray(k.keyId, 0, 4), hint));
662
- if (match && _inWindow(timeMs, k.validFor)) return k;
663
- }
664
- return null;
665
- }
666
-
667
- function _verifyCheckpoint(envelope, ip, rekorKeys, timeMs) {
668
- var buf = _bufferFrom(envelope, "utf8");
669
- var sep = _bufIndexOf(buf, "\n\n");
670
- if (sep < 0) throw _err("sigstore/bad-checkpoint", "the checkpoint has no note/signature separator");
671
- var body = _subarray(buf, 0, sep + 1);
672
- var lines = _split(_bufToString(body, "utf8"), "\n");
673
- if (_bufToString(_b64(lines[2], "checkpoint root"), "hex") !== _bufToString(_b64(ip.rootHash, "inclusionProof.rootHash"), "hex")) {
674
- throw _err("sigstore/inclusion-proof-mismatch", "the checkpoint root does not match the inclusion-proof root");
675
- }
676
- var sigBlock = _split(_bufToString(_subarray(buf, sep + 2), "utf8"), "\n");
677
- for (var i = 0; i < sigBlock.length; i++) {
678
- var note = _parseNoteSig(sigBlock[i]);
679
- if (!note) continue;
680
- var blob = _b64(note.sig, "checkpoint signature");
681
- if (blob.length < 5) continue;
682
- var hint = _subarray(blob, 0, 4), derSig = _subarray(blob, 4);
683
- var k = _rekorKey(rekorKeys, null, hint, timeMs);
684
- if (!k) continue;
685
- if (_rawVerify(_pubFromSpki(k.spki, "Rekor log"), body, derSig)) return true;
686
- }
687
- return false;
806
+ if (!_inWindow(timeMs, k.validFor)) continue;
807
+ if (claimed !== null && !(k.keyId && _bufferEquals(k.keyId, claimed))) continue;
808
+ /** @internal The caller's own key is validated here, by the function that owns that error, so a
809
+ * pinned key that does not parse is reported as the trust-material fault it is rather than as a
810
+ * malformed checkpoint. */
811
+ _pubFromSpki(k.spki, "Rekor log");
812
+ guard.list.append(usable, k);
813
+ }
814
+ if (!usable.length) return null;
815
+ var note;
816
+ try { note = _tlogParseNote(envelope); }
817
+ catch (e) { throw _err("sigstore/bad-checkpoint", "the checkpoint is not a well-formed signed note: " + e.message, e); }
818
+ var seen = _create(null), names = [];
819
+ for (var s = 0; s < note.signatures.length; s++) {
820
+ var nm = note.signatures[s].keyName;
821
+ if (seen[nm] === undefined) { seen[nm] = 1; guard.list.append(names, nm); }
822
+ }
823
+ var keys = [];
824
+ for (var u = 0; u < usable.length; u++) {
825
+ for (var n = 0; n < names.length; n++) guard.list.append(keys, { name: names[n], publicKey: usable[u].spki });
826
+ }
827
+ /** @internal Unreachable while the two steps above hold: a parsed note carries at least one
828
+ * signature line, so `names` is never empty, and an empty `usable` already returned. It is kept
829
+ * because the two are separate steps and a change to how names are collected should not reach a
830
+ * verify with no candidate. */
831
+ if (!keys.length) return null;
832
+ var v;
833
+ try { v = await _tlogVerifyCheckpoint(envelope, keys); }
834
+ catch (e) {
835
+ /** @internal A signature from a key this caller pinned that does not verify is not a malformed
836
+ * checkpoint; it is an unattested root, which is the verdict the caller already handles. */
837
+ if (e && e.code === "tlog/bad-signature") return null;
838
+ throw _err("sigstore/bad-checkpoint", "the checkpoint is malformed: " + e.message, e);
839
+ }
840
+ return v.verified ? v : null;
688
841
  }
689
842
 
690
843
  function _verifySet(te, rekorKeys, timeMs, canonBody) {
691
844
  var promise = te.inclusionPromise;
692
845
  if (!promise || typeof promise.signedEntryTimestamp !== "string") return false;
693
846
  var keyId = te.logId && te.logId.keyId ? _b64(te.logId.keyId, "logId.keyId") : null;
694
- var k = _rekorKey(rekorKeys, keyId, null, timeMs);
695
- if (!k) return false;
847
+ if (keyId === null) return false;
696
848
  var canonical = _jsonStringify({
697
849
  body: canonBody,
698
850
  integratedTime: _numOf(te.integratedTime),
@@ -700,7 +852,18 @@ function _verifySet(te, rekorKeys, timeMs, canonBody) {
700
852
  logIndex: _numOf(te.logIndex),
701
853
  });
702
854
  var sig = _b64(promise.signedEntryTimestamp, "signedEntryTimestamp");
703
- return _rawVerify(_pubFromSpki(k.spki, "Rekor log"), _bufferFrom(canonical, "utf8"), sig);
855
+ var msg = _bufferFrom(canonical, "utf8");
856
+ /** @internal EVERY key the entry's own identifier selects is tried, not the first. A caller may
857
+ * pin two keys carrying one identifier -- a rotated pair, or a stale entry beside a current one --
858
+ * and stopping at the first would report "not attested" for a timestamp the second key signed.
859
+ * That is a denial of a sound bundle rather than a security property. */
860
+ for (var i = 0; i < rekorKeys.length; i++) {
861
+ var k = rekorKeys[i];
862
+ if (!_inWindow(timeMs, k.validFor)) continue;
863
+ if (!(k.keyId && _bufferEquals(k.keyId, keyId))) continue;
864
+ if (_rawVerify(_pubFromSpki(k.spki, "Rekor log"), msg, sig)) return true;
865
+ }
866
+ return false;
704
867
  }
705
868
 
706
869
  /** @internal The entry rows this build reads, keyed on the entry's OWN kind and apiVersion together.
@@ -710,12 +873,25 @@ function _verifySet(te, rekorKeys, timeMs, canonBody) {
710
873
  * unsupported entry. Each row states which content arm it may accompany, because an entry describing
711
874
  * a DSSE envelope says nothing about a message signature and the two must agree. */
712
875
  var _ENTRY_ROWS = _freeze(_assign(_create(null), {
713
- "dsse@0.0.1": { content: "dsseEnvelope", bind: _bindDsseEntry },
714
- "hashedrekord@0.0.1": { content: "messageSignature", bind: _bindHashedRekordEntry },
876
+ "dsse@0.0.1": { contents: _freeze(_assign(_create(null), { dsseEnvelope: 1 })), bind: _bindDsseEntry, time: "set" },
877
+ "hashedrekord@0.0.1": { contents: _freeze(_assign(_create(null), { messageSignature: 1 })), bind: _bindHashedRekordEntry, time: "set" },
878
+ /** @internal Rekor v2 supports no DSSE entry type, so a DSSE signature is logged as a
879
+ * hashedrekord over the hash of its PAE preimage. One row therefore serves both arms, and the
880
+ * digest it binds differs by arm: the PAE hash for an envelope, the artifact hash for a message
881
+ * signature. The version is part of the key because a v2 body names its fields differently from a
882
+ * v0.0.1 one, so reading one shape's names out of the other would report a mismatch rather than an
883
+ * unsupported entry. `time: "rfc3161"` because Rekor v2 issues no signed entry timestamp. */
884
+ "hashedrekord@0.0.2": {
885
+ contents: _freeze(_assign(_create(null), { dsseEnvelope: 1, messageSignature: 1 })),
886
+ bind: _bindHashedRekordV002Entry, time: "rfc3161",
887
+ },
715
888
  }));
716
889
 
717
890
  function _entryBody(canonBytes) {
718
- var body = _jsonParse(canonBytes, "sigstore/bad-tlog-entry", "Rekor entry body");
891
+ /** @internal A Rekor entry body carries `logIndex` and `integratedTime` the same way the bundle does,
892
+ * and its canonical bytes are what the entry's inclusion is proven over, so the same members apply. */
893
+ var body = _jsonParse(canonBytes, "sigstore/bad-tlog-entry", "Rekor entry body",
894
+ EXACT_INTEGER_BUNDLE_MEMBERS);
719
895
  if (!body || typeof body !== "object" || _isArray(body)) throw _err("sigstore/bad-tlog-entry", "the Rekor entry body is not a JSON object");
720
896
  return body;
721
897
  }
@@ -733,7 +909,7 @@ function _entryRow(body, contentType) {
733
909
  if (!row) {
734
910
  throw _err("sigstore/unsupported-content", "unsupported Rekor entry kind and version: " + guard.text.showValue(key));
735
911
  }
736
- if (row.content !== contentType) {
912
+ if (row.contents[contentType] !== 1) {
737
913
  throw _err("sigstore/unsupported-content", "the Rekor entry is a " + guard.text.showValue(key) +
738
914
  " while the bundle carries a " + contentType + "; the entry and the content arm must describe the same signature");
739
915
  }
@@ -841,23 +1017,124 @@ function _bindHashedRekordEntry(body, ms, leafDer) {
841
1017
  return { algorithm: data.hash.algorithm, node: alg.node, value: data.hash.value };
842
1018
  }
843
1019
 
844
- function _verifyRekor(te, envelope, rekorKeys, leafDer) {
845
- return _verifyRekorEntry(te, rekorKeys, leafDer, "dsseEnvelope", function (body) {
846
- _bindDsseEntry(body, envelope, leafDer);
1020
+ /** @internal The Rekor v2 `hashedrekord` v0.0.2 binding. Proving inclusion of some bytes says
1021
+ * nothing until those bytes are shown to describe THIS artifact and THIS signature, and the entry
1022
+ * body is what the log actually wrote rather than something reconstructed here, so the whole burden
1023
+ * falls on these comparisons. Each leg is separate because dropping any one leaves inclusion proving
1024
+ * nothing about this bundle: the signature, the certificate that made it, and the digest it was made
1025
+ * over.
1026
+ *
1027
+ * The digest differs by arm. For a DSSE envelope Rekor v2 records the hash of the PAE preimage, so it
1028
+ * is recomputed here from the bundle's own envelope; for a message signature it records the
1029
+ * artifact's digest, which the caller confirms against the bytes it supplies, exactly as the v0.0.1
1030
+ * arm does. */
1031
+ function _bindHashedRekordV002Entry(body, arm, leafDer, contentType, payloadType, payload) {
1032
+ var spec = body.spec;
1033
+ if (!spec || typeof spec !== "object") {
1034
+ throw _err("sigstore/bad-tlog-entry", "the Rekor hashedrekord v0.0.2 entry has no spec");
1035
+ }
1036
+ var hr = spec.hashedRekordV002;
1037
+ if (!hr || typeof hr !== "object") {
1038
+ throw _err("sigstore/bad-tlog-entry", "the Rekor hashedrekord v0.0.2 entry has no hashedRekordV002 spec");
1039
+ }
1040
+ var sig = hr.signature;
1041
+ if (!sig || typeof sig !== "object" || typeof sig.content !== "string") {
1042
+ throw _err("sigstore/bad-tlog-entry", "the Rekor hashedrekord v0.0.2 entry is missing its signature content");
1043
+ }
1044
+ var bodySig = _b64(sig.content, "Rekor entry signature");
1045
+ var armSig = contentType === "dsseEnvelope"
1046
+ ? _b64(arm.signatures[0].sig, "dsseEnvelope.signatures[0].sig")
1047
+ : _b64(arm.signature, "messageSignature.signature");
1048
+ if (!_bufferEquals(armSig, bodySig)) {
1049
+ throw _err("sigstore/entry-mismatch", "the Rekor entry signature does not match the bundle signature");
1050
+ }
1051
+ var verifier = sig.verifier;
1052
+ if (!verifier || typeof verifier !== "object") {
1053
+ throw _err("sigstore/bad-tlog-entry", "the Rekor hashedrekord v0.0.2 entry is missing its signature verifier");
1054
+ }
1055
+ var x5 = verifier.x509Certificate;
1056
+ if (!x5 || typeof x5 !== "object" || typeof x5.rawBytes !== "string") {
1057
+ throw _err("sigstore/bad-tlog-entry", "the Rekor hashedrekord v0.0.2 verifier carries no x509Certificate rawBytes");
1058
+ }
1059
+ /** @internal A v0.0.2 verifier carries the raw certificate, where a v0.0.1 one carried PEM. */
1060
+ if (!_bufferEquals(_b64(x5.rawBytes, "verifier.x509Certificate.rawBytes"), leafDer)) {
1061
+ throw _err("sigstore/entry-mismatch", "the Rekor entry verifier certificate does not match the bundle leaf certificate");
1062
+ }
1063
+ /** @internal The entry states which algorithm the key is, and that statement is held to the key
1064
+ * itself: a declared algorithm a caller reads instead of the key's own would let the entry name one
1065
+ * thing while the signature was made with another. */
1066
+ /** @internal A declaration that is PRESENT is read, whatever its type. Entering this only for a string
1067
+ * meant a `keyDetails` of any other type was passed over as though the entry had declared nothing, which
1068
+ * is the looser of the two readings of one field: an unknown STRING is refused below, so a number would
1069
+ * have been the way past every check here. Protobuf JSON permits an enum as its numeric value, so a
1070
+ * number is a shape a real producer emits; this build reads the names, and says so. An ABSENT
1071
+ * keyDetails is the separate case that states nothing to hold. */
1072
+ if (verifier.keyDetails !== undefined && verifier.keyDetails !== null &&
1073
+ typeof verifier.keyDetails !== "string") {
1074
+ throw _err("sigstore/bad-tlog-entry", "the Rekor entry states a key algorithm as " +
1075
+ guard.text.showValue(verifier.keyDetails) + "; this build reads the enum by NAME, so a declaration " +
1076
+ "it cannot read is refused rather than passed over as though none had been made");
1077
+ }
1078
+ if (typeof verifier.keyDetails === "string") {
1079
+ /** @internal Checked BEFORE the key comparison: a name whose scheme cannot be honored is refused
1080
+ * whatever key the bundle carries, because verifying it would mean checking the signature under a
1081
+ * scheme other than the one the entry states. */
1082
+ var missing = _KEY_DETAILS_UNVERIFIABLE[guard.text.keyOf(verifier.keyDetails)];
1083
+ if (missing !== undefined) {
1084
+ throw _err("sigstore/bad-tlog-entry", "the Rekor entry states the signing key is " +
1085
+ guard.text.showValue(verifier.keyDetails) + ", whose signature scheme is " + missing +
1086
+ "; this build verifies RSA PKCS#1 v1.5 and ordinary Ed25519, so honoring that entry would mean " +
1087
+ "checking the signature under a scheme the entry does not name");
1088
+ }
1089
+ var want = _KEY_DETAILS_ALG[verifier.keyDetails];
1090
+ if (want === undefined) {
1091
+ throw _err("sigstore/bad-tlog-entry", "the Rekor entry names a key algorithm this build does not read: " +
1092
+ guard.text.showValue(verifier.keyDetails));
1093
+ }
1094
+ var leafSpki = _parseCert(leafDer, "Fulcio leaf certificate").subjectPublicKeyInfo;
1095
+ if (oid.byName(want.oid) !== leafSpki.algorithm.oid) {
1096
+ throw _err("sigstore/entry-mismatch", "the Rekor entry states the signing key is " +
1097
+ guard.text.showValue(verifier.keyDetails) + " while the bundle's leaf key is not");
1098
+ }
1099
+ _assertKeyDetailsParameters(want, leafSpki, verifier.keyDetails);
1100
+ }
1101
+ var data = hr.data;
1102
+ if (!data || typeof data !== "object" || typeof data.digest !== "string") {
1103
+ throw _err("sigstore/bad-tlog-entry", "the Rekor hashedrekord v0.0.2 entry has no data digest");
1104
+ }
1105
+ var alg = _hashOf(data.algorithm);
1106
+ if (!alg) {
1107
+ throw _err("sigstore/bad-tlog-entry", "the Rekor v0.0.2 entry names a digest algorithm outside the schema: " +
1108
+ (data.algorithm === undefined ? "(none)" : guard.text.showValue(data.algorithm)));
1109
+ }
1110
+ var digest = _b64(data.digest, "hashedRekordV002.data.digest");
1111
+ if (contentType === "dsseEnvelope") {
1112
+ var computed = nodeCrypto.createHash(alg.node).update(pae(payloadType, payload)).digest();
1113
+ if (!guard.crypto.constantTimeEqual(computed, digest)) {
1114
+ throw _err("sigstore/entry-mismatch", "the Rekor entry digest is not the hash of this envelope's " +
1115
+ "signing preimage, so the entry records a different signature than the bundle carries");
1116
+ }
847
1117
  return null;
848
- });
1118
+ }
1119
+ if (digest.length !== alg.bytes) {
1120
+ throw _err("sigstore/bad-tlog-entry", "the Rekor v0.0.2 entry digest is " + digest.length +
1121
+ " bytes where " + data.algorithm + " is " + alg.bytes);
1122
+ }
1123
+ return { algorithm: data.algorithm, node: alg.node, value: _bufToString(digest, "hex") };
1124
+ }
1125
+
1126
+ async function _verifyRekor(te, envelope, rekorKeys, leafDer, payloadType, payload, externalMs) {
1127
+ return _verifyRekorEntry(te, rekorKeys, leafDer, "dsseEnvelope", envelope, payloadType, payload, externalMs);
849
1128
  }
850
1129
 
851
1130
  /** @internal The message-signature route through the same entry verification. Only the binding
852
- * differs, so the inclusion proof, the checkpoint and the SET are checked in one place for both arms
853
- * rather than in a copy per arm. */
854
- function _verifyRekorMessage(te, ms, rekorKeys, leafDer) {
855
- return _verifyRekorEntry(te, rekorKeys, leafDer, "messageSignature", function (body) {
856
- return _bindHashedRekordEntry(body, ms, leafDer);
857
- });
1131
+ * differs, so the inclusion proof, the checkpoint and the log time are checked in one place for both
1132
+ * arms rather than in a copy per arm. */
1133
+ async function _verifyRekorMessage(te, ms, rekorKeys, leafDer, externalMs) {
1134
+ return _verifyRekorEntry(te, rekorKeys, leafDer, "messageSignature", ms, null, null, externalMs);
858
1135
  }
859
1136
 
860
- function _verifyRekorEntry(te, rekorKeys, leafDer, contentType, bind) {
1137
+ async function _verifyRekorEntry(te, rekorKeys, leafDer, contentType, arm, payloadType, payload, externalMs) {
861
1138
  var ip = te.inclusionProof;
862
1139
  if (!ip || typeof ip.rootHash !== "string" || !_isArray(ip.hashes)) {
863
1140
  throw _err("sigstore/bad-inclusion-proof", "the transparency-log entry has no inclusion proof");
@@ -875,75 +1152,159 @@ function _verifyRekorEntry(te, rekorKeys, leafDer, contentType, bind) {
875
1152
  }
876
1153
  var canonBytes = _b64(canonB64, "canonicalizedBody");
877
1154
  var body = _entryBody(canonBytes);
878
- _entryRow(body, contentType);
879
- var artifactHash = bind(body);
1155
+ var row = _entryRow(body, contentType);
1156
+ var artifactHash = row.bind(body, arm, leafDer, contentType, payloadType, payload) || null;
1157
+ /** @internal The instant a pinned Rekor key's validity window is judged at. An entry carrying a
1158
+ * signed entry timestamp dates itself, so that is the instant. A Rekor v2 entry does not: its
1159
+ * `integratedTime` is always zero and must be ignored, so the instant is the one established
1160
+ * outside the log, from a timestamp token or from the caller. */
1161
+ var atMs = row.time === "set" ? claimedMs : externalMs;
1162
+ /** @internal The checkpoint is verified BEFORE the fold, and the fold then uses the size and the
1163
+ * root IT carries. The `inclusionProof` fields naming a root and a tree size are covered by no
1164
+ * signature, so folding against them and comparing afterwards decides the proof against a number
1165
+ * the bundle chose. Verifying first means the only root in scope is an attested one. */
1166
+ var cpResult = (ip.checkpoint && typeof ip.checkpoint.envelope === "string")
1167
+ ? await _verifyCheckpoint(ip.checkpoint.envelope, te, rekorKeys, atMs) : null;
1168
+ if (!cpResult) throw _err("sigstore/unsigned-root", "the Rekor checkpoint (the signed tree root) did not verify under the Rekor key this entry names -- the inclusion-proof root is not attested");
1169
+ var checkpoint = cpResult.checkpoint;
1170
+ /** @internal WHICH field positions the leaf depends on the log's generation. Rekor v2 states the index
1171
+ * once, at the top level: its client guidance says the log index "should be read from the top-level
1172
+ * TransparencyLogEntry.log_index", and that `inclusion_proof.log_index` is one of the fields
1173
+ * "repeated elsewhere and should be ignored", along with the proof's own `root_hash` and `tree_size`
1174
+ * that the checkpoint already supplies above. Folding against a field a conforming log need not
1175
+ * populate, and reporting it as the entry's index, gave a caller a number that does not find the
1176
+ * entry in the log. A Rekor v1 entry carries two different numbers, a global index and a
1177
+ * shard-relative one, and the proof folds against the shard the checkpoint covers, so there the
1178
+ * proof's own field is the one that positions the leaf and the entry's is what the signed entry
1179
+ * timestamp covers.
1180
+ * ONE conversion either way, bound here and used both to fold the proof and to report the result.
1181
+ * Converting the field twice let the two disagree: `BigInt` reads a single element array through its
1182
+ * string form, so `["1"]` folds as leaf 1 and verifies, while a second numeric conversion of the same
1183
+ * field gives NaN and the verdict then said it had no index at all. */
1184
+ var leafIsFromProof = row.time === "set";
1185
+ var proofLeaf;
1186
+ try { proofLeaf = _bigIntOf(leafIsFromProof ? ip.logIndex : te.logIndex); }
1187
+ catch (e) {
1188
+ throw _err("sigstore/bad-inclusion-proof",
1189
+ (leafIsFromProof ? "the inclusion proof's logIndex" : "the entry's logIndex") + " is not an integer", e);
1190
+ }
880
1191
  var included;
881
1192
  try {
882
1193
  included = merkle.verifyInclusion({
883
- leafIndex: _bigIntOf(ip.logIndex),
884
- treeSize: _bigIntOf(ip.treeSize),
1194
+ leafIndex: proofLeaf,
1195
+ treeSize: checkpoint.treeSize,
885
1196
  leafHash: merkle.leafHash(canonBytes),
886
1197
  proof: _map(ip.hashes, function (h, i) { return _b64(h, "inclusionProof.hashes[" + i + "]"); }),
887
- rootHash: _b64(ip.rootHash, "inclusionProof.rootHash"),
1198
+ rootHash: checkpoint.rootHash,
888
1199
  });
889
1200
  } catch (e) {
890
1201
  throw _err("sigstore/bad-inclusion-proof", "the inclusion proof is malformed: " + e.message, e);
891
1202
  }
892
- if (!included) throw _err("sigstore/inclusion-proof-mismatch", "the inclusion proof does not reconstruct the tree root");
893
- var checkpointOk = ip.checkpoint && typeof ip.checkpoint.envelope === "string" && _verifyCheckpoint(ip.checkpoint.envelope, ip, rekorKeys, claimedMs);
894
- 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");
895
- var setOk = _verifySet(te, rekorKeys, claimedMs, canonB64);
896
- 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");
897
- var t = _numOf(te.integratedTime);
898
- if (!_isFiniteNumber(t) || t < 0) throw _err("sigstore/bad-tlog-entry", "the transparency-log entry has a malformed integratedTime");
899
- var li = _numOf(te.logIndex);
1203
+ if (!included) throw _err("sigstore/inclusion-proof-mismatch", "the inclusion proof does not reconstruct the tree root the checkpoint attests");
1204
+ /** @internal Only an entry whose log issues a signed entry timestamp has one to check. Rekor v2
1205
+ * issues none and writes `integratedTime` as zero, so reading it as a log time would date an
1206
+ * ephemeral certificate against a value nothing signed and nobody claimed; the instant for such an
1207
+ * entry comes from a timestamp token instead, and `integratedTime` is left null here so no caller
1208
+ * can mistake the absence for a time. */
1209
+ var integratedTime = null;
1210
+ if (row.time === "set") {
1211
+ if (!_verifySet(te, rekorKeys, claimedMs, canonB64)) {
1212
+ 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");
1213
+ }
1214
+ var t = _numOf(te.integratedTime);
1215
+ if (!_isFiniteNumber(t) || t < 0) throw _err("sigstore/bad-tlog-entry", "the transparency-log entry has a malformed integratedTime");
1216
+ integratedTime = t;
1217
+ }
1218
+ /** @internal The index reported is an ATTESTED one on either path. On v1 the signed entry timestamp
1219
+ * covers the entry's own `logIndex`, which is a different quantity from the shard-relative index the
1220
+ * proof folded at, so the two need not agree and comparing them would refuse sound bundles. On v2
1221
+ * there is one index and the fold above ran at it, so the number reported is the position the proof
1222
+ * established rather than one the bundle merely stated. */
1223
+ var li = leafIsFromProof ? _numOf(te.logIndex)
1224
+ : (proofLeaf <= _MAX_SAFE_BIGINT ? _numberOf(proofLeaf) : NaN);
1225
+ var signer = cpResult.signers.length ? cpResult.signers[0] : null;
900
1226
  return {
901
- integratedTime: t, logIndex: _isFiniteNumber(li) ? li : null,
1227
+ integratedTime: integratedTime, logIndex: _isFiniteNumber(li) ? li : null,
902
1228
  logId: te.logId && te.logId.keyId ? _bufToString(_b64(te.logId.keyId, "logId.keyId"), "hex") : null,
903
1229
  artifactHash: artifactHash,
1230
+ logOrigin: checkpoint.origin,
1231
+ treeSize: checkpoint.treeSize,
1232
+ checkpointKeyId: signer ? _bufToString(signer.keyId, "hex") : null,
904
1233
  };
905
1234
  }
906
1235
 
907
1236
 
908
- function _dn(cert, which) { return cert[which] && cert[which].dn; }
1237
+ /** @internal The instant an RFC 3161 timestamp token establishes over the signature bytes, in
1238
+ * milliseconds, or null when the bundle carries none or the caller pinned no timestamp-authority
1239
+ * anchor. Rekor v2 issues no signed entry timestamp, so for such a bundle this is the only attested
1240
+ * instant there is and a Fulcio certificate that lives about ten minutes cannot be dated without it.
1241
+ *
1242
+ * Every token offered is tried and every one that verifies contributes its instant, because a bundle
1243
+ * may carry a token from more than one authority and only the ones whose anchor the caller pinned can
1244
+ * be checked. A token that does not verify is passed over rather than refused: it is a statement from
1245
+ * an authority this caller did not pin, which says nothing either way. With no token verifying, the
1246
+ * caller is left with no attested instant and the verdict says so. */
1247
+ async function _timestampInstants(vm, sigBytes, tsaRoots) {
1248
+ if (!tsaRoots) return null;
1249
+ var anchors = _normTsaAnchors(tsaRoots);
1250
+ if (!anchors.length) {
1251
+ throw _err("sigstore/bad-input", "opts.tsaRoots names no anchor; omit it rather than passing an " +
1252
+ "empty list, which would read as a timestamp policy that checks nothing");
1253
+ }
1254
+ var tvd = vm.timestampVerificationData;
1255
+ if (!tvd || typeof tvd !== "object") return [];
1256
+ var tokens = _isArray(tvd.rfc3161Timestamps) ? tvd.rfc3161Timestamps : [];
1257
+ if (tokens.length > C.LIMITS.TLOG_MAX_COUNT) {
1258
+ throw _err("sigstore/bad-bundle", "a bundle offers at most " + C.LIMITS.TLOG_MAX_COUNT +
1259
+ " RFC 3161 timestamps; this one offers " + tokens.length);
1260
+ }
1261
+ /** @internal EVERY verified token's instant is returned, not the first. A bundle may carry a token over
1262
+ * an archived signature alongside the one made at signing, and the later instant can fall outside the
1263
+ * ephemeral certificate's validity while the earlier proves signing inside it. Stopping at the first
1264
+ * refused such a bundle for `sigstore/chain-invalid`, which is a denial of a sound bundle rather than a
1265
+ * security property; the caller tries each. Duplicates are dropped so two tokens naming one second cost
1266
+ * one attempt. */
1267
+ var instants = [];
1268
+ for (var i = 0; i < tokens.length; i++) {
1269
+ var t = tokens[i];
1270
+ var der = t && typeof t.signedTimestamp === "string"
1271
+ ? _b64(t.signedTimestamp, "rfc3161Timestamps[" + i + "].signedTimestamp") : null;
1272
+ if (der === null) continue;
1273
+ var res;
1274
+ try { res = await _tspVerify(der, sigBytes, { trustAnchors: anchors }); }
1275
+ catch (_e) { continue; } // allow:swallow-unverified a token this caller cannot check says nothing; the next is tried and a bundle with none verifying is refused for want of an instant
1276
+ if (!res || res.valid !== true || res.trusted !== true) continue;
1277
+ var ms = guard.time.instantOf(res.genTime);
1278
+ if (!_isFiniteNumber(ms)) continue;
1279
+ var seen = false;
1280
+ for (var j = 0; j < instants.length; j++) if (instants[j] === ms) { seen = true; break; }
1281
+ if (!seen) guard.list.append(instants, ms);
1282
+ }
1283
+ return instants;
1284
+ }
1285
+
1286
+ /** @internal A timestamp-authority anchor from the same `{ der }` shape the other trust material
1287
+ * takes, converted to the anchor `pki.tsp.verify` names. A caller pins one kind of thing. */
1288
+ function _normTsaAnchors(tsaRoots) {
1289
+ var list = _isArray(tsaRoots) ? tsaRoots : [tsaRoots];
1290
+ return _map(list, function (r, i) {
1291
+ var der = _isBuffer(r) ? r : (r && (r.der || r.rawBytes));
1292
+ if (!_isBuffer(der)) throw _err("sigstore/bad-input", "tsaRoots[" + i + "] must be a DER Buffer or { der }");
1293
+ var cert = _parseCert(der, "timestamp-authority root [" + i + "]");
1294
+ return { name: cert.subject, publicKey: cert.subjectPublicKeyInfo.bytes,
1295
+ algorithm: cert.subjectPublicKeyInfo.algorithm.oid };
1296
+ });
1297
+ }
1298
+
909
1299
 
910
1300
  function _normRoots(fulcioRoots) {
911
1301
  return _map(fulcioRoots, function (r, i) {
912
1302
  var der = _isBuffer(r) ? r : (r && (r.der || r.rawBytes));
913
1303
  if (!_isBuffer(der)) throw _err("sigstore/bad-input", "fulcioRoots[" + i + "] must be a DER Buffer or { der, validFor }");
914
- return { cert: _parseCert(der, "Fulcio CA root [" + i + "]"), validFor: (r && r.validFor) || null };
1304
+ return { cert: _parseCert(der, "Fulcio CA root [" + i + "]"), validFor: _normWindow(r && r.validFor) };
915
1305
  });
916
1306
  }
917
1307
 
918
- function _pathToAnchor(leaf, anchor, intBySubject) {
919
- var anchorSubj = _dn(anchor, "subject");
920
- /** @internal Issuers are collected leaf-first and the path is assembled once, so no element is
921
- * ever moved to a new index: an indexed setter has nothing to intercept and cannot replace the
922
- * leaf the preceding signature checks authenticated. */
923
- var above = [], cur = leaf, seen = _create(null), guardN = 0;
924
- function assemble() {
925
- var path = [];
926
- for (var k = above.length - 1; k >= 0; k--) guard.list.append(path, above[k]);
927
- guard.list.append(path, leaf);
928
- return path;
929
- }
930
- while (guardN++ < 16) {
931
- var issuerDn = _dn(cur, "issuer");
932
- if (issuerDn === anchorSubj) return assemble();
933
- var cands = intBySubject[issuerDn] || [];
934
- var next = null;
935
- for (var j = 0; j < cands.length; j++) {
936
- if (cands[j] === anchor || seen[_dn(cands[j], "subject")]) continue;
937
- next = cands[j]; break;
938
- }
939
- if (!next) return null;
940
- seen[_dn(next, "subject")] = 1;
941
- guard.list.append(above, next);
942
- cur = next;
943
- }
944
- return null;
945
- }
946
-
947
1308
  var OID_SCT_LIST = oid.byName("signedCertificateTimestampList");
948
1309
 
949
1310
  /** @internal The certificate-transparency logs a caller pins, in the shape the Rekor keys take: an
@@ -956,7 +1317,24 @@ function _normCtLogs(ctLogs) {
956
1317
  if (l === null || typeof l !== "object") throw _err("sigstore/bad-input", "ctLogs[" + i + "] must be { keyId, spki, validFor? }");
957
1318
  if (!_isBuffer(l.keyId)) throw _err("sigstore/bad-input", "ctLogs[" + i + "].keyId must be the log id as a Buffer");
958
1319
  if (!_isBuffer(l.spki)) throw _err("sigstore/bad-input", "ctLogs[" + i + "].spki must be the log's SubjectPublicKeyInfo DER");
959
- return { keyId: l.keyId, spki: l.spki, validFor: l.validFor || null };
1320
+ return { keyId: _maybeSnapshot(l.keyId, "ctLogs[" + i + "].keyId"),
1321
+ spki: _maybeSnapshot(l.spki, "ctLogs[" + i + "].spki"), validFor: _normWindow(l.validFor) };
1322
+ });
1323
+ }
1324
+
1325
+ /** @internal The pinned Rekor keys, copied record by record before the first await. `snapshotOptions`
1326
+ * copies the array reference, so each record is still the caller's own object, and the checkpoint and the
1327
+ * signed entry timestamp are verified on either side of an await: a caller replacing a record's `spki`
1328
+ * between them could have the two verified under different key material filed under one identifier, and
1329
+ * the verdict would then attest an entry no single pinned key authenticated. Every field either check
1330
+ * reads is copied here, and the window is resolved with it. */
1331
+ function _normRekorKeys(rekorKeys) {
1332
+ if (rekorKeys == null) return [];
1333
+ if (!_isArray(rekorKeys)) throw _err("sigstore/bad-input", "rekorKeys must be an array of { keyId, spki, validFor? }");
1334
+ return _map(rekorKeys, function (k, i) {
1335
+ if (k === null || typeof k !== "object") throw _err("sigstore/bad-input", "rekorKeys[" + i + "] must be { keyId, spki, validFor? }");
1336
+ return { keyId: _maybeSnapshot(k.keyId, "rekorKeys[" + i + "].keyId"),
1337
+ spki: _maybeSnapshot(k.spki, "rekorKeys[" + i + "].spki"), validFor: _normWindow(k.validFor) };
960
1338
  });
961
1339
  }
962
1340
 
@@ -965,9 +1343,10 @@ function _normCtLogs(ctLogs) {
965
1343
  * `pki.ct.x509CertEntry` reconstructs exactly that entry from the final certificate, and each receipt
966
1344
  * is checked against the log the caller pinned for it. At least one must verify, which is the sec. 3.3
967
1345
  * floor; a receipt from a log the caller did not pin counts for nothing. */
968
- async function _verifyEmbeddedScts(leaf, leafDer, issuer, ctLogs, timeMs, atMs) {
969
- if (ctLogs === undefined || ctLogs === null) return { checked: false, validScts: 0 };
970
- var logs = _normCtLogs(ctLogs);
1346
+ async function _verifyEmbeddedScts(leaf, leafDer, issuer, logs, timeMs, atMs) {
1347
+ /** @internal `logs` arrives ALREADY normalized, from before the first await, for the reason given at
1348
+ * `_verifyChain`: a list read after a yield is the caller's array as it stands then. */
1349
+ if (logs === undefined || logs === null) return { checked: false, validScts: 0 };
971
1350
  var ext = null;
972
1351
  var exts = leaf.extensions || [];
973
1352
  for (var i = 0; i < exts.length; i++) { if (exts[i].oid === OID_SCT_LIST) { ext = exts[i]; break; } }
@@ -1004,7 +1383,7 @@ async function _verifyEmbeddedScts(leaf, leafDer, issuer, ctLogs, timeMs, atMs)
1004
1383
  /** @internal A receipt the verifier refuses structurally counted as verified would be the whole leg
1005
1384
  * lost, so a fault is an unverified receipt and the next one is still tried. */
1006
1385
  function _sctVerifies(entry, sct, spki) {
1007
- return ct.verifySct(entry, sct, spki).then(function (ok) { return ok === true; }, function () { return false; });
1386
+ return _ctVerifySct(entry, sct, spki).then(function (ok) { return ok === true; }, function () { return false; });
1008
1387
  }
1009
1388
 
1010
1389
  /** @internal A receipt's own instant, in milliseconds. The parser's `timestampMs` is this same value
@@ -1019,38 +1398,87 @@ function _ctLogFor(logs, logIdBytes, timeMs) {
1019
1398
  return null;
1020
1399
  }
1021
1400
 
1022
- async function _verifyChain(leaf, chainDers, fulcioRoots, timeMs) {
1023
- var roots = _normRoots(fulcioRoots);
1401
+ /** @internal `roots` arrives ALREADY normalized, from before the first await in `verifyBundle`. Normalizing
1402
+ * here read the caller's array after a yield, so a caller could add an anchor while verification was
1403
+ * pending and decide which CA is trusted: an empty list that grows during the timestamp check would have
1404
+ * passed a chain the call was made without any anchor for. */
1405
+ async function _verifyChain(leaf, chainDers, roots, timeMs) {
1024
1406
  var bundleLinks = _map(_arraySlice(chainDers, 1), function (d, i) { return _parseCert(d, "bundle intermediate [" + i + "]"); });
1025
- var intBySubject = _create(null);
1026
- function addInt(c) { guard.list.append(intBySubject[_dn(c, "subject")] = intBySubject[_dn(c, "subject")] || [], c); }
1027
- _forEach(roots, function (r) { addInt(r.cert); });
1028
- _forEach(bundleLinks, addInt);
1029
- var candidates = _filter(roots, function (r) { return _inWindow(timeMs, r.validFor); });
1030
- var lastErr = _err("sigstore/chain-incomplete", "no caller-supplied Fulcio anchor (within its validity window) issues the chain");
1031
- for (var i = 0; i < candidates.length; i++) {
1032
- var anchor = candidates[i].cert;
1033
- var path = _pathToAnchor(leaf, anchor, intBySubject);
1034
- if (!path) continue;
1035
- var spki = anchor.subjectPublicKeyInfo;
1036
- try {
1037
- var res = await pathValidate.validate(path, {
1038
- time: new _Date(timeMs),
1039
- historicalMode: true,
1040
- trustAnchors: { name: anchor.subject, publicKey: spki.bytes, algorithm: spki.algorithm.oid, parameters: spki.algorithm.parameters },
1041
- requiredEku: ["codeSigning"],
1042
- });
1043
- /** @internal The path is anchor-proximal first and leaf last, so the certificate directly above
1044
- * the leaf issued it, and the anchor did when the leaf sits alone. Returning it here is what lets
1045
- * the certificate-transparency leg reconstruct the logged entry without resolving the issuer a
1046
- * second time, from a chain this leg has already validated. */
1047
- if (res.valid) return { issuer: path.length > 1 ? path[path.length - 2] : anchor };
1048
- lastErr = _err("sigstore/chain-invalid", "the Fulcio certificate chain is not valid as of the log time");
1049
- } catch (e) {
1050
- lastErr = _err("sigstore/chain-invalid", "the Fulcio certificate chain failed validation: " + e.message, e);
1407
+ var inWindow = _filter(roots, function (r) { return _inWindow(timeMs, r.validFor); });
1408
+ if (!inWindow.length) {
1409
+ throw _err("sigstore/chain-incomplete", "no caller-supplied Fulcio anchor (within its validity window) issues the chain");
1410
+ }
1411
+ /** @internal The search is `pki.path.build`, which owns it. This module walked the pool itself, and the
1412
+ * walk decided which candidates a bound was spent on: a rotated CA reissues under ONE subject DN, so
1413
+ * several bundle certificates chain by name and only one of them signed the leaf. The local walk capped
1414
+ * COLLECTED PATHS and ordered candidates by name alone, so where more than that many same-subject
1415
+ * certificates were issued directly by the anchor the cap was spent on whichever the bundle listed
1416
+ * first and the one that signed the leaf was never validated: a bundle holding a valid path was
1417
+ * reported `chain-invalid`. `build` ranks candidates by the RFC 4158 sec. 3.5 heuristics, the key
1418
+ * identifier among them, EXCLUDING none, and bounds the search by total candidate expansions rather
1419
+ * than by how many paths it has collected, so a later candidate stays reachable and an exhausted
1420
+ * budget is reported as `path/build-limit` rather than as a verdict about the chain. Every assembled
1421
+ * path still flows through `pki.path.validate`, so this delegates the search and duplicates no check.
1422
+ * The anchors go into the pool as well as the trust store, because a cross-signed root appears in a
1423
+ * chain as an intermediate under another anchor. */
1424
+ var anchorTuples = _map(inWindow, function (r) {
1425
+ var spki = r.cert.subjectPublicKeyInfo;
1426
+ return { name: r.cert.subject, publicKey: spki.bytes, algorithm: spki.algorithm.oid, parameters: spki.algorithm.parameters };
1427
+ });
1428
+ var pool = guard.list.snapshot(bundleLinks);
1429
+ _forEach(inWindow, function (r) { guard.list.append(pool, r.cert); });
1430
+ /** @internal A pool past the builder's ceiling is refused HERE, because the builder reports it as a
1431
+ * `path/bad-input` and a bundle carrying too many certificates to search is not a bad argument from
1432
+ * this verb nor a verdict that its chain is invalid: it is a chain this call did not establish. Read
1433
+ * as the builder reads it, against the same ceiling, so the two cannot disagree about which pools
1434
+ * are searchable. */
1435
+ if (pool.length > C.LIMITS.PATH_BUILD_MAX_CANDIDATES) {
1436
+ throw _err("sigstore/chain-incomplete", "the bundle and the supplied anchors together name " +
1437
+ pool.length + " certificates, past the " + C.LIMITS.PATH_BUILD_MAX_CANDIDATES +
1438
+ " the path search will consider, so no chain to an anchor was established");
1439
+ }
1440
+
1441
+ var built;
1442
+ try {
1443
+ built = await _pathBuild(leaf, {
1444
+ candidates: pool,
1445
+ trustAnchors: anchorTuples,
1446
+ time: new _Date(timeMs),
1447
+ historicalMode: true,
1448
+ requiredEku: ["codeSigning"],
1449
+ });
1450
+ } catch (e) {
1451
+ if (e && e.code === "path/no-path") {
1452
+ throw _err("sigstore/chain-incomplete", "no caller-supplied Fulcio anchor (within its validity window) issues the chain", e);
1453
+ }
1454
+ /** @internal A reached search bound is not a verdict about the chain. `build` stops looking when a
1455
+ * bundle's candidates expand past its cap, which a fan-out of same-subject certificates reaching no
1456
+ * anchor does, and this leg then knows no path was established rather than that one was refused. It
1457
+ * reports the incomplete code for that, with the bound as the cause so a caller that wants to tell
1458
+ * the two apart reads `path/build-limit` there. */
1459
+ if (e && e.code === "path/build-limit") {
1460
+ throw _err("sigstore/chain-incomplete", "no chain to a caller-supplied Fulcio anchor was established before the path search reached its candidate bound", e);
1461
+ }
1462
+ throw _err("sigstore/chain-invalid", "the Fulcio certificate chain failed validation: " + e.message, e);
1463
+ }
1464
+ if (!built.valid) {
1465
+ throw _err("sigstore/chain-invalid", "the Fulcio certificate chain is not valid as of the log time");
1466
+ }
1467
+ /** @internal `path` is anchor-proximal first and leaf last with the anchor excluded, so the certificate
1468
+ * directly above the leaf issued it, and the anchor did when the leaf sits alone. Returning it here is
1469
+ * what lets the certificate-transparency leg reconstruct the logged entry without resolving the issuer a
1470
+ * second time, from a chain this leg has already validated. The anchor is matched back to its
1471
+ * CERTIFICATE by its public key rather than by its name, because rotation is exactly the case where two
1472
+ * anchors carry one name and the transparency leg hashes the issuer's KEY. */
1473
+ var path = built.path;
1474
+ if (path.length > 1) return { issuer: path[path.length - 2] };
1475
+ var anchorKey = built.trustAnchor && built.trustAnchor.publicKey;
1476
+ for (var i = 0; i < inWindow.length; i++) {
1477
+ if (_isBuffer(anchorKey) && _bufferEquals(inWindow[i].cert.subjectPublicKeyInfo.bytes, anchorKey)) {
1478
+ return { issuer: inWindow[i].cert };
1051
1479
  }
1052
1480
  }
1053
- throw lastErr;
1481
+ throw _err("sigstore/chain-invalid", "the validated chain names a trust anchor this call was not given");
1054
1482
  }
1055
1483
 
1056
1484
 
@@ -1118,7 +1546,7 @@ function _fulcioExtValue(ext, leafArc) {
1118
1546
 
1119
1547
  var IDENTITY_FIELDS = ["san", "issuer", "sourceRepositoryURI"];
1120
1548
  var IDENTITY_KEYS = _assign(_create(null), { san: 1, issuer: 1, sourceRepositoryURI: 1 });
1121
- var _VERIFY_BUNDLE_OPTS = _assign(_create(null), { fulcioRoots: 1, rekorKeys: 1, ctLogs: 1, identity: 1, predicateType: 1, time: 1, artifact: 1 });
1549
+ var _VERIFY_BUNDLE_OPTS = _assign(_create(null), { fulcioRoots: 1, rekorKeys: 1, ctLogs: 1, identity: 1, predicateType: 1, time: 1, artifact: 1, tsaRoots: 1 });
1122
1550
 
1123
1551
  /** @internal The bytes a message signature covers, taken once. There is no shape here in which a
1124
1552
  * caller hands over a precomputed digest: the arm's own field comment forbids using one as a
@@ -1165,6 +1593,9 @@ function _statement(payload, payloadType, expectedPredicate) {
1165
1593
  if (payloadType !== "application/vnd.in-toto+json") {
1166
1594
  throw _err("sigstore/bad-statement", "unsupported DSSE payloadType: " + payloadType);
1167
1595
  }
1596
+ /** @internal NO member policy. An in-toto `predicate` is arbitrary JSON by specification, so no member
1597
+ * name in a statement means an integer, and naming one would constrain a predicate field for merely
1598
+ * sharing a name with the bundle's log metadata. */
1168
1599
  var st = _jsonParse(payload, "sigstore/bad-statement", "in-toto statement");
1169
1600
  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");
1170
1601
  if (!_isArray(st.subject) || !st.subject.length) throw _err("sigstore/bad-statement", "the in-toto statement has no subject");
@@ -1238,6 +1669,20 @@ function _statement(payload, payloadType, expectedPredicate) {
1238
1669
  * The verdict has one shape for both arms. `contentType` names the arm, and the
1239
1670
  * fields the other arm has nothing to report are present and `null`.
1240
1671
  *
1672
+ * A Rekor v2 entry (`hashedrekord` v0.0.2) is read on either arm. Rekor v2 supports no DSSE entry
1673
+ * type, so a DSSE signature is logged as a `hashedrekord` over the hash of its signing preimage, and
1674
+ * the entry is bound to the bundle by three separate comparisons: the signature it records, the
1675
+ * certificate that made it, and that digest. Where the entry states `keyDetails`, that statement is
1676
+ * held to the leaf key rather than read in its place.
1677
+ *
1678
+ * Rekor v2 issues no signed entry timestamp and writes `integratedTime` as zero, so it is ignored and
1679
+ * the instant comes from an RFC 3161 timestamp token over the signature bytes, verified against the
1680
+ * anchors `opts.tsaRoots` pins. With neither a token this caller can check nor `opts.time`, the
1681
+ * verdict is `sigstore/no-attested-time`: a Fulcio certificate lives about ten minutes and cannot be
1682
+ * validated against an unattested instant. `timestampSource` reports which of the three dated the
1683
+ * certificate, and `logOrigin`, `treeSize` and `checkpointKeyId` report what the verified checkpoint
1684
+ * said.
1685
+ *
1241
1686
  * @opts
1242
1687
  * fulcioRoots: Array, // the Fulcio CA anchors: a DER Buffer or { der, validFor } each
1243
1688
  * rekorKeys: Array, // [{ keyId, spki, validFor? }] the Rekor log public keys
@@ -1245,7 +1690,8 @@ function _statement(payload, payloadType, expectedPredicate) {
1245
1690
  * identity: object, // optional policy: { san, issuer, sourceRepositoryURI }; at least one required when present
1246
1691
  * predicateType: string, // optional: require this in-toto predicateType (e.g. the SLSA URI); dsse_envelope bundles only
1247
1692
  * artifact: BufferSource, // the bytes a message_signature covers; required for that arm, refused for a dsse_envelope
1248
- * time: Date, // optional check-date override (default: the Rekor integratedTime)
1693
+ * tsaRoots: Array, // optional timestamp-authority anchors: a DER Buffer or { der } each; required to date a Rekor v2 bundle, which carries no signed entry timestamp
1694
+ * time: Date, // optional check-date override (default: the Rekor integratedTime, or an RFC 3161 token's genTime on a v2 entry)
1249
1695
  *
1250
1696
  * @example
1251
1697
  * // requires: `bundle` from cosign / npm provenance, and `sigstoreTrust` built from
@@ -1272,8 +1718,13 @@ async function verifyBundle(bundle, opts) {
1272
1718
  * answer: one that passed a refusal could be reported by the verdict as a different value, and a
1273
1719
  * verdict that names an option is naming the value the check ran on. */
1274
1720
  var o = guard.identifier.snapshotOptions(opts, _VERIFY_BUNDLE_OPTS);
1275
- var rekorKeys = o.rekorKeys || [];
1276
- var fulcioRoots = o.fulcioRoots || [];
1721
+ var rekorKeys = _normRekorKeys(o.rekorKeys);
1722
+ /** @internal Every pinned trust list is normalized HERE, before the first await, because each is read
1723
+ * again after one: the anchors by the chain check, the logs by the transparency check. Left to the
1724
+ * point of use, the list read is the caller's own array as it stands then, and material added while
1725
+ * verification is pending would decide a check the call was made without it. */
1726
+ var fulcioRoots = _normRoots(o.fulcioRoots || []);
1727
+ var ctLogs = o.ctLogs === undefined || o.ctLogs === null ? o.ctLogs : _normCtLogs(o.ctLogs);
1277
1728
 
1278
1729
  /** @internal Each option belongs to one arm, and one supplied for the other is refused rather than
1279
1730
  * passed over. An ignored option reads as a check that ran: opts.artifact on a DSSE bundle names
@@ -1328,6 +1779,13 @@ async function verifyBundle(bundle, opts) {
1328
1779
  * and when none does the last failure is what the caller is told. */
1329
1780
  var haveCallerTime = guard.time.isDate(o.time);
1330
1781
  var callerTime = haveCallerTime ? guard.time.instantOf(o.time) : null;
1782
+ /** @internal An RFC 3161 token dates itself and chains to anchors the caller pins, so it is
1783
+ * established BEFORE any entry is tried: it needs no log time, and a Rekor v2 entry needs an
1784
+ * instant before its checkpoint key's validity window can be judged. The token must cover the
1785
+ * SIGNATURE bytes, which is what dates the ephemeral certificate that made them; a token over the
1786
+ * payload would date the thing signed rather than the signing. */
1787
+ var tsaMsList = null;
1788
+ if (!haveCallerTime) tsaMsList = await _timestampInstants(vm, derSig, o.tsaRoots);
1331
1789
  var chainDers = _chainDers(vm);
1332
1790
  /** @internal A caller-pinned instant is the same for every candidate, so the chain it decides is
1333
1791
  * computed on the first candidate and reused rather than rebuilt per entry. Without one the instant
@@ -1335,30 +1793,80 @@ async function verifyBundle(bundle, opts) {
1335
1793
  * beside it: a candidate that reaches them and passes is selected immediately, so no later
1336
1794
  * candidate could read the result. */
1337
1795
  var pinnedChain = null;
1796
+ /** @internal A candidate is an ENTRY paired with the instant to date it by, because both decide which
1797
+ * entry is selected and either can be the one that fails. A caller's own instant gives one pair per
1798
+ * entry. Otherwise every verified timestamp gives a pair, since the token a bundle lists first may fall
1799
+ * outside the certificate's validity while a later one proves signing inside it; trying only the first
1800
+ * refused such a bundle. The product is bounded so a hostile bundle cannot multiply the work: the
1801
+ * existing per-list cap is the budget for the pairs too. */
1802
+ var instantsToTry = (haveCallerTime || tsaMsList === null || tsaMsList.length === 0) ? [null] : tsaMsList;
1803
+ if (tlogs.length * instantsToTry.length > C.LIMITS.TLOG_MAX_COUNT) {
1804
+ throw _err("sigstore/bad-bundle", "a bundle offering " + tlogs.length + " transparency-log entries " +
1805
+ "and " + instantsToTry.length + " verified timestamps asks for " + (tlogs.length * instantsToTry.length) +
1806
+ " combinations; at most " + C.LIMITS.TLOG_MAX_COUNT + " are tried");
1807
+ }
1808
+ var attempts = [];
1809
+ for (var ei = 0; ei < tlogs.length; ei++) {
1810
+ for (var xi = 0; xi < instantsToTry.length; xi++) {
1811
+ guard.list.append(attempts, { entry: tlogs[ei], tsaMs: instantsToTry[xi] });
1812
+ }
1813
+ }
1338
1814
  var rekor = null, lastErr = null, artifactComputed = null, sctResult = null;
1339
- for (var ti = 0; ti < tlogs.length; ti++) {
1340
- if (!tlogs[ti] || typeof tlogs[ti] !== "object") { lastErr = _err("sigstore/bad-bundle", "a transparency-log entry is not an object"); continue; }
1815
+ /** @internal The artifact's digest under one algorithm, computed once for that algorithm. The
1816
+ * comparison below is per ATTEMPT and the artifact is the caller's whole file, so an attempt that
1817
+ * failed after the comparison sent the next one over those bytes again: at the cap of 32 attempts a
1818
+ * 100 MB artifact was hashed 32 times. The same bytes under the same algorithm are the same digest,
1819
+ * so each algorithm an entry names costs one pass. */
1820
+ var artifactDigests = intrinsic.create(null);
1821
+ function _artifactDigest(nodeName) {
1822
+ if (!intrinsic.hasOwn(artifactDigests, nodeName)) {
1823
+ artifactDigests[nodeName] = nodeCrypto.createHash(nodeName).update(artifact).digest();
1824
+ }
1825
+ return artifactDigests[nodeName];
1826
+ }
1827
+ for (var ti = 0; ti < attempts.length; ti++) {
1828
+ var entry = attempts[ti].entry, tsaMs = attempts[ti].tsaMs;
1829
+ if (!entry || typeof entry !== "object") { lastErr = _err("sigstore/bad-bundle", "a transparency-log entry is not an object"); continue; }
1341
1830
  try {
1342
- var candidate = isMsg ? _verifyRekorMessage(tlogs[ti], msParsed, rekorKeys, leafDer)
1343
- : _verifyRekor(tlogs[ti], env, rekorKeys, leafDer);
1831
+ var externalMs = haveCallerTime ? callerTime : (tsaMs === null ? NaN : tsaMs);
1832
+ var candidate = isMsg ? await _verifyRekorMessage(entry, msParsed, rekorKeys, leafDer, externalMs)
1833
+ : await _verifyRekor(entry, env, rekorKeys, leafDer, env.payloadType, payload, externalMs);
1344
1834
  var candidateDigest = null;
1345
1835
  if (isMsg) {
1346
- candidateDigest = nodeCrypto.createHash(candidate.artifactHash.node).update(artifact).digest();
1836
+ candidateDigest = _artifactDigest(candidate.artifactHash.node);
1347
1837
  if (!guard.crypto.constantTimeEqual(candidateDigest, _bufferFrom(candidate.artifactHash.value, "hex"))) {
1348
1838
  throw _err("sigstore/artifact-mismatch", "the artifact does not match the one the transparency-log entry records; its " +
1349
1839
  candidate.artifactHash.algorithm + " digest is " + _bufToString(candidateDigest, "hex") + " and the entry names " + candidate.artifactHash.value);
1350
1840
  }
1351
1841
  }
1352
- var candidateTime = haveCallerTime ? callerTime : C.TIME.seconds(candidate.integratedTime);
1842
+ /** @internal Three sources, in the order of what they establish. The caller's own instant wins
1843
+ * where given. Otherwise an entry carrying a signed entry timestamp dates itself. An entry that
1844
+ * does not -- a Rekor v2 one -- is dated by the timestamp token, and with neither there is no
1845
+ * attested instant and nothing can date the ephemeral certificate. */
1846
+ var candidateSource, candidateTime, candidateBound;
1847
+ if (haveCallerTime) {
1848
+ candidateSource = "caller"; candidateTime = callerTime; candidateBound = callerTime;
1849
+ } else if (candidate.integratedTime !== null) {
1850
+ candidateSource = "set";
1851
+ candidateTime = C.TIME.seconds(candidate.integratedTime);
1852
+ /** @internal The authenticated log-entry time's whole second is inclusive: a certificate
1853
+ * logged in the same second as its entry is not dated after it, and nothing beyond that
1854
+ * second is accepted. */
1855
+ candidateBound = candidateTime + 999;
1856
+ } else if (tsaMs !== null) {
1857
+ candidateSource = "rfc3161"; candidateTime = tsaMs; candidateBound = tsaMs;
1858
+ } else {
1859
+ throw _err("sigstore/no-attested-time", "the transparency-log entry carries no signed entry " +
1860
+ "timestamp and the bundle carries no RFC 3161 timestamp this caller can check, so no " +
1861
+ "attested instant dates the ephemeral Fulcio certificate: pin opts.tsaRoots for the " +
1862
+ "timestamp authority, or supply opts.time");
1863
+ }
1353
1864
  var candidateChain = pinnedChain !== null ? pinnedChain
1354
1865
  : await _verifyChain(leaf, chainDers, fulcioRoots, candidateTime);
1355
1866
  if (haveCallerTime) pinnedChain = candidateChain;
1356
- /** @internal With no caller instant the reference is the authenticated log-entry time, whose
1357
- * whole second is inclusive: a certificate logged in the same second as its entry is not dated
1358
- * after it, and nothing beyond that second is accepted. */
1359
- var candidateBound = haveCallerTime ? candidateTime : C.TIME.seconds(candidate.integratedTime) + 999;
1360
- var candidateScts = await _verifyEmbeddedScts(leaf, leafDer, candidateChain.issuer, o.ctLogs, candidateTime, candidateBound);
1867
+ var candidateScts = await _verifyEmbeddedScts(leaf, leafDer, candidateChain.issuer, ctLogs, candidateTime, candidateBound);
1361
1868
  rekor = candidate;
1869
+ rekor.timestampSource = candidateSource;
1362
1870
  artifactComputed = candidateDigest;
1363
1871
  sctResult = candidateScts;
1364
1872
  break;
@@ -1378,8 +1886,12 @@ async function verifyBundle(bundle, opts) {
1378
1886
  var computed = artifactComputed;
1379
1887
  artifactDigest = _bufToString(computed, "hex");
1380
1888
  if (msParsed.digest !== null) {
1889
+ /** @internal Through the same memo as the attempts above, so the whole call reads the artifact
1890
+ * once per algorithm. Matching the selected entry's algorithm was already answered from
1891
+ * `computed`; a messageDigest naming a DIFFERENT algorithm went around that to hash the artifact
1892
+ * again, and an earlier attempt that failed under that same algorithm had already hashed it. */
1381
1893
  var againstClaim = msParsed.digestAlg.node === rekor.artifactHash.node ? computed
1382
- : nodeCrypto.createHash(msParsed.digestAlg.node).update(artifact).digest();
1894
+ : _artifactDigest(msParsed.digestAlg.node);
1383
1895
  if (!guard.crypto.constantTimeEqual(againstClaim, msParsed.digest)) {
1384
1896
  throw _err("sigstore/artifact-mismatch", "the artifact does not match the messageDigest the bundle carries");
1385
1897
  }
@@ -1418,6 +1930,10 @@ async function verifyBundle(bundle, opts) {
1418
1930
  integratedTime: integratedTime,
1419
1931
  logIndex: rekor.logIndex,
1420
1932
  logId: rekor.logId,
1933
+ logOrigin: rekor.logOrigin,
1934
+ treeSize: rekor.treeSize,
1935
+ checkpointKeyId: rekor.checkpointKeyId,
1936
+ timestampSource: rekor.timestampSource,
1421
1937
  });
1422
1938
  }
1423
1939