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