@blamejs/pki 0.5.7 → 0.5.9
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 +418 -386
- package/MIGRATING.md +43 -0
- package/README.md +12 -12
- package/lib/acme.js +31 -31
- package/lib/asn1-der.js +10 -10
- package/lib/attrcert-sign.js +19 -19
- package/lib/byte-reader.js +6 -6
- package/lib/byte-writer.js +5 -5
- package/lib/cbor-det.js +27 -24
- package/lib/cmc-build.js +97 -92
- package/lib/cmc-verify.js +106 -30
- package/lib/cmp-build.js +25 -25
- package/lib/cmp-session.js +70 -70
- package/lib/cmp-verify.js +71 -57
- package/lib/cms-compress.js +7 -7
- package/lib/cms-decrypt.js +90 -72
- package/lib/cms-encrypt.js +32 -32
- package/lib/cms-sign.js +74 -55
- package/lib/cms-verify.js +97 -75
- package/lib/composite-sig.js +13 -13
- package/lib/constants.js +4 -4
- package/lib/crl-sign.js +22 -22
- package/lib/crl-verify.js +7 -6
- package/lib/crmf-sign.js +14 -14
- package/lib/csr-sign.js +8 -8
- package/lib/ct.js +37 -37
- package/lib/edwards-point.js +7 -7
- package/lib/est.js +98 -55
- package/lib/framework-error.js +5 -5
- package/lib/guard-all.js +5 -3
- package/lib/guard-async.js +4 -4
- package/lib/guard-bytes.js +79 -79
- package/lib/guard-compress.js +17 -17
- package/lib/guard-crypto.js +1 -1
- package/lib/guard-der.js +152 -0
- package/lib/guard-encoding.js +15 -15
- package/lib/guard-header.js +3 -3
- package/lib/guard-identifier.js +16 -16
- package/lib/guard-json.js +15 -15
- package/lib/guard-limits.js +7 -7
- package/lib/guard-name.js +81 -16
- package/lib/guard-parsed.js +80 -80
- package/lib/guard-range.js +19 -19
- package/lib/guard-secret.js +11 -10
- package/lib/guard-text.js +6 -6
- package/lib/guard-time.js +10 -10
- package/lib/hpke.js +18 -17
- package/lib/http-digest.js +35 -35
- package/lib/http-retry-after.js +13 -13
- package/lib/http-transport.js +20 -19
- package/lib/inspect.js +53 -53
- package/lib/ip-utils.js +2 -2
- package/lib/jose.js +13 -13
- package/lib/key.js +16 -16
- package/lib/lint.js +51 -51
- package/lib/merkle.js +51 -36
- package/lib/mime.js +18 -18
- package/lib/ocsp-verify.js +10 -10
- package/lib/ocsp.js +13 -13
- package/lib/oid.js +29 -29
- package/lib/path-validate.js +114 -113
- package/lib/pbes2.js +16 -16
- package/lib/pkcs12-build.js +53 -53
- package/lib/pki-build.js +48 -20
- package/lib/rc2.js +1 -1
- package/lib/rfc3339.js +5 -5
- package/lib/schema-all.js +31 -31
- package/lib/schema-attrcert.js +12 -12
- package/lib/schema-c509.js +149 -197
- package/lib/schema-cmc.js +58 -58
- package/lib/schema-cmp.js +43 -43
- package/lib/schema-cms.js +45 -45
- package/lib/schema-crl.js +7 -7
- package/lib/schema-crmf.js +28 -28
- package/lib/schema-csr.js +12 -12
- package/lib/schema-csrattrs.js +16 -16
- package/lib/schema-engine.js +18 -18
- package/lib/schema-ocsp.js +15 -15
- package/lib/schema-pkcs12.js +20 -20
- package/lib/schema-pkcs8.js +2 -2
- package/lib/schema-pkix.js +131 -126
- package/lib/schema-smime.js +19 -19
- package/lib/schema-tsp.js +12 -12
- package/lib/schema-x509.js +3 -3
- package/lib/shbs.js +18 -18
- package/lib/sign-scheme.js +13 -13
- package/lib/sigstore.js +10 -11
- package/lib/sleep.js +1 -1
- package/lib/smime.js +308 -96
- package/lib/tls-cert-compress.js +18 -18
- package/lib/trust.js +27 -27
- package/lib/tsp-sign.js +17 -17
- package/lib/validator-all.js +1 -1
- package/lib/validator-attcert.js +1 -1
- package/lib/validator-cose.js +43 -44
- package/lib/validator-keydesc.js +3 -3
- package/lib/validator-sig.js +13 -13
- package/lib/validator-tls.js +11 -11
- package/lib/validator-tpm.js +20 -19
- package/lib/webauthn-mds.js +66 -66
- package/lib/webauthn.js +33 -33
- package/lib/webcrypto.js +15 -15
- package/lib/x509-sign.js +13 -13
- package/package.json +3 -2
- package/sbom.cdx.json +6 -6
package/lib/webauthn-mds.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// The ordering is the load-bearing part: the signature and its certificate chain are established
|
|
14
14
|
// against a CALLER-supplied FIDO root before a single byte of the payload is read, so a hostile
|
|
15
15
|
// BLOB never reaches the JSON reader, the entry walk, or any per-entry certificate decode. No
|
|
16
|
-
// root is bundled and there is no trust-on-first-use
|
|
16
|
+
// root is bundled and there is no trust-on-first-use: an operator supplies the anchor, exactly
|
|
17
17
|
// as pki.trust does for a root store. Retrieval is out of scope: the BLOB is caller-supplied
|
|
18
18
|
// bytes, so this module never touches a socket.
|
|
19
19
|
//
|
|
@@ -73,14 +73,14 @@ function _schemeE(kind, message, cause) { return new WebauthnError("webauthn/" +
|
|
|
73
73
|
// question "which algorithms does this toolkit accept for a JWS?" has exactly one answer, and a
|
|
74
74
|
// row added there reaches here without anybody remembering to copy it.
|
|
75
75
|
//
|
|
76
|
-
// The derivation is
|
|
76
|
+
// The derivation is total over the registry: every algorithm pki.jose verifies gets a row, and an
|
|
77
77
|
// X.509 SubjectPublicKeyInfo can carry a key of every type in it. EC becomes ECDSA over the curve
|
|
78
78
|
// the alg fixes; RSA with a salt length becomes RSASSA-PSS (RFC 7518 sec. 3.5) and without one
|
|
79
79
|
// RSASSA-PKCS1-v1_5; OKP becomes EdDSA, whose curve the certificate supplies because the algorithm
|
|
80
80
|
// does not name one; AKP becomes the ML-DSA parameter set the algorithm itself fixes.
|
|
81
81
|
//
|
|
82
82
|
// A row's import and verify parameters may therefore depend on the LEAF as well as the alg, which
|
|
83
|
-
// is why they are resolved per-verification
|
|
83
|
+
// is why they are resolved per-verification and never frozen into the row: EdDSA over an Ed25519
|
|
84
84
|
// certificate and EdDSA over an Ed448 certificate are the same JWS algorithm and two different
|
|
85
85
|
// WebCrypto algorithms.
|
|
86
86
|
//
|
|
@@ -129,7 +129,7 @@ var DISQUALIFYING = Object.assign(Object.create(null), {
|
|
|
129
129
|
// The statuses whose optional `certificate` field NARROWS the report to the certificate it names
|
|
130
130
|
// (MDS v3.0 sec. 3.1.3). Only a compromised attestation key is about one certificate; every other
|
|
131
131
|
// disqualifying status is about the model, and a certificate attached to one of those is a
|
|
132
|
-
// nonconforming field
|
|
132
|
+
// nonconforming field, not a narrower scope.
|
|
133
133
|
var CERT_SCOPED_STATUS = Object.assign(Object.create(null), { ATTESTATION_KEY_COMPROMISE: 1 });
|
|
134
134
|
|
|
135
135
|
var _BLOB_OPTS = Object.assign(Object.create(null), {
|
|
@@ -141,7 +141,7 @@ function _isPlainObject(v) { return !!v && typeof v === "object" && !Array.isArr
|
|
|
141
141
|
|
|
142
142
|
// The results this module actually produced. Membership is the PROVENANCE check: a metadata object
|
|
143
143
|
// is only allowed to decide trust if verifyMetadataBlob made it. Recognising it by shape instead
|
|
144
|
-
// would accept anything carrying the right property names
|
|
144
|
+
// would accept anything carrying the right property names, including a catalogue deserialized
|
|
145
145
|
// from a cache, where nothing establishes that the signature and chain were ever checked, and an
|
|
146
146
|
// attacker who can write that cache chooses which roots an authenticator is allowed to chain to.
|
|
147
147
|
// A WeakSet keys on object IDENTITY, which no serialization survives, so a round-tripped catalogue
|
|
@@ -173,9 +173,9 @@ function _deepFreeze(v, depth) {
|
|
|
173
173
|
return v;
|
|
174
174
|
}
|
|
175
175
|
var AAGUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
176
|
-
// "This authenticator declares no model identity"
|
|
176
|
+
// "This authenticator declares no model identity": the AAGUID a U2F authenticator carries, since
|
|
177
177
|
// U2F has no such concept. It is a sentinel, never a lookup key, and lives in one place so the
|
|
178
|
-
// lookup and the dispatch that decides
|
|
178
|
+
// lookup and the dispatch that decides which key space applies cannot disagree about it.
|
|
179
179
|
var ZERO_AAGUID = "00000000-0000-0000-0000-000000000000";
|
|
180
180
|
// The JWS header parameters this reader understands (RFC 7515 sec. 4.1). Used only to tell a
|
|
181
181
|
// standard name from an extension name when checking `crit`.
|
|
@@ -196,7 +196,7 @@ function _assertLeafSigns(leaf) {
|
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
// Is this certificate the anchor itself? RFC 5280 sec. 6.1.1 defines a trust anchor as a name and a
|
|
199
|
-
// public key, so that pair
|
|
199
|
+
// public key, so that pair (not the issuer field, and not the certificate's bytes) is what
|
|
200
200
|
// decides. The DN comparison is the canonical one, so two spellings of one name still match.
|
|
201
201
|
function _isAnchorItself(cert, anchor) {
|
|
202
202
|
return guard.name.dnEqual(cert.subject.rdns, anchor.subject.rdns, _err, "webauthn/bad-att-cert", "the anchor subject") &&
|
|
@@ -205,8 +205,8 @@ function _isAnchorItself(cert, anchor) {
|
|
|
205
205
|
|
|
206
206
|
// A parsed anchor certificate, whichever shape the caller supplied it in.
|
|
207
207
|
//
|
|
208
|
-
// An already-parsed certificate is taken as-is
|
|
209
|
-
// recognition test names every field the anchor is later
|
|
208
|
+
// An already-parsed certificate is taken as-is, never re-encoded and re-parsed, but the
|
|
209
|
+
// recognition test names every field the anchor is later read for: the subject name, the public
|
|
210
210
|
// key bytes, and the signature algorithm OID. Recognising an object on a looser test lets something
|
|
211
211
|
// that is merely certificate-SHAPED through: a parsed CSR satisfies "has a subject and an SPKI", and
|
|
212
212
|
// a hand-built object literal satisfies it too and then raises a raw TypeError from deep inside the
|
|
@@ -214,7 +214,7 @@ function _isAnchorItself(cert, anchor) {
|
|
|
214
214
|
function _asCert(v, label) {
|
|
215
215
|
// Through the shared certificate door: these become the anchors a metadata BLOB's signer chain is
|
|
216
216
|
// judged against, and an anchor's identity is its subject and its key. A caller-assembled object
|
|
217
|
-
// could carry a real root's subject beside a substituted key
|
|
217
|
+
// could carry a real root's subject beside a substituted key, every field well-formed, nothing
|
|
218
218
|
// for a shape test to catch -- so the object is re-derived from the bytes its parser read instead.
|
|
219
219
|
// Bytes in any form (a Buffer, a typed-array view, a DataView, an ArrayBuffer) are parsed here;
|
|
220
220
|
// refusing one of those would make the accepted set depend on how the caller received the file.
|
|
@@ -225,7 +225,7 @@ function _asCert(v, label) {
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
// Verify a FIDO Metadata Service BLOB and return its entries indexed for lookup. `blob` is the
|
|
228
|
-
// BLOB as caller-supplied bytes or a string
|
|
228
|
+
// BLOB as caller-supplied bytes or a string; this module never fetches it. The signature is
|
|
229
229
|
// checked under the certificate in the BLOB's own header, that chain is validated to one of
|
|
230
230
|
// `opts.rootCertificates`, and only then is the payload parsed: a BLOB that does not verify never
|
|
231
231
|
// reaches the JSON reader. `no` must exceed a supplied `previousNo` (the rollback check) and
|
|
@@ -267,14 +267,14 @@ function _verifyMetadataBlob(blob, opts) {
|
|
|
267
267
|
}
|
|
268
268
|
var anchors = roots.map(function (r, i) { return _asCert(r, "opts.rootCertificates[" + i + "]"); });
|
|
269
269
|
|
|
270
|
-
// Cap
|
|
271
|
-
// is fully materialized in order to discover it should have been refused
|
|
270
|
+
// Cap before the copy, not after it. Measuring the converted buffer would mean an oversized input
|
|
271
|
+
// is fully materialized in order to discover it should have been refused, which is the allocation the
|
|
272
272
|
// ceiling exists to prevent, performed on the way to reporting that it was too large. A string's
|
|
273
273
|
// length is its character count and a BLOB is ASCII base64url + dots, so it bounds the byte count.
|
|
274
274
|
// A byte input is re-viewed through the shared guard, which is where the detached-backing-buffer
|
|
275
|
-
// case is handled once
|
|
275
|
+
// case is handled once and never re-derived here.
|
|
276
276
|
// Every byte form, not only the two a Buffer-shaped API thinks of. A BLOB is retrieved over the
|
|
277
|
-
// network, and the ordinary way to hold a fetched body is an ArrayBuffer
|
|
277
|
+
// network, and the ordinary way to hold a fetched body is an ArrayBuffer, so the form an
|
|
278
278
|
// operator most naturally arrives with was the one form this refused. `byteLength` is declared by
|
|
279
279
|
// an ArrayBuffer and by every view over one, so the ceiling below still bites before any copy.
|
|
280
280
|
var raw = (ArrayBuffer.isView(blob) || blob instanceof ArrayBuffer)
|
|
@@ -282,7 +282,7 @@ function _verifyMetadataBlob(blob, opts) {
|
|
|
282
282
|
// A string's `.length` counts UTF-16 code units, not the UTF-8 bytes the conversion produces, so
|
|
283
283
|
// measuring it would let a string of multi-byte characters sit under the ceiling and then expand
|
|
284
284
|
// several-fold past it during the copy -- the allocation the ceiling exists to prevent.
|
|
285
|
-
// Buffer.byteLength computes the encoded size
|
|
285
|
+
// Buffer.byteLength computes the encoded size without producing the buffer, so the bound still
|
|
286
286
|
// bites before anything is materialized. (A conforming BLOB is base64url and dots, hence ASCII,
|
|
287
287
|
// where the two counts agree; a non-ASCII one is not a JWS at all and is refused on size or on
|
|
288
288
|
// shape immediately after.)
|
|
@@ -308,17 +308,17 @@ function _verifyMetadataBlob(blob, opts) {
|
|
|
308
308
|
}
|
|
309
309
|
// The signature is bounded on the same principle and for the same reason: it is read before
|
|
310
310
|
// anything is authenticated, and every algorithm here has a tightly bounded signature size, so a
|
|
311
|
-
// segment consuming the whole envelope allowance is not a signature
|
|
311
|
+
// segment consuming the whole envelope allowance is not a signature; it is a request to
|
|
312
312
|
// allocate megabytes and hand them to the verifier without possessing any key.
|
|
313
313
|
if (segs[2].length > Math.ceil(C.MDS_BLOB_SIG_MAX_BYTES / 3) * 4) {
|
|
314
314
|
throw _err("webauthn/too-large", "the metadata BLOB signature is above the " + C.MDS_BLOB_SIG_MAX_BYTES + "-byte ceiling");
|
|
315
315
|
}
|
|
316
316
|
var header, sig;
|
|
317
317
|
try {
|
|
318
|
-
// The header goes through the
|
|
318
|
+
// The header goes through the same bounded reader as the payload: a duplicate `alg` or `x5c`
|
|
319
319
|
// must not be resolvable to whichever copy a permissive parser happens to keep.
|
|
320
320
|
header = guard.json.parse(Buffer.from(jose.base64url.decode(segs[0])), _err, {
|
|
321
|
-
// The header is read
|
|
321
|
+
// The header is read before anything is authenticated, so its ceiling is the header's own,
|
|
322
322
|
// not the whole BLOB's: a JWS protected header is a few hundred bytes, and giving this reader
|
|
323
323
|
// the 32 MiB envelope cap would let unauthenticated bytes buy an unbounded multiple of that
|
|
324
324
|
// in heap ahead of the signature check. The payload's reader keeps the envelope cap, which is
|
|
@@ -346,7 +346,7 @@ function _verifyMetadataBlob(blob, opts) {
|
|
|
346
346
|
// and a JWS naming one this implementation does not is invalid. Ignoring it is the same fault as
|
|
347
347
|
// ignoring x5u, one parameter over: the producer said "refuse this unless you handle it" and a
|
|
348
348
|
// reader that skips the list accepts a token on terms it never met. Nothing here processes an
|
|
349
|
-
// extension parameter, so any name is unprocessed
|
|
349
|
+
// extension parameter, so any name is unprocessed, including a standard name, which sec. 4.1.11
|
|
350
350
|
// forbids listing at all.
|
|
351
351
|
if (Object.prototype.hasOwnProperty.call(header, "crit")) {
|
|
352
352
|
var crit = header.crit;
|
|
@@ -404,13 +404,13 @@ function _verifyMetadataBlob(blob, opts) {
|
|
|
404
404
|
}
|
|
405
405
|
}
|
|
406
406
|
|
|
407
|
-
// An Edwards leaf key is validated on-curve and full-order
|
|
407
|
+
// An Edwards leaf key is validated on-curve and full-order before it is imported. The identity
|
|
408
408
|
// point and the other low-order points are accepted by the platform and verify a trivial
|
|
409
409
|
// signature over ANY message, so a leaf carrying one authenticates whatever payload it is shown.
|
|
410
410
|
// Chaining to the caller's FIDO root is not protection here: the point is malformed, not
|
|
411
411
|
// unissued, and the certificate that carries it can be perfectly well signed. This is the same
|
|
412
|
-
// gate every other Edwards key in the toolkit passes
|
|
413
|
-
// validator, sigstore, JOSE
|
|
412
|
+
// gate every other Edwards key in the toolkit passes (the attestation path, CMS, the path
|
|
413
|
+
// validator, sigstore, JOSE), and a new verification route skipping it is how one key type comes
|
|
414
414
|
// to be checked everywhere except the newest door.
|
|
415
415
|
if (leafAlg === "Ed25519" || leafAlg === "Ed448") {
|
|
416
416
|
edwardsPoint.validateSpki(leaf.subjectPublicKeyInfo.bytes, leafAlg === "Ed25519" ? 6 : 7,
|
|
@@ -421,7 +421,7 @@ function _verifyMetadataBlob(blob, opts) {
|
|
|
421
421
|
return webcrypto.webcrypto.subtle.importKey("spki", leaf.subjectPublicKeyInfo.bytes, params.imp, false, ["verify"])
|
|
422
422
|
.then(function (key) {
|
|
423
423
|
// The VERIFY is wrapped as well as the import. A rejection handler attached to the import
|
|
424
|
-
// alone leaves anything the verify itself rejects with
|
|
424
|
+
// alone leaves anything the verify itself rejects with, such as a certificate whose RSASSA-PSS
|
|
425
425
|
// parameters demand a longer salt than the algorithm supplies imports cleanly and then fails
|
|
426
426
|
// inside OpenSSL -- to escape as a raw platform Error, out of a verb whose whole contract is
|
|
427
427
|
// that every failure is a typed webauthn/* verdict.
|
|
@@ -433,7 +433,7 @@ function _verifyMetadataBlob(blob, opts) {
|
|
|
433
433
|
return _chainToAnchor(chain, anchors, at);
|
|
434
434
|
})
|
|
435
435
|
.then(function () {
|
|
436
|
-
//
|
|
436
|
+
// Only now is the payload read. Everything above establishes that these bytes came from the
|
|
437
437
|
// holder of a key the caller anchored; parsing before that would expose the JSON reader, the
|
|
438
438
|
// entry walk and every per-entry decode to bytes nobody vouched for.
|
|
439
439
|
return _parsePayload(segs[1], at, opts);
|
|
@@ -442,11 +442,11 @@ function _verifyMetadataBlob(blob, opts) {
|
|
|
442
442
|
|
|
443
443
|
// The x5c chain must validate to one of the caller's anchors. Every anchor is tried because an
|
|
444
444
|
// operator may hold several across a rotation; the last path verdict is threaded as the cause so a
|
|
445
|
-
// caller can see
|
|
445
|
+
// caller can see why it did not chain, and not merely that it did not.
|
|
446
446
|
//
|
|
447
447
|
// `what` and `code` name the caller's subject and its own refusal code, so every chain in the
|
|
448
448
|
// namespace -- the BLOB's, an attestation's, an android-safetynet service chain -- reaches its
|
|
449
|
-
// anchors through
|
|
449
|
+
// anchors through this same walk while keeping the verdict its own callers already handle. A second copy
|
|
450
450
|
// of the walk is not merely duplication: the anchor-stripping rule is subtle (a terminal
|
|
451
451
|
// certificate that IS the anchor is identified by subject name AND public key, which is what
|
|
452
452
|
// recognises a cross-signed root), and a copy that got it slightly differently would refuse a valid
|
|
@@ -459,18 +459,18 @@ function _chainToAnchor(chain, anchors, at, what, code) {
|
|
|
459
459
|
return anchors.reduce(function (p, anchor) {
|
|
460
460
|
return p.then(function (done) {
|
|
461
461
|
if (done) return true;
|
|
462
|
-
// A terminal certificate that
|
|
462
|
+
// A terminal certificate that is the anchor is the anchor, not a path element, so validating it
|
|
463
463
|
// against itself would be a different assertion from the one being made. What identifies it is
|
|
464
464
|
// the trust-anchor identity the validator itself uses: the SUBJECT NAME and the PUBLIC KEY. A
|
|
465
465
|
// self-issued test instead of a key comparison misses the cross-signed form of the same root,
|
|
466
|
-
// which carries that identity but was signed by a cross-signing CA
|
|
466
|
+
// which carries that identity but was signed by a cross-signing CA, so it would be left in
|
|
467
467
|
// the path and then fail to verify under the anchor that never issued it, refusing an
|
|
468
468
|
// otherwise valid chain. Matching on name AND key cannot be looser: two certificates naming
|
|
469
469
|
// one subject with one key ARE the same entity for anchoring, whoever signed them.
|
|
470
470
|
var path = ordered.slice();
|
|
471
471
|
var strippedAnchor = false;
|
|
472
472
|
if (path.length && _isAnchorItself(path[0], anchor)) { path = path.slice(1); strippedAnchor = true; }
|
|
473
|
-
// Nothing left to validate. The two ways of getting here are
|
|
473
|
+
// Nothing left to validate. The two ways of getting here are not the same, and the difference
|
|
474
474
|
// is the whole verdict. If the anchor itself was the entire chain, then the signature was
|
|
475
475
|
// verified under the anchor's own key -- the identity was established by name AND key, so
|
|
476
476
|
// there is nothing further to chain and it is trusted. If the path was empty for any other
|
|
@@ -479,7 +479,7 @@ function _chainToAnchor(chain, anchors, at, what, code) {
|
|
|
479
479
|
if (path.length === 0) return strippedAnchor;
|
|
480
480
|
return pathValidate.validate(path, {
|
|
481
481
|
time: at,
|
|
482
|
-
// The anchor tuple names the anchor's own
|
|
482
|
+
// The anchor tuple names the anchor's own key: its SubjectPublicKeyInfo algorithm and that
|
|
483
483
|
// algorithm's parameters -- not the algorithm its issuer used to sign it. The validator
|
|
484
484
|
// carries these forward as the working public key, and a certificate below the anchor may
|
|
485
485
|
// inherit its key parameters from them (the DSA-style parameter-inheritance case), so
|
|
@@ -497,8 +497,8 @@ function _chainToAnchor(chain, anchors, at, what, code) {
|
|
|
497
497
|
}
|
|
498
498
|
|
|
499
499
|
// The instant a catalogue stops being current. `nextUpdate` is a DATE, so the BLOB is current
|
|
500
|
-
// through the
|
|
501
|
-
// BLOB is verified, and again whenever a verified result is
|
|
500
|
+
// through the end of that UTC day. One home, because the rule is applied twice (once when the
|
|
501
|
+
// BLOB is verified, and again whenever a verified result is used) and two copies would drift.
|
|
502
502
|
function _staleAfter(nextUpdate) {
|
|
503
503
|
var d = rfc3339.parseDate(nextUpdate, function (c, m) { return _err("webauthn/bad-metadata-blob", m); },
|
|
504
504
|
"webauthn/bad-metadata-blob", "the metadata BLOB nextUpdate");
|
|
@@ -506,11 +506,11 @@ function _staleAfter(nextUpdate) {
|
|
|
506
506
|
}
|
|
507
507
|
|
|
508
508
|
// A verified result is a plain object the caller may hold for as long as it likes, so its freshness
|
|
509
|
-
// has to be re-established every time it
|
|
509
|
+
// has to be re-established every time it decides something, not only when it was parsed. A
|
|
510
510
|
// catalogue fetched before its nextUpdate and reused a month later would otherwise keep authorizing
|
|
511
511
|
// an authenticator whose status reports have since revoked it, which is precisely what nextUpdate
|
|
512
512
|
// exists to prevent. The caller's original allowStale decision is carried on the result and honoured
|
|
513
|
-
// here, so opting out stays opted out
|
|
513
|
+
// here, so opting out stays opted out and never silently reappears at the point of use.
|
|
514
514
|
function assertFresh(metadata, at, label) {
|
|
515
515
|
if (!metadata || metadata.allowStale === true || typeof metadata.nextUpdate !== "string") return;
|
|
516
516
|
var atMs = at instanceof Date ? at.getTime() : NaN;
|
|
@@ -542,7 +542,7 @@ function _parsePayload(seg, at, opts) {
|
|
|
542
542
|
if (typeof payload.legalHeader !== "string") throw _err("webauthn/bad-metadata-blob", "the metadata BLOB payload must carry a string legalHeader");
|
|
543
543
|
if (!Number.isSafeInteger(payload.no) || payload.no < 0) throw _err("webauthn/bad-metadata-blob", "the metadata BLOB payload must carry a non-negative integer no");
|
|
544
544
|
// Rollback: a BLOB older than the one already held would reinstate authenticators whose trust was
|
|
545
|
-
// withdrawn since, so it is refused
|
|
545
|
+
// withdrawn since, so it is refused, not merely reported.
|
|
546
546
|
if (opts.previousNo !== undefined && payload.no <= opts.previousNo) {
|
|
547
547
|
throw _err("webauthn/metadata-rollback", "the metadata BLOB no " + payload.no + " does not exceed the previously held " + opts.previousNo);
|
|
548
548
|
}
|
|
@@ -553,7 +553,7 @@ function _parsePayload(seg, at, opts) {
|
|
|
553
553
|
//
|
|
554
554
|
// Unreachable as the code stands, and kept deliberately. Both operands are already gated by
|
|
555
555
|
// throwing guards -- `at` by guard.time.assertValid at the entry, `staleAfter` by
|
|
556
|
-
// rfc3339.parseDate above, which refuses a date that does not exist
|
|
556
|
+
// rfc3339.parseDate above, which refuses a date that does not exist and never rolls it over,
|
|
557
557
|
// so no caller input reaches here non-finite. It stays because the cost is one comparison and the
|
|
558
558
|
// failure it catches is silent: if either gate is ever relaxed or moved, this is what keeps a NaN
|
|
559
559
|
// from being read as fresh instead of stale.
|
|
@@ -585,7 +585,7 @@ function _parsePayload(seg, at, opts) {
|
|
|
585
585
|
" status reports, above the " + C.MDS_MAX_STATUS_REPORTS_PER_ENTRY + " ceiling");
|
|
586
586
|
}
|
|
587
587
|
// sec. 3.1.3 makes `status` REQUIRED on every report. A report that is not an object, or that
|
|
588
|
-
// omits it, is refused
|
|
588
|
+
// omits it, is refused here and never skipped by the status gate: the gate reads a missing
|
|
589
589
|
// status as "nothing disqualifying", so a malformed report would be silently read as a clean
|
|
590
590
|
// bill of health for the authenticator whose status it was supposed to carry.
|
|
591
591
|
e.statusReports.forEach(function (r, ri) {
|
|
@@ -628,7 +628,7 @@ function _parsePayload(seg, at, opts) {
|
|
|
628
628
|
// A SHA-1 key identifier is 40 hex digits (RFC 5280 sec. 4.2.1.2 method 1). Anything else
|
|
629
629
|
// cannot be what a certificate hashes to, so accepting it would add a key that never
|
|
630
630
|
// matches -- an entry that silently cannot be found. sec. 3.1.1 requires lower case; the
|
|
631
|
-
// value is canonicalized
|
|
631
|
+
// value is canonicalized, not refused, so a catalogue that differs only in letter
|
|
632
632
|
// case still resolves instead of denying service for every authenticator it lists.
|
|
633
633
|
if (typeof k !== "string" || !/^[0-9a-fA-F]{40}$/.test(k)) {
|
|
634
634
|
throw _err("webauthn/bad-metadata-blob", "metadata entry " + i + " has a malformed attestation certificate key identifier");
|
|
@@ -636,14 +636,14 @@ function _parsePayload(seg, at, opts) {
|
|
|
636
636
|
var lower = k.toLowerCase();
|
|
637
637
|
// The entry and its statement legitimately repeat an identifier; that is one entry naming
|
|
638
638
|
// itself twice, not two entries claiming one authenticator, so it is deduplicated here
|
|
639
|
-
//
|
|
639
|
+
// and never reaches the cross-entry duplicate check below.
|
|
640
640
|
if (!seenKeyId[lower]) { seenKeyId[lower] = 1; keyIds.push(lower); }
|
|
641
641
|
});
|
|
642
642
|
});
|
|
643
643
|
var out = { index: i, aaguid: aaguid, keyIdentifiers: keyIds, statusReports: e.statusReports,
|
|
644
644
|
metadataStatement: st || null, timeOfLastStatusChange: e.timeOfLastStatusChange || null };
|
|
645
645
|
if (aaguid) {
|
|
646
|
-
// A duplicate identifier is refused
|
|
646
|
+
// A duplicate identifier is refused, never resolved by position: two entries claiming
|
|
647
647
|
// one authenticator give the lookup a choice the specification does not define.
|
|
648
648
|
if (byAaguid[aaguid]) throw _err("webauthn/duplicate-metadata-entry", "two metadata entries claim aaguid " + aaguid);
|
|
649
649
|
byAaguid[aaguid] = out;
|
|
@@ -656,21 +656,21 @@ function _parsePayload(seg, at, opts) {
|
|
|
656
656
|
});
|
|
657
657
|
// The rollback rule leaves a trace, exactly as the freshness rule does either side of it. It runs
|
|
658
658
|
// only when a caller supplies the sequence number it already holds, so a result that does not say
|
|
659
|
-
// whether it ran cannot be told apart from one where the check was skipped
|
|
659
|
+
// whether it ran cannot be told apart from one where the check was skipped, and the whole point
|
|
660
660
|
// of the rule is that a caller can show its catalogue never went backwards. `previousNo` is the
|
|
661
|
-
// baseline it was compared against, so the claim is auditable
|
|
661
|
+
// baseline it was compared against, so the claim is auditable, not merely asserted.
|
|
662
662
|
var result = { no: payload.no, legalHeader: payload.legalHeader, nextUpdate: payload.nextUpdate,
|
|
663
663
|
stale: stale, allowStale: opts.allowStale === true,
|
|
664
664
|
rollbackChecked: opts.previousNo !== undefined,
|
|
665
665
|
previousNo: opts.previousNo === undefined ? null : opts.previousNo,
|
|
666
666
|
entries: entries, byAaguid: byAaguid, byKeyIdentifier: byKeyIdentifier,
|
|
667
667
|
statusPolicy: opts.statusPolicy || "any", rejectUnknownStatus: opts.rejectUnknownStatus === true };
|
|
668
|
-
// Frozen
|
|
668
|
+
// Frozen first, then recorded as verified: the mark means "this exact catalogue passed the
|
|
669
669
|
// signature, chain, rollback and freshness gates", and that claim only holds if the object cannot
|
|
670
670
|
// be edited afterwards. This is the only place a catalogue can have been through those gates.
|
|
671
671
|
_deepFreeze(result, 0);
|
|
672
672
|
_verifiedResults.add(result);
|
|
673
|
-
// Recorded
|
|
673
|
+
// Recorded after the freeze, alongside the provenance mark and for the same reason: an entry may
|
|
674
674
|
// only be judged against the catalogue it was read out of.
|
|
675
675
|
entries.forEach(function (e) { _entryOrigin.set(e, result); });
|
|
676
676
|
return result;
|
|
@@ -679,10 +679,10 @@ function _parsePayload(seg, at, opts) {
|
|
|
679
679
|
// The verified metadata entry for an AAGUID, or `null` when the BLOB lists none. `metadata` is a
|
|
680
680
|
// `verifyMetadataBlob` result -- never raw bytes, so a lookup cannot be answered from an
|
|
681
681
|
// unverified BLOB. The all-zero AAGUID means "no model identity" and never matches.
|
|
682
|
-
// It also accepts the identifier a U2F authenticator is keyed by instead
|
|
682
|
+
// It also accepts the identifier a U2F authenticator is keyed by instead: the key identifier of
|
|
683
683
|
// its attestation certificate -- so one verb covers both of the catalogue's key spaces. The two are
|
|
684
684
|
// disjoint by shape (a dashed 36-character UUID against 40 hex digits), so the form is DISPATCHED
|
|
685
|
-
//
|
|
685
|
+
// on, never guessed at: anything matching neither is a miss, not a lookup in whichever table
|
|
686
686
|
// happens to answer.
|
|
687
687
|
function metadataFor(metadata, identifier) {
|
|
688
688
|
if (!isVerifiedResult(metadata)) throw _err("webauthn/bad-input", "metadataFor expects a verifyMetadataBlob result -- an object that merely resembles one, such as a catalogue restored from a cache, has not been through the signature and chain checks");
|
|
@@ -701,7 +701,7 @@ function metadataFor(metadata, identifier) {
|
|
|
701
701
|
var _ANCHOR_OPTS = Object.assign(Object.create(null), { metadata: 1, time: 1, certificate: 1 });
|
|
702
702
|
|
|
703
703
|
// The attestation root certificates an entry's authenticator chains to, decoded on demand. Decoding
|
|
704
|
-
// is deliberately per entry
|
|
704
|
+
// is deliberately per entry, and not for the whole BLOB: a handful of the certificates in the
|
|
705
705
|
// live metadata do not parse under a strict decoder, and decoding everything up front would let one
|
|
706
706
|
// vendor's malformed root refuse the entire BLOB for every authenticator in it.
|
|
707
707
|
function metadataAnchors(entry, opts) {
|
|
@@ -709,7 +709,7 @@ function metadataAnchors(entry, opts) {
|
|
|
709
709
|
opts = opts || {};
|
|
710
710
|
if (typeof opts !== "object" || Array.isArray(opts)) throw _err("webauthn/bad-input", "metadataAnchors opts must be an object");
|
|
711
711
|
guard.identifier.assertKnownKeys(opts, _ANCHOR_OPTS, _err, "webauthn/bad-input", "metadataAnchors opts has an unknown key ");
|
|
712
|
-
//
|
|
712
|
+
// One read of the caller's object: the status gate below decides on these values, and a value
|
|
713
713
|
// read twice is a value that can differ between the check and the use.
|
|
714
714
|
opts = Object.assign({}, opts);
|
|
715
715
|
if (opts.time !== undefined) guard.time.assertValid(opts.time, _err, "webauthn/bad-input", "opts.time");
|
|
@@ -738,7 +738,7 @@ function metadataAnchors(entry, opts) {
|
|
|
738
738
|
// Judged with the same inputs the attestation path uses, so the two readings cannot diverge: the
|
|
739
739
|
// caller's own statusPolicy when the verified result is supplied, the instant being judged, and
|
|
740
740
|
// the attestation certificate actually presented (so a report naming a single certificate is
|
|
741
|
-
// judged against that one
|
|
741
|
+
// judged against that one, so the entry does not deny every device it covers). With none of them,
|
|
742
742
|
// the strictest reading applies -- any disqualifying report, judged now.
|
|
743
743
|
var at = opts.time === undefined ? new Date() : opts.time;
|
|
744
744
|
if (opts.metadata !== undefined) assertFresh(opts.metadata, at, "metadataAnchors");
|
|
@@ -761,11 +761,11 @@ function metadataAnchors(entry, opts) {
|
|
|
761
761
|
|
|
762
762
|
// Does this entry's status deny trust? Default: ANY disqualifying report denies, wherever it sits
|
|
763
763
|
// in the array. The array is not stated to be chronological, `effectiveDate` is optional, and in
|
|
764
|
-
// the live metadata a number of entries are not in date order
|
|
764
|
+
// the live metadata a number of entries are not in date order, and one of them flipping its verdict
|
|
765
765
|
// between "last element" and "newest by date", in the direction that matters. A caller who wants
|
|
766
766
|
// the by-date reading asks for it.
|
|
767
767
|
// A status report may name the single certificate it concerns (MDS v3.0 sec. 3.1.3 `certificate`),
|
|
768
|
-
// and a key-compromise report that does so is about
|
|
768
|
+
// and a key-compromise report that does so is about that one attestation key, not about every
|
|
769
769
|
// authenticator the entry covers. A whole batch is often listed under one entry, so ignoring the
|
|
770
770
|
// scoping would refuse registrations from devices whose key was never compromised. A report that
|
|
771
771
|
// names nothing applies to the entry as a whole, and a report whose named certificate cannot be
|
|
@@ -788,7 +788,7 @@ function _reportInForceAt(report, atMs) {
|
|
|
788
788
|
var d = rfc3339.parseDate(report.effectiveDate, function (c, m) { return _err("webauthn/bad-metadata-blob", m); },
|
|
789
789
|
"webauthn/bad-metadata-blob", "a status report effectiveDate");
|
|
790
790
|
// Only ever called with a report whose effectiveDate rfc3339.isValidDate already accepted, and
|
|
791
|
-
// parseDate throws
|
|
791
|
+
// parseDate throws and never returns an Invalid Date, so d is finite; atMs was isFinite-
|
|
792
792
|
// checked by the caller before this runs.
|
|
793
793
|
// allow:nan-date-comparison-unguarded -- both operands are source-validated, as described above.
|
|
794
794
|
return d.getTime() <= atMs;
|
|
@@ -805,17 +805,17 @@ function statusDenied(entry, metadata, leaf, at) {
|
|
|
805
805
|
// For the built-in policies, reports that demonstrably concern a DIFFERENT certificate are removed
|
|
806
806
|
// first, before recency is considered. Scope has to be settled before recency, or under
|
|
807
807
|
// latest-by-date such a report could be selected as the newest, displace an older model-wide
|
|
808
|
-
// revocation, and then be discarded as inapplicable
|
|
808
|
+
// revocation, and then be discarded as inapplicable, clearing the entry using a report that was
|
|
809
809
|
// never about this authenticator at all.
|
|
810
810
|
reports = reports.filter(function (r) {
|
|
811
811
|
return !(r && typeof r.status === "string" && CERT_SCOPED_STATUS[r.status] && _reportNamesOtherCert(r, leaf));
|
|
812
812
|
});
|
|
813
|
-
// A report dated
|
|
814
|
-
// belongs here
|
|
815
|
-
// revocation would otherwise deny every registration from the moment it is published
|
|
813
|
+
// A report dated after the instant being judged has not taken effect, whatever the policy. This
|
|
814
|
+
// belongs here, not inside one policy's branch: under the default reading, a scheduled
|
|
815
|
+
// revocation would otherwise deny every registration from the moment it is published instead of
|
|
816
816
|
// from the date it names, and a deliberately historical verification would see reports filed
|
|
817
817
|
// after the time it asks about. When every dated report is still in the future, the answer is
|
|
818
|
-
// that none of them is in force
|
|
818
|
+
// that none of them is in force, not that all of them are.
|
|
819
819
|
var isDated = function (r) { return r && typeof r.effectiveDate === "string" && rfc3339.isValidDate(r.effectiveDate); };
|
|
820
820
|
var atMs = (at instanceof Date && isFinite(at.getTime())) ? at.getTime() : null;
|
|
821
821
|
if (atMs !== null) {
|
|
@@ -825,14 +825,14 @@ function statusDenied(entry, metadata, leaf, at) {
|
|
|
825
825
|
if (policy === "latest-by-date") {
|
|
826
826
|
var dated = reports.filter(isDated);
|
|
827
827
|
if (dated.length) {
|
|
828
|
-
//
|
|
828
|
+
// Every report on the newest date, not the first one found there. Reducing to a single report
|
|
829
829
|
// makes a tie resolve by array position: a same-day clean report and a same-day REVOKED would
|
|
830
830
|
// deny or not depending purely on which the catalogue happened to list first, and reversing
|
|
831
831
|
// the array would flip the verdict. Reports that are equally recent are equally current, so a
|
|
832
832
|
// disqualifying one among them cannot be discarded.
|
|
833
833
|
var newest = dated.reduce(function (a, b) { return a.effectiveDate >= b.effectiveDate ? a : b; }).effectiveDate;
|
|
834
834
|
// effectiveDate is OPTIONAL (sec. 3.1.3), and a report without one cannot be shown to be
|
|
835
|
-
// older than anything
|
|
835
|
+
// older than anything, so it is kept and never dropped. Discarding it would let an entry
|
|
836
836
|
// clear an undated REVOKED simply by adding a dated clean report, which is the fail-open this
|
|
837
837
|
// policy is most likely to be reached for. Where the ordering cannot be established, the
|
|
838
838
|
// report still counts.
|
|
@@ -875,12 +875,12 @@ function aaguidToString(buf) {
|
|
|
875
875
|
// match nothing in the catalogue and silently turn every U2F lookup into a miss.
|
|
876
876
|
//
|
|
877
877
|
// This is the same value pki-build's spkiKeyId derives for the subjectKeyIdentifier extension, from
|
|
878
|
-
// the DER
|
|
878
|
+
// the DER, and not from a parsed certificate. Two derivations of one definition can drift, and a
|
|
879
879
|
// drift here is silent -- every U2F lookup simply stops matching -- so a vector pins the value
|
|
880
|
-
// against a real certificate's
|
|
880
|
+
// against a real certificate's own subjectKeyIdentifier extension, which is the independent oracle
|
|
881
881
|
// for method 1 and fails the moment either derivation changes.
|
|
882
882
|
//
|
|
883
|
-
// The algorithm is not a choice: SHA-1 is what the standard names, this is an
|
|
883
|
+
// The algorithm is not a choice: SHA-1 is what the standard names, and this is an identifier, not
|
|
884
884
|
// a signature or an integrity check, and the identity it labels is re-established by the certificate
|
|
885
885
|
// chain validation that follows. Choosing a stronger hash would produce a value the catalogue does
|
|
886
886
|
// not contain.
|
|
@@ -891,13 +891,13 @@ function certKeyIdentifier(cert) {
|
|
|
891
891
|
}
|
|
892
892
|
// Collision resistance is not the property relied on here: a second key hashing to the same
|
|
893
893
|
// identifier would resolve to the same catalogue entry, and its certificate would then still have
|
|
894
|
-
// to validate to the roots
|
|
894
|
+
// to validate to the roots that same entry registers, which is the check that actually grants trust.
|
|
895
895
|
// nosemgrep: pki-weak-hash-md5-sha1
|
|
896
896
|
return nodeCrypto.createHash("sha1").update(pk.bytes).digest("hex");
|
|
897
897
|
}
|
|
898
898
|
|
|
899
899
|
// The verified metadata entry registering an attestation-certificate key identifier, or null. This
|
|
900
|
-
// is the lookup for an authenticator with no AAGUID
|
|
900
|
+
// is the lookup for an authenticator with no AAGUID (the U2F case) and it takes a
|
|
901
901
|
// verifyMetadataBlob result for the same reason metadataFor does: an unverified catalogue must not
|
|
902
902
|
// be able to answer which roots an authenticator is allowed to chain to.
|
|
903
903
|
function metadataForKeyIdentifier(metadata, keyId) {
|