@blamejs/pki 0.6.13 → 0.6.15
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 +22 -1
- package/README.md +1 -1
- package/index.js +1 -1
- package/lib/acme.js +59 -815
- package/lib/asn1-der.js +7 -245
- package/lib/attrcert-sign.js +12 -260
- package/lib/attrcert-verify.js +1 -7
- package/lib/byte-reader.js +0 -8
- package/lib/byte-writer.js +0 -11
- package/lib/cbor-det.js +0 -54
- package/lib/cmc-build.js +15 -357
- package/lib/cmc-verify.js +6 -239
- package/lib/cmp-build.js +41 -317
- package/lib/cmp-session.js +73 -760
- package/lib/cmp-verify.js +32 -488
- package/lib/cms-compress.js +3 -32
- package/lib/cms-decrypt.js +29 -294
- package/lib/cms-encrypt.js +14 -234
- package/lib/cms-sign.js +29 -324
- package/lib/cms-verify.js +9 -500
- package/lib/composite-kem.js +2 -102
- package/lib/composite-sig.js +1 -77
- package/lib/constants.js +0 -249
- package/lib/crl-sign.js +9 -237
- package/lib/crl-verify.js +1 -10
- package/lib/crmf-sign.js +19 -198
- package/lib/crmf-verify.js +1 -12
- package/lib/csr-sign.js +3 -34
- package/lib/csr-verify.js +1 -12
- package/lib/ct.js +28 -253
- package/lib/edwards-point.js +10 -44
- package/lib/est.js +28 -764
- package/lib/framework-error.js +4 -182
- package/lib/guard-all.js +1 -58
- package/lib/guard-async.js +1 -36
- package/lib/guard-bytes.js +6 -650
- package/lib/guard-compress.js +5 -102
- package/lib/guard-crypto.js +1 -40
- package/lib/guard-der.js +2 -93
- package/lib/guard-encoding.js +2 -60
- package/lib/guard-header.js +1 -36
- package/lib/guard-identifier.js +7 -531
- package/lib/guard-intrinsic.js +1 -150
- package/lib/guard-json.js +10 -73
- package/lib/guard-limits.js +1 -77
- package/lib/guard-list.js +1 -49
- package/lib/guard-name.js +3 -209
- package/lib/guard-parsed.js +7 -335
- package/lib/guard-range.js +1 -71
- package/lib/guard-secret.js +1 -102
- package/lib/guard-text.js +1 -64
- package/lib/guard-time.js +1 -97
- package/lib/hpke.js +3 -105
- package/lib/http-digest.js +14 -142
- package/lib/http-retry-after.js +1 -47
- package/lib/http-transport.js +21 -131
- package/lib/inspect.js +30 -284
- package/lib/ip-utils.js +6 -47
- package/lib/jose.js +7 -142
- package/lib/key.js +2 -67
- package/lib/lint.js +9 -194
- package/lib/merkle.js +1 -27
- package/lib/mime.js +9 -82
- package/lib/ocsp-request-verify.js +1 -14
- package/lib/ocsp-verify.js +1 -82
- package/lib/ocsp.js +11 -169
- package/lib/oid.js +5 -337
- package/lib/path-validate.js +103 -1492
- package/lib/pbes2.js +2 -89
- package/lib/pkcs12-build.js +31 -379
- package/lib/pki-build.js +64 -290
- package/lib/rc2.js +2 -18
- package/lib/rfc3339.js +4 -27
- package/lib/scep.js +59 -233
- package/lib/schema-all.js +0 -122
- package/lib/schema-attrcert.js +4 -146
- package/lib/schema-c509.js +153 -802
- package/lib/schema-cmc.js +5 -220
- package/lib/schema-cmp.js +2 -223
- package/lib/schema-cms.js +13 -427
- package/lib/schema-crl.js +5 -68
- package/lib/schema-crmf.js +6 -167
- package/lib/schema-csr.js +1 -54
- package/lib/schema-csrattrs.js +2 -87
- package/lib/schema-engine.js +0 -152
- package/lib/schema-ocsp.js +0 -117
- package/lib/schema-pkcs12.js +2 -93
- package/lib/schema-pkcs8.js +3 -35
- package/lib/schema-pkix.js +24 -655
- package/lib/schema-smime.js +0 -54
- package/lib/schema-tsp.js +3 -99
- package/lib/schema-x509.js +4 -63
- package/lib/shbs.js +11 -95
- package/lib/sign-scheme.js +2 -97
- package/lib/sigstore.js +9 -169
- package/lib/sleep.js +2 -8
- package/lib/smime.js +15 -408
- package/lib/tls-cert-compress.js +0 -62
- package/lib/trust.js +19 -185
- package/lib/tsp-sign.js +16 -270
- package/lib/validator-all.js +1 -30
- package/lib/validator-attcert.js +2 -46
- package/lib/validator-cose.js +2 -153
- package/lib/validator-keydesc.js +1 -37
- package/lib/validator-sig.js +1 -46
- package/lib/validator-tls.js +1 -88
- package/lib/validator-tpm.js +19 -122
- package/lib/verify-seam.js +1 -24
- package/lib/webauthn-mds.js +3 -400
- package/lib/webauthn.js +12 -833
- package/lib/webcrypto.js +5 -485
- package/lib/x509-sign.js +4 -108
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/sigstore.js
CHANGED
|
@@ -44,40 +44,32 @@ function _err(code, message, cause) { return new SigstoreError(code, message, ca
|
|
|
44
44
|
var JSON_MAX = C.LIMITS.JSON_MAX_BYTES;
|
|
45
45
|
var SP = Buffer.from(" ", "ascii");
|
|
46
46
|
var DSSEV1 = Buffer.from("DSSEv1", "ascii");
|
|
47
|
-
// The checkpoint signature-line marker is EM DASH (U+2014) + space; built at runtime so the source
|
|
48
|
-
// file stays pure ASCII. The line is parsed by character scan (no regex): "<EM DASH> <keyId> <sig>",
|
|
49
|
-
// two whitespace-separated non-empty tokens, nothing after.
|
|
50
47
|
var _EM_DASH = String.fromCharCode(0x2014);
|
|
51
48
|
|
|
52
|
-
// The \S token scan uses the shared JS regex \s code-point predicate (pkix.isJsWhitespace) so the
|
|
53
|
-
// note-line grammar stays exactly the /\S/ complement without a second copy of the code-point set.
|
|
54
49
|
function _parseNoteSig(line) {
|
|
55
50
|
var prefix = _EM_DASH + " ";
|
|
56
51
|
if (line.slice(0, prefix.length) !== prefix) return null;
|
|
57
52
|
var p = prefix.length, n = line.length;
|
|
58
53
|
var t1s = p;
|
|
59
54
|
while (p < n && !pkix.isJsWhitespace(line.charCodeAt(p))) p += 1;
|
|
60
|
-
if (p === t1s) return null;
|
|
55
|
+
if (p === t1s) return null;
|
|
61
56
|
var keyId = line.slice(t1s, p);
|
|
62
|
-
if (line.charAt(p) !== " ") return null;
|
|
57
|
+
if (line.charAt(p) !== " ") return null;
|
|
63
58
|
p += 1;
|
|
64
59
|
var t2s = p;
|
|
65
60
|
while (p < n && !pkix.isJsWhitespace(line.charCodeAt(p))) p += 1;
|
|
66
61
|
if (p === t2s) return null;
|
|
67
62
|
var sig = line.slice(t2s, p);
|
|
68
|
-
if (p !== n) return null;
|
|
63
|
+
if (p !== n) return null;
|
|
69
64
|
return { keyId: keyId, sig: sig };
|
|
70
65
|
}
|
|
71
66
|
|
|
72
|
-
// A run of trailing "=" removed (the /=+$/ base64-padding strip).
|
|
73
67
|
function _stripTrailingEquals(s) {
|
|
74
68
|
var i = s.length;
|
|
75
69
|
while (i > 0 && s.charAt(i - 1) === "=") i -= 1;
|
|
76
70
|
return s.slice(0, i);
|
|
77
71
|
}
|
|
78
72
|
|
|
79
|
-
// The recognized bundle media types (draft: the inline ".v0.N+json" form and the
|
|
80
|
-
// "+json;version=0.N" form, N in 1..3) -- an explicit set in place of the anchored alternation regex.
|
|
81
73
|
var _BUNDLE_MEDIA_TYPES = {
|
|
82
74
|
"application/vnd.dev.sigstore.bundle.v0.1+json": 1,
|
|
83
75
|
"application/vnd.dev.sigstore.bundle.v0.2+json": 1,
|
|
@@ -90,7 +82,6 @@ function _isBundleMediaType(mt) {
|
|
|
90
82
|
return typeof mt === "string" && Object.prototype.hasOwnProperty.call(_BUNDLE_MEDIA_TYPES, mt);
|
|
91
83
|
}
|
|
92
84
|
|
|
93
|
-
// Parse untrusted JSON through the bounded, duplicate-member-rejecting guard.
|
|
94
85
|
function _jsonParse(input, code, label) {
|
|
95
86
|
return guard.json.parse(input, _err, {
|
|
96
87
|
maxBytes: JSON_MAX, maxDepth: 64,
|
|
@@ -98,7 +89,6 @@ function _jsonParse(input, code, label) {
|
|
|
98
89
|
});
|
|
99
90
|
}
|
|
100
91
|
|
|
101
|
-
// ---- DSSE PAE (secure-systems-lab/dsse protocol.md) --------------------------
|
|
102
92
|
|
|
103
93
|
/**
|
|
104
94
|
* @primitive pki.sigstore.pae
|
|
@@ -129,10 +119,7 @@ function pae(payloadType, payloadBytes) {
|
|
|
129
119
|
]);
|
|
130
120
|
}
|
|
131
121
|
|
|
132
|
-
// ---- strict base64 decode (fail-closed, canonical) ---------------------------
|
|
133
122
|
|
|
134
|
-
// Sigstore fields are standard (padded) base64; DSSE permits URL-safe. Decode
|
|
135
|
-
// either, but reject a non-canonical re-encoding (an encoding-malleability guard).
|
|
136
123
|
function _b64(s, label) {
|
|
137
124
|
if (typeof s !== "string") throw _err("sigstore/bad-bundle", label + " must be a base64 string");
|
|
138
125
|
var urlSafe = s.indexOf("-") !== -1 || s.indexOf("_") !== -1;
|
|
@@ -144,7 +131,6 @@ function _b64(s, label) {
|
|
|
144
131
|
return buf;
|
|
145
132
|
}
|
|
146
133
|
|
|
147
|
-
// ---- JSON bundle reader (fail-closed) ----------------------------------------
|
|
148
134
|
|
|
149
135
|
/**
|
|
150
136
|
* @primitive pki.sigstore.parseBundle
|
|
@@ -176,9 +162,6 @@ function parseBundle(input) {
|
|
|
176
162
|
throw _err("sigstore/bad-bundle", "bundle must be a JSON object, string, or Buffer");
|
|
177
163
|
}
|
|
178
164
|
if (!obj || typeof obj !== "object" || Array.isArray(obj)) throw _err("sigstore/bad-bundle", "bundle must be a JSON object");
|
|
179
|
-
// Accept only the bundle versions this verifier actually handles (v0.1-v0.3);
|
|
180
|
-
// a newer version is recognized-and-deferred, never accepted by an over-broad
|
|
181
|
-
// match (the inline `.v0.N+json` form and the `+json;version=0.N` form).
|
|
182
165
|
var mt = obj.mediaType;
|
|
183
166
|
if (!_isBundleMediaType(mt)) {
|
|
184
167
|
throw _err("sigstore/bad-bundle-version", "unsupported or unknown bundle media type: " + (mt === undefined ? "(none)" : mt));
|
|
@@ -200,10 +183,7 @@ function parseBundle(input) {
|
|
|
200
183
|
return obj;
|
|
201
184
|
}
|
|
202
185
|
|
|
203
|
-
// ---- leaf-key + signature primitives (node:crypto, sync) ---------------------
|
|
204
186
|
|
|
205
|
-
// A bundle X509Certificate element is `{ rawBytes: <base64 DER> }`; a null /
|
|
206
|
-
// non-object / non-string element fails closed, never a raw property deref.
|
|
207
187
|
function _certBytes(c, label) {
|
|
208
188
|
if (!c || typeof c !== "object" || typeof c.rawBytes !== "string") throw _err("sigstore/bad-bundle", label + " is not a { rawBytes } certificate");
|
|
209
189
|
return _b64(c.rawBytes, label);
|
|
@@ -222,14 +202,9 @@ function _chainDers(vm) {
|
|
|
222
202
|
return [_leafCertDer(vm)];
|
|
223
203
|
}
|
|
224
204
|
|
|
225
|
-
// Verify a DER ECDSA / Ed25519 signature over `data` under a node public key.
|
|
226
205
|
function _rawVerify(keyObj, data, derSig) {
|
|
227
206
|
var t = keyObj.asymmetricKeyType;
|
|
228
207
|
if (t === "ed25519" || t === "ed448") {
|
|
229
|
-
// Defense in depth at the verify SINK (not only at _pubFromSpki): node imports a low-order /
|
|
230
|
-
// off-curve OKP key without complaint and such a key verifies a FORGED EdDSA signature, so re-gate
|
|
231
|
-
// the point here regardless of how keyObj was obtained -- no verify sink may hand an unvalidated
|
|
232
|
-
// Edwards key to node's verify.
|
|
233
208
|
edwardsPoint.validateSpki(keyObj.export({ type: "spki", format: "der" }), t === "ed25519" ? 6 : 7, SigstoreError, "sigstore/bad-key");
|
|
234
209
|
return nodeCrypto.verify(null, data, keyObj, derSig);
|
|
235
210
|
}
|
|
@@ -241,35 +216,21 @@ function _pubFromSpki(spkiDer, label) {
|
|
|
241
216
|
var key;
|
|
242
217
|
try { key = nodeCrypto.createPublicKey({ key: spkiDer, format: "der", type: "spki" }); }
|
|
243
218
|
catch (e) { throw _err("sigstore/bad-key", "invalid " + label + " public key", e); }
|
|
244
|
-
// node imports a low-order (e.g. all-zeroes) OKP public key without complaint, and such a key
|
|
245
|
-
// verifies a FORGED EdDSA signature -- route an Ed25519/Ed448 SPKI through the shared full-order,
|
|
246
|
-
// on-curve Edwards-point gate (as the webauthn and path-validation EdDSA paths already do) before it
|
|
247
|
-
// can be handed to verify.
|
|
248
219
|
var t = key.asymmetricKeyType;
|
|
249
220
|
if (t === "ed25519" || t === "ed448") {
|
|
250
221
|
edwardsPoint.validateSpki(spkiDer, t === "ed25519" ? 6 : 7, SigstoreError, "sigstore/bad-key");
|
|
251
222
|
}
|
|
252
223
|
return key;
|
|
253
224
|
}
|
|
254
|
-
// Parse a certificate, re-typing a malformed-DER fault to a sigstore/* error so
|
|
255
|
-
// the caller never sees a raw certificate/* / asn1/* leak from this boundary.
|
|
256
225
|
function _parseCert(der, label) {
|
|
257
226
|
try { return x509.parse(der); }
|
|
258
227
|
catch (e) { throw _err("sigstore/bad-certificate", "invalid " + label, e); }
|
|
259
228
|
}
|
|
260
229
|
|
|
261
|
-
// ---- Rekor leg (RFC 9162 inclusion + Rekor-signed root + entry binding) -------
|
|
262
230
|
|
|
263
231
|
function _sha256(buf) { return nodeCrypto.createHash("sha256").update(buf).digest(); }
|
|
264
232
|
|
|
265
|
-
// Select the caller Rekor key whose logId.keyId matches the entry's logId (full
|
|
266
|
-
// 32-byte id) or a 4-byte checkpoint keyhint. Never trust a key from the bundle.
|
|
267
|
-
// A key carrying a validFor window is used only if `timeMs` (the entry's log
|
|
268
|
-
// time) falls inside it -- a rotated-out key must not verify a later entry.
|
|
269
|
-
// A validFor bound is an epoch-ms number, a Date, or an ISO-8601 string (the form
|
|
270
|
-
// a Sigstore trusted_root carries). Returns null for absent/unparseable.
|
|
271
233
|
function _toMs(x) {
|
|
272
|
-
// Coverage residual -- _inWindow gates every _toMs call behind a != null check; the null guard is dead defensive code.
|
|
273
234
|
if (x == null) return null;
|
|
274
235
|
if (typeof x === "number") return Number.isFinite(x) ? x : null;
|
|
275
236
|
if (guard.time.isDate(x)) { var d = guard.time.instantOf(x); return isNaN(d) ? null : d; }
|
|
@@ -278,8 +239,6 @@ function _toMs(x) {
|
|
|
278
239
|
}
|
|
279
240
|
function _inWindow(timeMs, vf) {
|
|
280
241
|
if (!vf) return true;
|
|
281
|
-
// A present-but-unparseable bound fails closed (the key/CA is not used) rather
|
|
282
|
-
// than silently disabling the window it configures.
|
|
283
242
|
if (vf.start != null) { var s = _toMs(vf.start); if (s === null || !(timeMs >= s)) return false; }
|
|
284
243
|
if (vf.end != null) { var e = _toMs(vf.end); if (e === null || !(timeMs <= e)) return false; }
|
|
285
244
|
return true;
|
|
@@ -294,15 +253,12 @@ function _rekorKey(rekorKeys, keyIdBuf, hint, timeMs) {
|
|
|
294
253
|
return null;
|
|
295
254
|
}
|
|
296
255
|
|
|
297
|
-
// Verify the checkpoint (C2SP signed-note): ECDSA-P256-SHA256 over the note body
|
|
298
|
-
// (bytes up to and including the newline before the blank-line separator).
|
|
299
256
|
function _verifyCheckpoint(envelope, ip, rekorKeys, timeMs) {
|
|
300
257
|
var buf = Buffer.from(envelope, "utf8");
|
|
301
258
|
var sep = buf.indexOf("\n\n");
|
|
302
259
|
if (sep < 0) throw _err("sigstore/bad-checkpoint", "the checkpoint has no note/signature separator");
|
|
303
260
|
var body = buf.subarray(0, sep + 1);
|
|
304
261
|
var lines = body.toString("utf8").split("\n");
|
|
305
|
-
// The note body's third line is the base64 tree root; it MUST equal the proof root.
|
|
306
262
|
if (_b64(lines[2], "checkpoint root").toString("hex") !== _b64(ip.rootHash, "inclusionProof.rootHash").toString("hex")) {
|
|
307
263
|
throw _err("sigstore/inclusion-proof-mismatch", "the checkpoint root does not match the inclusion-proof root");
|
|
308
264
|
}
|
|
@@ -320,8 +276,6 @@ function _verifyCheckpoint(envelope, ip, rekorKeys, timeMs) {
|
|
|
320
276
|
return false;
|
|
321
277
|
}
|
|
322
278
|
|
|
323
|
-
// Verify the SET (inclusionPromise.signedEntryTimestamp): ECDSA over the RFC 8785
|
|
324
|
-
// canonical JSON { body, integratedTime, logID, logIndex } (keys sorted).
|
|
325
279
|
function _verifySet(te, rekorKeys, timeMs) {
|
|
326
280
|
var promise = te.inclusionPromise;
|
|
327
281
|
if (!promise || typeof promise.signedEntryTimestamp !== "string") return false;
|
|
@@ -338,9 +292,6 @@ function _verifySet(te, rekorKeys, timeMs) {
|
|
|
338
292
|
return _rawVerify(_pubFromSpki(k.spki, "Rekor log"), Buffer.from(canonical, "utf8"), sig);
|
|
339
293
|
}
|
|
340
294
|
|
|
341
|
-
// Bind the tlog entry to THIS bundle's signature: the dsse-kind body's embedded
|
|
342
|
-
// signature + payload hash must be the envelope's (RFC 9162 proves the body is
|
|
343
|
-
// logged; this proves the body is OUR envelope, not some other logged entry).
|
|
344
295
|
function _bindEntry(te, envelope, leafDer) {
|
|
345
296
|
var body = _jsonParse(_b64(te.canonicalizedBody, "canonicalizedBody"), "sigstore/bad-tlog-entry", "Rekor entry body");
|
|
346
297
|
if (!body || typeof body !== "object" || Array.isArray(body)) throw _err("sigstore/bad-tlog-entry", "the Rekor entry body is not a JSON object");
|
|
@@ -353,16 +304,10 @@ function _bindEntry(te, envelope, leafDer) {
|
|
|
353
304
|
var bodySig = _b64(spec.signatures[0].signature, "Rekor entry signature");
|
|
354
305
|
if (!envSig.equals(bodySig)) throw _err("sigstore/entry-mismatch", "the Rekor entry signature does not match the bundle signature");
|
|
355
306
|
var payload = _b64(envelope.payload, "dsseEnvelope.payload");
|
|
356
|
-
// payloadHash is a required binding field: bind it to the envelope payload
|
|
357
|
-
// rather than skipping the check when it is absent.
|
|
358
307
|
if (!spec.payloadHash || typeof spec.payloadHash.value !== "string") throw _err("sigstore/bad-tlog-entry", "the Rekor dsse entry is missing its payloadHash");
|
|
359
308
|
if (spec.payloadHash.value !== _sha256(payload).toString("hex")) {
|
|
360
309
|
throw _err("sigstore/entry-mismatch", "the Rekor entry payloadHash does not match the bundle payload");
|
|
361
310
|
}
|
|
362
|
-
// The dsse entry's verifier certificate is REQUIRED and MUST be the bundle's
|
|
363
|
-
// leaf cert -- otherwise a valid inclusion proof for an entry signed by a
|
|
364
|
-
// DIFFERENT Fulcio cert could be paired with this envelope (the signature match
|
|
365
|
-
// alone leaves cert substitution).
|
|
366
311
|
var vf = spec.signatures[0].verifier;
|
|
367
312
|
if (typeof vf !== "string") throw _err("sigstore/bad-tlog-entry", "the Rekor dsse entry is missing its verifier certificate");
|
|
368
313
|
var vDer;
|
|
@@ -376,13 +321,9 @@ function _verifyRekor(te, envelope, rekorKeys, leafDer) {
|
|
|
376
321
|
if (!ip || typeof ip.rootHash !== "string" || !Array.isArray(ip.hashes)) {
|
|
377
322
|
throw _err("sigstore/bad-inclusion-proof", "the transparency-log entry has no inclusion proof");
|
|
378
323
|
}
|
|
379
|
-
// The entry's CLAIMED log time selects which caller Rekor key is in its validity
|
|
380
|
-
// window; the SET (required below) re-confirms it by signing it.
|
|
381
324
|
var claimedSec = Number(te.integratedTime);
|
|
382
325
|
var claimedMs = Number.isFinite(claimedSec) ? C.TIME.seconds(claimedSec) : NaN;
|
|
383
|
-
// Bind the entry to this signature FIRST (a proof for another entry is not evidence).
|
|
384
326
|
_bindEntry(te, envelope, leafDer);
|
|
385
|
-
// Fold the audit path to the tree root (RFC 9162), reusing pki.merkle.
|
|
386
327
|
var included;
|
|
387
328
|
try {
|
|
388
329
|
included = merkle.verifyInclusion({
|
|
@@ -396,33 +337,19 @@ function _verifyRekor(te, envelope, rekorKeys, leafDer) {
|
|
|
396
337
|
throw _err("sigstore/bad-inclusion-proof", "the inclusion proof is malformed: " + e.message, e);
|
|
397
338
|
}
|
|
398
339
|
if (!included) throw _err("sigstore/inclusion-proof-mismatch", "the inclusion proof does not reconstruct the tree root");
|
|
399
|
-
// The inclusion proof above only reconstructs the attacker-supplied
|
|
400
|
-
// inclusionProof.rootHash; that root is trust ONLY once the Rekor-signed
|
|
401
|
-
// checkpoint (which signs the tree root) verifies over it -- so the checkpoint
|
|
402
|
-
// is REQUIRED, or the inclusion check is theater against an unsigned root.
|
|
403
340
|
var checkpointOk = ip.checkpoint && typeof ip.checkpoint.envelope === "string" && _verifyCheckpoint(ip.checkpoint.envelope, ip, rekorKeys, claimedMs);
|
|
404
341
|
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");
|
|
405
|
-
// The integratedTime dates the ephemeral (~10-min) Fulcio cert, so it too must
|
|
406
|
-
// be Rekor-attested: ONLY the SET signs it (the checkpoint signs the root, not
|
|
407
|
-
// the time). Without a verified SET the time is attacker-controlled and could
|
|
408
|
-
// date an expired cert into validity -- require it (the RFC 3161 timestamp
|
|
409
|
-
// source is the deferred alternative).
|
|
410
342
|
var setOk = _verifySet(te, rekorKeys, claimedMs);
|
|
411
343
|
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");
|
|
412
344
|
var t = Number(te.integratedTime);
|
|
413
345
|
if (!Number.isFinite(t) || t < 0) throw _err("sigstore/bad-tlog-entry", "the transparency-log entry has a malformed integratedTime");
|
|
414
|
-
// Surface which attested Rekor log entry this is: the global log index and the log's key id (hex), so a
|
|
415
|
-
// caller can locate the exact transparency-log record for an audit rather than re-deriving it.
|
|
416
346
|
var li = Number(te.logIndex);
|
|
417
347
|
return { integratedTime: t, logIndex: Number.isFinite(li) ? li : null, logId: te.logId && te.logId.keyId ? _b64(te.logId.keyId, "logId.keyId").toString("hex") : null };
|
|
418
348
|
}
|
|
419
349
|
|
|
420
|
-
// ---- Fulcio chain leg (RFC 5280 path validation, as-of the log time) ----------
|
|
421
350
|
|
|
422
351
|
function _dn(cert, which) { return cert[which] && cert[which].dn; }
|
|
423
352
|
|
|
424
|
-
// Normalize a caller Fulcio root: a raw DER Buffer, or { der|rawBytes, validFor }
|
|
425
|
-
// (the shape carried by a Sigstore trusted_root certificateAuthorities entry).
|
|
426
353
|
function _normRoots(fulcioRoots) {
|
|
427
354
|
return fulcioRoots.map(function (r, i) {
|
|
428
355
|
var der = Buffer.isBuffer(r) ? r : (r && (r.der || r.rawBytes));
|
|
@@ -431,12 +358,6 @@ function _normRoots(fulcioRoots) {
|
|
|
431
358
|
});
|
|
432
359
|
}
|
|
433
360
|
|
|
434
|
-
// Build the ordered path body [intermediates..., leaf] from the leaf up to (not
|
|
435
|
-
// including) a cert issued by `anchor`, drawing intermediates from `intBySubject`
|
|
436
|
-
// (caller certs AND bundle links -- all get cryptographically re-checked by
|
|
437
|
-
// path.validate). `anchor` itself is never placed in the body. Returns null if no
|
|
438
|
-
// such path exists. The terminal trust anchor is always a caller cert; a bundle
|
|
439
|
-
// cert can appear only here, as a non-terminal path step.
|
|
440
361
|
function _pathToAnchor(leaf, anchor, intBySubject) {
|
|
441
362
|
var anchorSubj = _dn(anchor, "subject");
|
|
442
363
|
var path = [leaf], cur = leaf, seen = {}, guardN = 0;
|
|
@@ -460,15 +381,10 @@ function _pathToAnchor(leaf, anchor, intBySubject) {
|
|
|
460
381
|
async function _verifyChain(leaf, chainDers, fulcioRoots, timeMs) {
|
|
461
382
|
var roots = _normRoots(fulcioRoots);
|
|
462
383
|
var bundleLinks = chainDers.slice(1).map(function (d, i) { return _parseCert(d, "bundle intermediate [" + i + "]"); });
|
|
463
|
-
// Path-body intermediates: caller certs (trusted) + bundle intermediates (path
|
|
464
|
-
// steps only). The terminal anchor is chosen ONLY from the caller roots below.
|
|
465
384
|
var intBySubject = {};
|
|
466
385
|
function addInt(c) { (intBySubject[_dn(c, "subject")] = intBySubject[_dn(c, "subject")] || []).push(c); }
|
|
467
386
|
roots.forEach(function (r) { addInt(r.cert); });
|
|
468
387
|
bundleLinks.forEach(addInt);
|
|
469
|
-
// Try each caller anchor within its validity window (the trusted_root carries
|
|
470
|
-
// several CA rotations, some sharing a subject DN); accept the first that both
|
|
471
|
-
// yields a path and cryptographically validates as of the log time.
|
|
472
388
|
var candidates = roots.filter(function (r) { return _inWindow(timeMs, r.validFor); });
|
|
473
389
|
var lastErr = _err("sigstore/chain-incomplete", "no caller-supplied Fulcio anchor (within its validity window) issues the chain");
|
|
474
390
|
for (var i = 0; i < candidates.length; i++) {
|
|
@@ -486,29 +402,18 @@ async function _verifyChain(leaf, chainDers, fulcioRoots, timeMs) {
|
|
|
486
402
|
if (res.valid) return;
|
|
487
403
|
lastErr = _err("sigstore/chain-invalid", "the Fulcio certificate chain is not valid as of the log time");
|
|
488
404
|
} catch (e) {
|
|
489
|
-
// Coverage residual -- pathValidate.validate is fail-safe (builtinVerify never throws; every decodeExt wrapped), so no raw throw escapes for a well-formed path; the catch is a defensive re-type.
|
|
490
405
|
lastErr = _err("sigstore/chain-invalid", "the Fulcio certificate chain failed validation: " + e.message, e);
|
|
491
406
|
}
|
|
492
407
|
}
|
|
493
408
|
throw lastErr;
|
|
494
409
|
}
|
|
495
410
|
|
|
496
|
-
// ---- Fulcio identity leg -----------------------------------------------------
|
|
497
411
|
|
|
498
|
-
// Decode the identity the Fulcio cert commits to: the SAN plus the Fulcio
|
|
499
|
-
// extension arc (Issuer .1.8, SourceRepositoryURI .1.12, etc). `.1.8`+ values are
|
|
500
|
-
// DER UTF8String; the legacy `.1.1`-`.1.6` are raw strings (never DER).
|
|
501
|
-
// Derive the OIDs from the registry (never a dotted-decimal literal in source):
|
|
502
|
-
// the SAN, and the Fulcio arc base (the parent of the `.7` otherName member).
|
|
503
412
|
var SAN_OID = oid.byName("subjectAltName");
|
|
504
413
|
var FULCIO_PREFIX = oid.byName("otherName").split(".").slice(0, -1).join(".") + ".";
|
|
505
414
|
function _identity(leaf) {
|
|
506
415
|
var out = { san: null, extensions: {} };
|
|
507
|
-
// The identity is security-relevant: an undecodable SAN / Fulcio member is a
|
|
508
|
-
// malformed certificate and fails closed with a named reason (never a silent
|
|
509
|
-
// drop that would hide an identity claim a caller policy relies on).
|
|
510
416
|
try {
|
|
511
|
-
// Coverage residual -- x509.parse always surfaces extensions as an array.
|
|
512
417
|
(leaf.extensions || []).forEach(function (ext) {
|
|
513
418
|
if (ext.oid === SAN_OID) { out.san = _sanValue(ext); return; }
|
|
514
419
|
if (ext.oid.indexOf(FULCIO_PREFIX) === 0) {
|
|
@@ -521,86 +426,52 @@ function _identity(leaf) {
|
|
|
521
426
|
}
|
|
522
427
|
return out;
|
|
523
428
|
}
|
|
524
|
-
// The SAN GeneralNames the OIDC identity is carried in: a context-tagged
|
|
525
|
-
// [1] rfc822Name / [2] dNSName / [6] uniformResourceIdentifier. Return the URI
|
|
526
|
-
// (a machine identity) preferentially, else the first name, with its type.
|
|
527
429
|
var GN_TYPE = { 1: "rfc822Name", 2: "dNSName", 6: "uri" };
|
|
528
430
|
function _sanValue(ext) {
|
|
529
431
|
var seq = asn1.decode(ext.value);
|
|
530
432
|
var names = [];
|
|
531
|
-
// Coverage residual -- path.validate's GeneralName gate (context-only, SEQUENCE SAN, strict otherName [0]) rejects the shapes that would trigger these fallbacks before _identity; asn1.decode guarantees a children array on any constructed node.
|
|
532
433
|
for (var i = 0; i < (seq.children || []).length; i++) {
|
|
533
434
|
var n = seq.children[i];
|
|
534
435
|
if (n.tagClass !== "context") continue;
|
|
535
436
|
var val = null;
|
|
536
437
|
if (!n.constructed && GN_TYPE[n.tagNumber]) {
|
|
537
|
-
// A primitive IMPLICIT IA5String name: rfc822Name / dNSName / URI.
|
|
538
438
|
val = { type: GN_TYPE[n.tagNumber], value: asn1.read.octetStringImplicit(n, n.tagNumber).toString("utf8") };
|
|
539
439
|
} else if (n.constructed && n.tagNumber === 0 && (n.children || []).length >= 2) {
|
|
540
|
-
// otherName ::= SEQUENCE { type-id OID, [0] EXPLICIT value } -- Fulcio carries
|
|
541
|
-
// machine identities here (type-id .1.7, value a UTF8String).
|
|
542
440
|
var inner = (n.children[1].children || [])[0] || n.children[1];
|
|
543
441
|
var v;
|
|
544
|
-
|
|
545
|
-
|
|
442
|
+
try {
|
|
443
|
+
v = asn1.read.string(inner);
|
|
444
|
+
} catch (_e) {
|
|
445
|
+
v = null;
|
|
446
|
+
}
|
|
546
447
|
val = { type: "otherName", oid: asn1.read.oid(n.children[0]), value: v };
|
|
547
448
|
}
|
|
548
449
|
if (val) names.push(val);
|
|
549
450
|
}
|
|
550
|
-
// A Fulcio certificate binds exactly one identity; more than one SAN lets a
|
|
551
|
-
// mis-issued cert pair the expected identity with a smuggled extra one, so a
|
|
552
|
-
// caller policy that matches the wrong entry cannot be relied on -- reject.
|
|
553
451
|
if (names.length > 1) throw _err("sigstore/bad-certificate", "the Fulcio certificate carries multiple SAN identities");
|
|
554
452
|
return names[0] || null;
|
|
555
453
|
}
|
|
556
454
|
function _fulcioExtValue(ext, leafArc) {
|
|
557
|
-
// Coverage residual -- ext.value is read.octetString output, always a Buffer.
|
|
558
455
|
if (!Buffer.isBuffer(ext.value)) throw _err("sigstore/bad-certificate", "Fulcio extension " + ext.oid + " has no value");
|
|
559
|
-
// The raw-vs-DER split by member (Fulcio oid-info): .1-.6 legacy are raw UTF-8
|
|
560
|
-
// strings; .8+ are DER UTF8String -- the arc is open-ended past .22. An
|
|
561
|
-
// undecodable member throws (caught by _identity) rather than dropping silently.
|
|
562
456
|
if (leafArc >= 1 && leafArc <= 6) return ext.value.toString("utf8");
|
|
563
457
|
return asn1.read.string(asn1.decode(ext.value));
|
|
564
458
|
}
|
|
565
459
|
|
|
566
|
-
// The fields an identity policy may constrain. Named once so the check, the refusal of an
|
|
567
|
-
// unknown key, and the report of what ran cannot drift apart.
|
|
568
460
|
var IDENTITY_FIELDS = ["san", "issuer", "sourceRepositoryURI"];
|
|
569
|
-
// The guard tests membership with hasOwnProperty, so the permitted set is a lookup object --
|
|
570
|
-
// an array would treat "0"/"1" as the known keys and reject every real field name.
|
|
571
461
|
var IDENTITY_KEYS = { san: 1, issuer: 1, sourceRepositoryURI: 1 };
|
|
572
|
-
// Every option pki.sigstore.verifyBundle reads. Adding one here is the only way to make it
|
|
573
|
-
// accepted, so a capability cannot arrive with its option silently ignored at this boundary.
|
|
574
462
|
var _VERIFY_BUNDLE_OPTS = { fulcioRoots: 1, rekorKeys: 1, identity: 1, predicateType: 1, time: 1 };
|
|
575
463
|
|
|
576
|
-
// Returns WHICH fields were actually compared. A bundle verifies its own signature and log
|
|
577
|
-
// inclusion whoever signed it -- Fulcio issues to anyone who completes an OIDC flow -- so
|
|
578
|
-
// `verified: true` without an identity policy says the artifact was signed and logged, not that
|
|
579
|
-
// a trusted party signed it. The caller cannot tell those apart from a bare boolean.
|
|
580
464
|
function _checkIdentity(id, policy) {
|
|
581
465
|
var ran = { san: false, issuer: false, sourceRepositoryURI: false };
|
|
582
466
|
if (policy === undefined || policy === null) return ran;
|
|
583
467
|
if (typeof policy !== "object" || Array.isArray(policy)) {
|
|
584
468
|
throw _err("sigstore/bad-input", "opts.identity must be an object naming the identity fields to pin (" + IDENTITY_FIELDS.join(", ") + ")");
|
|
585
469
|
}
|
|
586
|
-
// An unknown key is refused, not ignored: cosign spells this `certificateIdentity`, and a
|
|
587
|
-
// swallowed spelling checks nothing under a name the operator believes pins the signer.
|
|
588
|
-
// The guard rejects through a (code, message) FACTORY. Handing it the error CLASS raises
|
|
589
|
-
// "class constructor cannot be invoked without new" -- a raw, untyped throw escaping a public
|
|
590
|
-
// verb, on the branch a valid-input test never takes.
|
|
591
470
|
guard.identifier.assertKnownKeys(policy, IDENTITY_KEYS, _err, "sigstore/bad-input", "opts.identity has an unknown key ");
|
|
592
|
-
// A policy that constrains NOTHING is a configuration mistake, and the most dangerous input on
|
|
593
|
-
// this surface: every guard below is falsy, so it accepts every signer while reading as though
|
|
594
|
-
// an identity policy is in force. Refused at the boundary rather than answered.
|
|
595
471
|
var asked = IDENTITY_FIELDS.filter(function (f) { return policy[f] !== undefined; });
|
|
596
472
|
if (!asked.length) {
|
|
597
473
|
throw _err("sigstore/bad-input", "opts.identity constrains nothing -- name at least one of " + IDENTITY_FIELDS.join(", ") + ", or omit it to state that the signer is not being checked");
|
|
598
474
|
}
|
|
599
|
-
// A named field must carry a value that can actually be compared. The comparisons below are
|
|
600
|
-
// truthiness-guarded, so an empty string or a null would be skipped while the field had been
|
|
601
|
-
// named -- reporting a signer check that never ran, which is the exact confusion this report
|
|
602
|
-
// exists to remove. Deciding "asked" and deciding "compared" must be the SAME test, so a value
|
|
603
|
-
// that cannot be compared is refused here rather than quietly becoming "not asked".
|
|
604
475
|
asked.forEach(function (f) {
|
|
605
476
|
if (typeof policy[f] !== "string" || policy[f] === "") {
|
|
606
477
|
throw _err("sigstore/bad-input", "opts.identity." + f + " must be a non-empty string -- a value that cannot be compared would leave the signer unchecked under a policy that names it");
|
|
@@ -609,14 +480,11 @@ function _checkIdentity(id, policy) {
|
|
|
609
480
|
});
|
|
610
481
|
var sanValue = id.san && id.san.value;
|
|
611
482
|
if (policy.san && sanValue !== policy.san) throw _err("sigstore/identity-mismatch", "the certificate SAN " + JSON.stringify(sanValue) + " does not match the expected identity");
|
|
612
|
-
// The OIDC issuer is carried by the current Issuer V2 (.1.8) or, on older certs,
|
|
613
|
-
// only by the deprecated raw-string issuer (.1.1); match against either.
|
|
614
483
|
if (policy.issuer && policy.issuer !== id.extensions.issuer && policy.issuer !== id.extensions.issuerLegacy) throw _err("sigstore/identity-mismatch", "the certificate OIDC issuer does not match the expected issuer");
|
|
615
484
|
if (policy.sourceRepositoryURI && id.extensions.sourceRepositoryURI !== policy.sourceRepositoryURI) throw _err("sigstore/identity-mismatch", "the certificate source-repository URI does not match");
|
|
616
485
|
return ran;
|
|
617
486
|
}
|
|
618
487
|
|
|
619
|
-
// ---- in-toto Statement leg ---------------------------------------------------
|
|
620
488
|
|
|
621
489
|
function _statement(payload, payloadType, expectedPredicate) {
|
|
622
490
|
if (payloadType !== "application/vnd.in-toto+json") {
|
|
@@ -625,16 +493,12 @@ function _statement(payload, payloadType, expectedPredicate) {
|
|
|
625
493
|
var st = _jsonParse(payload, "sigstore/bad-statement", "in-toto statement");
|
|
626
494
|
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");
|
|
627
495
|
if (!Array.isArray(st.subject) || !st.subject.length) throw _err("sigstore/bad-statement", "the in-toto statement has no subject");
|
|
628
|
-
// The verdict carries predicateType for the caller's own gate; when the caller
|
|
629
|
-
// pins one (opts.predicateType), a mismatch fails closed so a non-SLSA
|
|
630
|
-
// attestation (e.g. an SBOM) is not mistaken for the expected provenance.
|
|
631
496
|
if (expectedPredicate && st.predicateType !== expectedPredicate) {
|
|
632
497
|
throw _err("sigstore/predicate-mismatch", "the statement predicateType " + JSON.stringify(st.predicateType) + " does not match the expected " + JSON.stringify(expectedPredicate));
|
|
633
498
|
}
|
|
634
499
|
return st;
|
|
635
500
|
}
|
|
636
501
|
|
|
637
|
-
// ---- orchestrator ------------------------------------------------------------
|
|
638
502
|
|
|
639
503
|
/**
|
|
640
504
|
* @primitive pki.sigstore.verifyBundle
|
|
@@ -685,12 +549,6 @@ async function verifyBundle(bundle, opts) {
|
|
|
685
549
|
throw new TypeError("verifyBundle: bundle must be an object, JSON string, or Buffer");
|
|
686
550
|
}
|
|
687
551
|
opts = opts || {};
|
|
688
|
-
// An unrecognized option is refused, not swallowed. The identity policy one level down already
|
|
689
|
-
// closes this door for exactly the reason it needs closing here too: cosign spells the signer pin
|
|
690
|
-
// `certificateIdentity`, and a swallowed spelling checks nothing under a name the operator
|
|
691
|
-
// believes pins the signer. At the top level the same slip loses the SLSA predicate pin as well,
|
|
692
|
-
// and `predicateType` has no `identityChecked`-style field, so nothing in the verdict reveals
|
|
693
|
-
// that the pin never ran.
|
|
694
552
|
guard.identifier.assertKnownKeys(opts, _VERIFY_BUNDLE_OPTS, _err, "sigstore/bad-input", "pki.sigstore.verifyBundle has an unknown option ");
|
|
695
553
|
var b = parseBundle(bundle);
|
|
696
554
|
var vm = b.verificationMaterial;
|
|
@@ -698,7 +556,6 @@ async function verifyBundle(bundle, opts) {
|
|
|
698
556
|
var rekorKeys = opts.rekorKeys || [];
|
|
699
557
|
var fulcioRoots = opts.fulcioRoots || [];
|
|
700
558
|
|
|
701
|
-
// Leg 1 -- DSSE signature over PAE under the Fulcio leaf public key.
|
|
702
559
|
var leafDer = _leafCertDer(vm);
|
|
703
560
|
var leaf = _parseCert(leafDer, "Fulcio leaf certificate");
|
|
704
561
|
var payload = _b64(env.payload, "dsseEnvelope.payload");
|
|
@@ -707,9 +564,6 @@ async function verifyBundle(bundle, opts) {
|
|
|
707
564
|
var leafKey = _pubFromSpki(leaf.subjectPublicKeyInfo.bytes, "Fulcio leaf");
|
|
708
565
|
if (!_rawVerify(leafKey, preimage, derSig)) throw _err("sigstore/dsse-verify-failed", "the DSSE signature does not verify under the Fulcio leaf key");
|
|
709
566
|
|
|
710
|
-
// Leg 2 -- Rekor inclusion + Rekor-signed root + entry binding (yields the
|
|
711
|
-
// trusted time). Try every transparency-log entry; accept the first that fully
|
|
712
|
-
// verifies (and binds this signature), rejecting only if none do.
|
|
713
567
|
var tlogs = Array.isArray(vm.tlogEntries) ? vm.tlogEntries : [];
|
|
714
568
|
if (!tlogs.length) throw _err("sigstore/bad-bundle", "a keyless bundle requires at least one transparency-log entry");
|
|
715
569
|
var rekor = null, lastErr = null;
|
|
@@ -721,22 +575,15 @@ async function verifyBundle(bundle, opts) {
|
|
|
721
575
|
if (rekor === null) throw lastErr;
|
|
722
576
|
var integratedTime = rekor.integratedTime;
|
|
723
577
|
|
|
724
|
-
// Leg 3 -- Fulcio chain, validated as of the (trusted) log time; then identity.
|
|
725
|
-
// The instant comes through the time guard's intrinsic read. `getTime` is an ordinary method a
|
|
726
|
-
// subclass answers, and the slot predicate above accepts a subclass from any realm, so reading
|
|
727
|
-
// the method off the value let a caller name the moment the Fulcio chain is validated at --
|
|
728
|
-
// epoch zero, say, where every certificate in it is still unissued.
|
|
729
578
|
var checkTime = guard.time.isDate(opts.time) ? guard.time.instantOf(opts.time) : C.TIME.seconds(integratedTime);
|
|
730
579
|
await _verifyChain(leaf, _chainDers(vm), fulcioRoots, checkTime);
|
|
731
580
|
var identity = _identity(leaf);
|
|
732
581
|
var identityChecked = _checkIdentity(identity, opts.identity);
|
|
733
582
|
|
|
734
|
-
// Leg 4 -- the in-toto SLSA statement + subject binding (with an optional
|
|
735
|
-
// caller-pinned predicateType).
|
|
736
583
|
var st = _statement(payload, env.payloadType, opts.predicateType);
|
|
737
584
|
|
|
738
585
|
return {
|
|
739
|
-
valid: true,
|
|
586
|
+
valid: true,
|
|
740
587
|
verified: true,
|
|
741
588
|
payload: payload,
|
|
742
589
|
statement: st,
|
|
@@ -744,16 +591,9 @@ async function verifyBundle(bundle, opts) {
|
|
|
744
591
|
predicateType: st.predicateType,
|
|
745
592
|
predicate: st.predicate,
|
|
746
593
|
identity: identity,
|
|
747
|
-
// Which identity fields were actually compared. `verified` says the artifact was signed and
|
|
748
|
-
// logged; only these say a signer the caller named was the one who signed it.
|
|
749
594
|
identityChecked: identityChecked,
|
|
750
|
-
// Whether the caller pinned a predicateType (opts.predicateType) that was checked. The verdict is
|
|
751
|
-
// only reached when a pinned predicate matched (a mismatch throws sigstore/predicate-mismatch), so
|
|
752
|
-
// a caller that omitted the pin gets `false` and knows the SLSA predicate went unverified.
|
|
753
595
|
predicateTypeChecked: opts.predicateType != null,
|
|
754
596
|
integratedTime: integratedTime,
|
|
755
|
-
// The attested Rekor entry this verdict rests on: `logIndex` is its global log index and `logId` the
|
|
756
|
-
// log's key id (hex), so a caller can fetch or audit the exact transparency-log record.
|
|
757
597
|
logIndex: rekor.logIndex,
|
|
758
598
|
logId: rekor.logId,
|
|
759
599
|
};
|
package/lib/sleep.js
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Copyright (c) blamejs contributors
|
|
3
3
|
"use strict";
|
|
4
|
-
//
|
|
5
|
-
// @internal -- the shared bounded poll sleeper for the stateful network clients (pki.acme.client and
|
|
6
|
-
// pki.cmp.session). A delay above Node's 32-bit setTimeout ceiling is SPLIT into chained maximum-size
|
|
7
|
-
// chunks: a bare setTimeout(fn, > 2^31-1) is silently clamped to 1 ms (a TimeoutOverflowWarning) and would
|
|
8
|
-
// then rapidly re-poll instead of waiting the full interval. Each client's opts.sleep overrides this in
|
|
9
|
-
// tests, so this default, the one sleeper that touches a real timer, is never driven by a test wait.
|
|
4
|
+
// @internal
|
|
10
5
|
|
|
11
|
-
var SETTIMEOUT_MAX_MS = 2147483647;
|
|
6
|
+
var SETTIMEOUT_MAX_MS = 2147483647;
|
|
12
7
|
|
|
13
|
-
// sleep(ms) -> Promise resolved after `ms` milliseconds, chunking a delay past the timer ceiling.
|
|
14
8
|
function sleep(ms) {
|
|
15
9
|
return new Promise(function (resolve) {
|
|
16
10
|
(function step(remaining) {
|