@blamejs/pki 0.8.30 → 0.8.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +411 -0
- package/README.md +67 -16
- package/bin/pki.js +716 -30
- package/examples/issue-a-certificate.js +90 -0
- package/examples/post-quantum.js +66 -0
- package/examples/read-a-certificate.js +56 -0
- package/examples/revoke-and-check.js +86 -0
- package/examples/sign-and-verify-cms.js +63 -0
- package/index.js +18 -5
- package/lib/acme.js +223 -74
- package/lib/alt-sig.js +441 -0
- package/lib/asn1-der.js +48 -25
- package/lib/attrcert-sign.js +27 -19
- package/lib/cbor-det.js +24 -14
- package/lib/cmc-build.js +9 -2
- package/lib/cmc-verify.js +14 -3
- package/lib/cmp-build.js +17 -5
- package/lib/cmp-session.js +24 -11
- package/lib/cmp-verify.js +28 -19
- package/lib/cms-compress.js +3 -1
- package/lib/cms-decrypt.js +102 -7
- package/lib/cms-encrypt.js +59 -7
- package/lib/cms-sign.js +31 -9
- package/lib/cms-verify.js +22 -18
- package/lib/composite-kem.js +30 -18
- package/lib/composite-sig.js +29 -14
- package/lib/constants.js +28 -0
- package/lib/crl-sign.js +111 -19
- package/lib/crmf-sign.js +167 -15
- package/lib/csr-sign.js +117 -6
- package/lib/ct.js +939 -48
- package/lib/est.js +29 -16
- package/lib/framework-error.js +12 -0
- package/lib/guard-async.js +1 -1
- package/lib/guard-bytes.js +103 -20
- package/lib/guard-crypto.js +205 -3
- package/lib/guard-encoding.js +6 -1
- package/lib/guard-identifier.js +55 -8
- package/lib/guard-intrinsic.js +43 -0
- package/lib/guard-json.js +122 -9
- package/lib/guard-limits.js +12 -1
- package/lib/guard-list.js +63 -8
- package/lib/guard-name.js +167 -20
- package/lib/guard-range.js +43 -5
- package/lib/guard-text.js +41 -0
- package/lib/guard-verdict.js +39 -8
- package/lib/hpke.js +688 -42
- package/lib/http-transport.js +156 -4
- package/lib/identity-match.js +4 -4
- package/lib/inspect.js +448 -2
- package/lib/jose.js +93 -25
- package/lib/key.js +18 -13
- package/lib/lint.js +165 -1
- package/lib/merkle.js +233 -27
- package/lib/ocsp.js +27 -14
- package/lib/oid.js +14 -4
- package/lib/path-validate.js +143 -61
- package/lib/pkcs12-build.js +7 -2
- package/lib/pki-build.js +31 -7
- package/lib/possession.js +575 -0
- package/lib/related-cert.js +708 -0
- package/lib/revocation-fetch.js +6 -6
- package/lib/scep.js +15 -5
- package/lib/schema-c509.js +5 -2
- package/lib/schema-cmc.js +1 -1
- package/lib/schema-cmp.js +8 -8
- package/lib/schema-cms.js +2 -15
- package/lib/schema-csr.js +69 -0
- package/lib/schema-engine.js +10 -3
- package/lib/schema-pkix.js +267 -4
- package/lib/schema-tsp.js +7 -6
- package/lib/sign-scheme.js +22 -9
- package/lib/sigstore.js +684 -168
- package/lib/sleep.js +8 -1
- package/lib/smime.js +22 -1
- package/lib/tlog.js +1235 -0
- package/lib/tls-cert-compress.js +25 -8
- package/lib/tsp-sign.js +120 -35
- package/lib/tuf.js +1395 -0
- package/lib/validator-tpm.js +4 -1
- package/lib/verify-seam.js +7 -1
- package/lib/webauthn-mds.js +41 -4
- package/lib/webauthn.js +43 -25
- package/lib/webcrypto.js +48 -46
- package/lib/x509-sign.js +225 -19
- package/package.json +2 -1
- package/sbom.cdx.json +6 -6
package/lib/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
|
-
|
|
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
|
-
|
|
620
|
-
|
|
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
|
|
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 =
|
|
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
|
|
654
|
-
if (vf.end
|
|
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
|
-
|
|
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
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
var
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
var
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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": {
|
|
714
|
-
"hashedrekord@0.0.1": {
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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
|
|
853
|
-
* rather than in a copy per arm. */
|
|
854
|
-
function _verifyRekorMessage(te, ms, rekorKeys, leafDer) {
|
|
855
|
-
return _verifyRekorEntry(te, rekorKeys, leafDer, "messageSignature",
|
|
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,
|
|
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:
|
|
884
|
-
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:
|
|
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
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
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:
|
|
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
|
-
|
|
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)
|
|
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,
|
|
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,
|
|
969
|
-
|
|
970
|
-
|
|
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
|
|
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
|
-
|
|
1023
|
-
|
|
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
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
1340
|
-
|
|
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
|
|
1343
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
:
|
|
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
|
|