@blamejs/pki 0.5.28 → 0.5.29
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 +23 -1
- package/lib/acme.js +50 -25
- package/lib/path-validate.js +441 -18
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,29 @@ All notable changes to `@blamejs/pki` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
-
## v0.5.
|
|
7
|
+
## v0.5.29 — 2026-08-24
|
|
8
|
+
|
|
9
|
+
pki.path.validate refuses a mis-shaped trust anchor instead of returning a verdict against it, and accepts a parsed certificate directly.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `pki.path.anchorFromCert(cert)` turns a parsed certificate into the `{ name, publicKey, algorithm }` trust-anchor tuple `pki.path.validate` and `pki.path.build` consume, so a root can be pinned from its certificate without hand-building the tuple. `pki.path.validate` and `pki.path.build` also accept a parsed certificate passed directly as the anchor.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- A normalized trust anchor -- what `pki.path.anchorFromCert` returns and what `pki.path.build` reports as `result.trustAnchor` -- carries a defined set of fields: `name`, `publicKey`, `algorithm`, and `parameters`, plus `purposes`, `distrustAfter`, `subjectDer`, `label`, and `mozillaCaPolicy` when the source supplies them (the fields `pki.trust.parseCertdata` emits). These survive normalization, so a pinned trust-store entry round-trips; a field attached outside that set is not carried onto the anchor.
|
|
18
|
+
- A trust anchor's `purposes` and `distrustAfter` constraint maps must be plain own data properties. A map -- or one of its entries -- supplied as a getter, or reached through the object's prototype rather than as an own property, is refused with `path/bad-input` rather than read, so a getter cannot answer a purpose or distrust check inconsistently and a restriction is never silently dropped. The `{ purposes, distrustAfter }` shape `pki.trust.parseCertdata` emits and a hand-built one are the normal, unaffected form.
|
|
19
|
+
- A trust-anchor tuple's nested `algorithm.oid` and `name.rdns` must likewise be plain own data properties. An accessor-backed or prototype-inherited definition is refused with `path/bad-input` rather than read, so a getter on one nested field cannot rewrite the other -- for example, rewriting a wrong declared algorithm OID to the key's real one -- before it is validated. A plain `{ oid }` algorithm and `{ rdns, bytes }` name, and a parsed certificate's own, are the normal, unaffected form.
|
|
20
|
+
- The ACME client's per-request methods refuse an unrecognized option instead of reading it as absent and applying a default: `finalize`, `pollOrder` / `pollAuthorization`, `downloadCertificate`, `revokeCert`, `keyChange`, and `renewalWindow`. A mistyped option name is now an `acme/bad-input` error rather than a silently ignored setting, matching the client constructor and the rest of the toolkit's option-taking verbs.
|
|
21
|
+
|
|
22
|
+
### Security
|
|
23
|
+
|
|
24
|
+
- `pki.path.validate` refuses a malformed `opts.trustAnchor` with `path/bad-input` at entry, rather than seeding the path from it and returning a soft verdict. An anchor tuple missing its `algorithm` could previously make the path validate and return `valid: true` -- a self-describing key algorithm filled the gap the absent field left -- so a caller who passed a mis-shaped anchor received a verdict that did not answer the question they asked. The anchor is now normalized and shape-checked at the door, the same way `pki.path.build` already treated its `trustAnchors`.
|
|
25
|
+
- `pki.path.validate` and `pki.path.build` also refuse a trust anchor whose declared `algorithm` does not match the algorithm of its `publicKey`, and take the key's algorithm parameters from that SubjectPublicKeyInfo rather than a declared `parameters` field. An anchor that named a different or unrelated algorithm than the key it carried was previously accepted and validated against the real key; a declared algorithm inconsistent with the key is now a `path/bad-input` refusal at entry. And because the key's own parameters are authoritative, a declared curve that disagreed with the key can no longer be promoted and inherited by an intermediate certificate that omits its own parameters -- which could otherwise validate a chain RFC 5280 parameter inheritance from the real key should reject.
|
|
26
|
+
- A trust anchor passed as a parsed certificate is recognized as a certificate before any tuple field is read, so a value reached through the object's prototype -- such as a polluted `Object.prototype` supplying `name`, `publicKey`, and `algorithm` -- cannot reclassify the certificate as a hand-built tuple and bind a substituted key. The certificate's own key is always the one used.
|
|
27
|
+
- A trust anchor supplied as a `Proxy`, or one whose `purposes` / `distrustAfter` constraint map is a `Proxy`, is refused with `path/bad-input`. A `Proxy`'s traps can answer a field read differently on successive lookups or report a field absent while forwarding the rest, so no field-by-field normalization can trust it to describe itself -- a `Proxy` could report `purposes` absent while carrying the other fields, or a `Proxy` distrust map could report no keys, dropping a `{ serverAuth: false }` restriction or an expired-cutoff date the caller attached and validating a path the anchor forbids. A normal anchor -- a plain tuple, a parsed certificate, or an object inheriting from one, with plain-object constraint maps -- is not a `Proxy` and is unaffected.
|
|
28
|
+
|
|
29
|
+
## v0.5.28 — 2026-08-22
|
|
8
30
|
|
|
9
31
|
Importing the toolkit is now silent, and a key's WebCrypto algorithm can no longer change under a signature once the key has been created.
|
|
10
32
|
|
package/lib/acme.js
CHANGED
|
@@ -1955,6 +1955,28 @@ var _CLIENT_OPTS = {
|
|
|
1955
1955
|
accountKey: 1, accountJwk: 1, alg: 1, transport: 1, tls: 1, timeout: 1, maxResponseBytes: 1,
|
|
1956
1956
|
maxRedirects: 1, maxNonceRetries: 1, maxPolls: 1, maxTotalWait: 1, sleep: 1, clock: 1,
|
|
1957
1957
|
};
|
|
1958
|
+
// The per-request options each client method reads. A near-miss spelling in a method's own bag is
|
|
1959
|
+
// refused the same way the constructor refuses one, rather than read as undefined and silently
|
|
1960
|
+
// defaulted -- a mistyped `requireBinding` or `reason` would otherwise change the request with no error.
|
|
1961
|
+
// finalize accepts `identifiers` but NEVER lets it override the order's own set (RFC 8555 sec. 7.4), so
|
|
1962
|
+
// it is recognized here though the binding ignores it.
|
|
1963
|
+
var _METHOD_OPTS = {
|
|
1964
|
+
finalize: { csr: 1, identifiers: 1 },
|
|
1965
|
+
poll: { maxPolls: 1, maxTotalWait: 1, onRetryAfter: 1 },
|
|
1966
|
+
download: { expectedSpki: 1, identifiers: 1, requireBinding: 1, selectChain: 1, maxAlternates: 1 },
|
|
1967
|
+
revoke: { certAlg: 1, certJwk: 1, certKey: 1, certificate: 1, reason: 1 },
|
|
1968
|
+
keyChange: { newAlg: 1, newJwk: 1, newKey: 1 },
|
|
1969
|
+
renewalWindow: { random: 1, clock: 1, replaced: 1, previous: 1 },
|
|
1970
|
+
};
|
|
1971
|
+
// Settle a per-method options bag the way the constructor settles its own: guard.identifier.optionsObject
|
|
1972
|
+
// snapshots the bag and refuses one a getter rewrites mid-read (a `requireBinding` getter that grows a
|
|
1973
|
+
// misspelled key after the check would otherwise slip past assertKnownKeys), THEN reject any unknown key.
|
|
1974
|
+
// Returns the settled bag so the method reads the frozen copy, never the live object a getter could rewrite.
|
|
1975
|
+
function _gateOpts(bag, whitelist, name) {
|
|
1976
|
+
bag = guard.identifier.optionsObject(bag, E, "acme/bad-input", "pki.acme " + name + " options");
|
|
1977
|
+
guard.identifier.assertKnownKeys(bag, whitelist, E, "acme/bad-input", "unknown " + name + " option ");
|
|
1978
|
+
return bag;
|
|
1979
|
+
}
|
|
1958
1980
|
function client(directoryUrl, opts) {
|
|
1959
1981
|
opts = guard.identifier.optionsObject(opts, E, "acme/bad-input", "pki.acme.client options");
|
|
1960
1982
|
guard.identifier.assertKnownKeys(opts, _CLIENT_OPTS, E, "acme/bad-input", "unknown pki.acme.client option ");
|
|
@@ -2216,7 +2238,7 @@ function client(directoryUrl, opts) {
|
|
|
2216
2238
|
|
|
2217
2239
|
function _finalize(order, o) { return _promised(function () { return _finalizeBody(order, o); }); }
|
|
2218
2240
|
function _finalizeBody(order, o) {
|
|
2219
|
-
o = o
|
|
2241
|
+
o = _gateOpts(o, _METHOD_OPTS.finalize, "finalize");
|
|
2220
2242
|
if (!_isObject(order) || !_isString(order.finalize)) throw E("acme/bad-input", "finalize requires the order object with its finalize URL");
|
|
2221
2243
|
if (!Array.isArray(order.identifiers)) throw E("acme/bad-input", "finalize requires the order's identifiers to enforce the RFC 8555 sec. 7.4 CSR-set match");
|
|
2222
2244
|
// The sec. 7.4 CSR-vs-order check is ALWAYS bound to the order's OWN authoritative identifiers (the
|
|
@@ -2229,12 +2251,12 @@ function client(directoryUrl, opts) {
|
|
|
2229
2251
|
// slept via the injectable sleep), bounded by maxPolls + maxTotalWait; each observed pair runs
|
|
2230
2252
|
// assertTransition. A terminal `invalid` surfaces the object (the caller inspects .status).
|
|
2231
2253
|
function _poll(kind, url, budget) {
|
|
2232
|
-
budget = budget || {};
|
|
2233
2254
|
// A per-call poll override is validated through the SAME bounds as the constructor budget, so an
|
|
2234
2255
|
// Infinity / NaN / over-ceiling value cannot turn a non-terminal resource into an unbounded loop.
|
|
2235
2256
|
// Surface a bad override (or url) as a REJECTION, not a sync throw, so this async verb is uniform.
|
|
2236
2257
|
var capPolls, capWait;
|
|
2237
2258
|
try {
|
|
2259
|
+
budget = _gateOpts(budget, _METHOD_OPTS.poll, "poll");
|
|
2238
2260
|
capPolls = budget.maxPolls != null ? guard.limits.cap(budget.maxPolls, "maxPolls", maxPolls, { E: E, code: "acme/bad-input", min: 1, max: 1000 }) : maxPolls;
|
|
2239
2261
|
capWait = budget.maxTotalWait != null ? guard.limits.cap(budget.maxTotalWait, "maxTotalWait", maxTotalWait, { E: E, code: "acme/bad-input", min: 0, max: retryAfter.MAX_RETRY_AFTER_SECONDS }) : maxTotalWait;
|
|
2240
2262
|
url = _clientUrl(url);
|
|
@@ -2346,9 +2368,9 @@ function client(directoryUrl, opts) {
|
|
|
2346
2368
|
}
|
|
2347
2369
|
|
|
2348
2370
|
function _downloadCertificate(url, opts) {
|
|
2349
|
-
opts = opts || {};
|
|
2350
2371
|
// Prelude in a resolved-promise .then so a bad opt / URL is a REJECTION, uniform for a .catch caller.
|
|
2351
2372
|
return Promise.resolve().then(function () {
|
|
2373
|
+
opts = _gateOpts(opts, _METHOD_OPTS.download, "downloadCertificate");
|
|
2352
2374
|
var select = opts.selectChain;
|
|
2353
2375
|
if (select !== undefined && typeof select !== "function") throw E("acme/bad-input", "downloadCertificate opts.selectChain must be a function");
|
|
2354
2376
|
var maxAlt = guard.limits.cap(opts.maxAlternates, "maxAlternates", DEFAULT_MAX_ALTERNATES, { E: E, code: "acme/bad-input", min: 0, max: 64 });
|
|
@@ -2405,26 +2427,29 @@ function client(directoryUrl, opts) {
|
|
|
2405
2427
|
}
|
|
2406
2428
|
|
|
2407
2429
|
function _revokeCert(o) {
|
|
2408
|
-
|
|
2409
|
-
//
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
if (
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2430
|
+
// Bad input surfaces as a REJECTION -- _promised turns a throw into one -- so this async verb stays
|
|
2431
|
+
// uniform for a .catch caller rather than throwing synchronously.
|
|
2432
|
+
return _promised(function () {
|
|
2433
|
+
o = _gateOpts(o, _METHOD_OPTS.revoke, "revokeCert");
|
|
2434
|
+
if (!Buffer.isBuffer(o.certificate)) throw E("acme/bad-input", "revokeCert requires a DER certificate Buffer (opts.certificate)");
|
|
2435
|
+
// Certificate-key mode (RFC 8555 sec. 7.6) needs the COMPLETE triple certKey + certJwk + certAlg: an
|
|
2436
|
+
// incomplete set must NOT fall through to the account-key path (wrong credential), and certAlg must be
|
|
2437
|
+
// explicit -- inheriting the account alg would sign a different-family certificate key under the wrong
|
|
2438
|
+
// JWS algorithm (an ES256 account revoking with an RSA certificate key), which JOSE then rejects.
|
|
2439
|
+
var certKeyMode = o.certKey != null || _isObject(o.certJwk) || o.certAlg != null;
|
|
2440
|
+
if (certKeyMode && !(o.certKey != null && _isObject(o.certJwk) && _isString(o.certAlg))) throw E("acme/bad-input", "revokeCert certificate-key mode requires certKey, certJwk, AND certAlg together, or none (RFC 8555 sec. 7.6)");
|
|
2441
|
+
var extra = { certificate: o.certificate };
|
|
2442
|
+
if (o.reason !== undefined) extra.reason = o.reason;
|
|
2443
|
+
return _resource("revokeCert").then(function (url) {
|
|
2444
|
+
if (certKeyMode) {
|
|
2445
|
+
// sign with the certificate key (jwk mode) rather than the account (RFC 8555 sec. 7.6), through
|
|
2446
|
+
// the same _postJws so it inherits the badNonce bounded retry the kid path has.
|
|
2447
|
+
return _postJws(url, function (nonce) {
|
|
2448
|
+
return revokeCert(Object.assign({}, extra, { key: o.certKey, alg: o.certAlg, nonce: nonce, url: url, jwk: o.certJwk }));
|
|
2449
|
+
}).then(function () { return true; });
|
|
2450
|
+
}
|
|
2451
|
+
return _post(url, revokeCert, extra, "kid").then(function () { return true; });
|
|
2452
|
+
});
|
|
2428
2453
|
});
|
|
2429
2454
|
}
|
|
2430
2455
|
|
|
@@ -2433,7 +2458,7 @@ function client(directoryUrl, opts) {
|
|
|
2433
2458
|
|
|
2434
2459
|
function _keyChange(o) { return _promised(function () { return _keyChangeBody(o); }); }
|
|
2435
2460
|
function _keyChangeBody(o) {
|
|
2436
|
-
o = o
|
|
2461
|
+
o = _gateOpts(o, _METHOD_OPTS.keyChange, "keyChange");
|
|
2437
2462
|
if (!o.newKey || !_isObject(o.newJwk) || !_isString(o.newAlg)) throw E("acme/bad-input", "keyChange requires newKey, newJwk, and newAlg");
|
|
2438
2463
|
var account = _requireKid();
|
|
2439
2464
|
return _resource("keyChange").then(function (url) {
|
|
@@ -2502,8 +2527,8 @@ function client(directoryUrl, opts) {
|
|
|
2502
2527
|
// returns the decision as DATA. It never sleeps or schedules: an auto-renewing daemon is an explicit
|
|
2503
2528
|
// future opt-in, not a hidden timer inside the thin client.
|
|
2504
2529
|
function _renewalWindow(certDer, o) {
|
|
2505
|
-
o = o || {};
|
|
2506
2530
|
return Promise.resolve().then(function () {
|
|
2531
|
+
o = _gateOpts(o, _METHOD_OPTS.renewalWindow, "renewalWindow");
|
|
2507
2532
|
if (!Buffer.isBuffer(certDer)) throw E("acme/bad-input", "renewalWindow requires a DER certificate Buffer");
|
|
2508
2533
|
if (o.random !== undefined && typeof o.random !== "function") throw E("acme/bad-input", "renewalWindow opts.random must be a function returning a number in [0, 1]");
|
|
2509
2534
|
if (o.clock !== undefined && typeof o.clock !== "function") throw E("acme/bad-input", "renewalWindow opts.clock must be a function returning epoch milliseconds");
|
package/lib/path-validate.js
CHANGED
|
@@ -84,6 +84,21 @@ function mapsAnyPolicy(mappings) {
|
|
|
84
84
|
|
|
85
85
|
var NS = pkix.makeNS("path", PathError, oid);
|
|
86
86
|
var EXT_DECODERS = pkix.certExtensionDecoders(NS).byOid;
|
|
87
|
+
// Validates a trust-anchor tuple's raw publicKey as a COMPLETE SubjectPublicKeyInfo (AlgorithmIdentifier +
|
|
88
|
+
// key BIT STRING), not just a decodable first OID -- a structurally-incomplete SPKI must fail closed at the
|
|
89
|
+
// anchor door, not slip through to a soft valid:false when key import later rejects it.
|
|
90
|
+
var ANCHOR_SPKI_SCHEMA = pkix.spki(NS);
|
|
91
|
+
// A trust anchor carries a CLOSED, producer-derived field set -- the shape pki.trust.parseCertdata documents
|
|
92
|
+
// and path validation + trust dedup consume: name, publicKey, algorithm, parameters, purposes, distrustAfter,
|
|
93
|
+
// subjectDer, label, mozillaCaPolicy. Normalizing a caller tuple reads each of those fields exactly once (by
|
|
94
|
+
// property access, which follows the prototype chain, so an inherited restriction is kept) and builds a fresh
|
|
95
|
+
// object from those reads -- publicKey pinned to a private copy, algorithm/parameters taken from the validated
|
|
96
|
+
// SubjectPublicKeyInfo (never the caller's declared value), the validation-read metadata (name, purposes,
|
|
97
|
+
// distrustAfter) materialized as DATA because validation reads each more than once and a live accessor could
|
|
98
|
+
// answer inconsistently. entry's own shape is NEVER enumerated (no `in` / ownKeys / getOwnPropertyDescriptor)
|
|
99
|
+
// and the prototype chain is never walked, so a Proxy anchor cannot use an enumeration trap to mutate state
|
|
100
|
+
// mid-normalization and a hostile getPrototypeOf cannot be driven into a loop (Buffer.isBuffer invokes
|
|
101
|
+
// getPrototypeOf once). A field the caller attached OUTSIDE the closed set is not carried. See toAnchor.
|
|
87
102
|
|
|
88
103
|
var subtle = webcrypto.webcrypto.subtle;
|
|
89
104
|
|
|
@@ -332,6 +347,10 @@ function resolveDescriptor(sigAlg) {
|
|
|
332
347
|
function assertKeyMatchesSigAlg(spkiBytes, sigOid, d) {
|
|
333
348
|
if (!d || !d.sameKeyOid) return;
|
|
334
349
|
var keyOid;
|
|
350
|
+
// The working-key SPKI is decodable on the shipped path (the anchor's is validated at toAnchor entry and
|
|
351
|
+
// every chain SPKI is strict-parsed), so this catch is defense-in-depth -- unreachable through
|
|
352
|
+
// pki.path.validate, kept to keep a typed verdict for any future caller that supplies raw key bytes. The
|
|
353
|
+
// RFC 9814 sec. 4 algorithm-confusion check below stays reachable and covered.
|
|
335
354
|
try { keyOid = asn1.read.oid(asn1.decode(spkiBytes).children[0].children[0]); }
|
|
336
355
|
catch (e) { throw E("path/algorithm-mismatch", "cannot read the issuer public-key algorithm identifier", e); }
|
|
337
356
|
if (keyOid !== sigOid) {
|
|
@@ -791,7 +810,7 @@ function checkedSubtreeSeeds(list, optName) {
|
|
|
791
810
|
});
|
|
792
811
|
}
|
|
793
812
|
|
|
794
|
-
function initialize(certs, params, seeds) {
|
|
813
|
+
function initialize(certs, params, seeds, anchor) {
|
|
795
814
|
var n = certs.length;
|
|
796
815
|
return {
|
|
797
816
|
validPolicyTree: rootNode(),
|
|
@@ -804,10 +823,10 @@ function initialize(certs, params, seeds) {
|
|
|
804
823
|
explicitPolicy: params.initialExplicitPolicy ? 0 : n + 1,
|
|
805
824
|
inhibitAnyPolicy: params.initialAnyPolicyInhibit ? 0 : n + 1,
|
|
806
825
|
policyMapping: params.initialPolicyMappingInhibit ? 0 : n + 1,
|
|
807
|
-
workingPublicKeyAlgorithm:
|
|
808
|
-
workingPublicKey:
|
|
809
|
-
workingPublicKeyParameters:
|
|
810
|
-
workingIssuerName:
|
|
826
|
+
workingPublicKeyAlgorithm: anchor.algorithm,
|
|
827
|
+
workingPublicKey: anchor.publicKey,
|
|
828
|
+
workingPublicKeyParameters: anchor.parameters || null,
|
|
829
|
+
workingIssuerName: anchor.name,
|
|
811
830
|
maxPathLength: n,
|
|
812
831
|
userInitialPolicySet: params.userInitialPolicySet || [OID.anyPolicy],
|
|
813
832
|
results: [],
|
|
@@ -1230,6 +1249,10 @@ async function validate(path, opts) {
|
|
|
1230
1249
|
var n = certs.length;
|
|
1231
1250
|
if (n < 1) throw E("path/empty-path", "validate: the certification path is empty");
|
|
1232
1251
|
if (!opts.trustAnchor) throw E("path/bad-input", "validate: a trustAnchor is required");
|
|
1252
|
+
// Normalize the anchor at the door: a parsed root CERTIFICATE becomes a { name, publicKey, algorithm }
|
|
1253
|
+
// tuple, and a mis-shaped tuple fails closed with path/bad-input rather than seeding an undefined
|
|
1254
|
+
// working key that a self-describing SPKI could still validate against (a soft-verdict fail-open).
|
|
1255
|
+
var anchor = toAnchor(opts.trustAnchor, "validate");
|
|
1233
1256
|
// The validity-window check is always on (6.1.3(a)(2)); a missing/invalid
|
|
1234
1257
|
// check date must fail closed, never silently disable it.
|
|
1235
1258
|
guard.time.assertValid(opts.time, E, "path/bad-input", "validate: opts.time (the always-on validity-window check date)");
|
|
@@ -1262,7 +1285,7 @@ async function validate(path, opts) {
|
|
|
1262
1285
|
var requiredEku = purposeOpts.requiredEku;
|
|
1263
1286
|
var checkPurpose = purposeOpts.checkPurpose;
|
|
1264
1287
|
|
|
1265
|
-
var state = initialize(certs, opts, seeds);
|
|
1288
|
+
var state = initialize(certs, opts, seeds, anchor);
|
|
1266
1289
|
state._n = n;
|
|
1267
1290
|
state.requiredEku = requiredEku;
|
|
1268
1291
|
var verifier = opts.verifier || null;
|
|
@@ -1287,7 +1310,7 @@ async function validate(path, opts) {
|
|
|
1287
1310
|
// `distrustAfter.serverAuth` without being told the validation is about serverAuth -- so without
|
|
1288
1311
|
// one the caller's stated intent would be discarded silently, and a root distrusted years ago
|
|
1289
1312
|
// would validate a current leaf. An anchor carrying no such metadata is unaffected.
|
|
1290
|
-
if (!checkPurpose &&
|
|
1313
|
+
if (!checkPurpose && _hasPurposeScopedMetadata(anchor)) {
|
|
1291
1314
|
throw E("path/bad-input", "validate: the trust anchor carries purpose-scoped metadata (distrustAfter / purposes), which is keyed by key purpose -- supply opts.checkPurpose to say which purpose this validation is for, or the constraint cannot be applied");
|
|
1292
1315
|
}
|
|
1293
1316
|
|
|
@@ -1473,7 +1496,7 @@ async function validate(path, opts) {
|
|
|
1473
1496
|
// anchor or an absent checkPurpose is byte-identical to today). The anchor's
|
|
1474
1497
|
// per-purpose distrust-after date and delegator purposes apply to the
|
|
1475
1498
|
// end-entity leaf it ultimately certifies.
|
|
1476
|
-
var ta =
|
|
1499
|
+
var ta = anchor;
|
|
1477
1500
|
// A PRESENT-but-malformed distrustAfter (an Invalid Date: instanceof Date yet a
|
|
1478
1501
|
// NaN time) would make `notBefore > it` NaN-false and SILENTLY drop the distrust
|
|
1479
1502
|
// restriction -- the NaN-Date fail-open. Validate a present date fail-closed
|
|
@@ -2617,13 +2640,25 @@ function _hasPurposeScopedMetadata(ta) {
|
|
|
2617
2640
|
if (!ta || typeof ta !== "object") return false;
|
|
2618
2641
|
return guard.list.anyMatches(["distrustAfter", "purposes"], function (k) {
|
|
2619
2642
|
var m = ta[k];
|
|
2620
|
-
|
|
2643
|
+
// Own names, not Object.keys: a non-enumerable own entry is still a restriction and must count, so it cannot
|
|
2644
|
+
// evade this "the anchor carries purpose-scoped metadata -> require checkPurpose" gate. An inherited map
|
|
2645
|
+
// (Object.create(base) or a polluted Object.prototype) is already refused by toAnchor before an anchor
|
|
2646
|
+
// reaches here, so this read does not resolve an inherited value.
|
|
2647
|
+
return !!m && typeof m === "object" && intrinsic.getOwnPropertyNames(m).length > 0;
|
|
2621
2648
|
});
|
|
2622
2649
|
}
|
|
2623
2650
|
|
|
2624
2651
|
function assertAnchorConstraints(ta, checkPurpose) {
|
|
2625
|
-
|
|
2626
|
-
|
|
2652
|
+
if (!checkPurpose || !ta) return null;
|
|
2653
|
+
// Read distrustAfter ONCE into a local: reading the anchor field for the presence gate and again for the
|
|
2654
|
+
// value would let an accessor-backed field answer the two reads differently and slip the distrust control.
|
|
2655
|
+
// The normalized anchor already materializes this to plain data; reading once holds the guarantee for any
|
|
2656
|
+
// caller and keeps this from being a live-getter multi-read even if a future path skips normalization. An
|
|
2657
|
+
// inherited distrustAfter (Object.create(base), or a polluted Object.prototype) is already refused by toAnchor
|
|
2658
|
+
// -- _captureConstraintMap / the certificate branch reject a `"distrustAfter" in entry` that is not own -- so
|
|
2659
|
+
// this read never resolves an inherited value on an anchor that reached here.
|
|
2660
|
+
var da = ta.distrustAfter;
|
|
2661
|
+
var d = (da && intrinsic.hasOwn(da, checkPurpose)) ? da[checkPurpose] : null;
|
|
2627
2662
|
if (d == null) return null;
|
|
2628
2663
|
return guard.time.assertValid(d, E, "path/bad-input", "trustAnchor.distrustAfter." + checkPurpose);
|
|
2629
2664
|
}
|
|
@@ -2661,24 +2696,411 @@ function resolvePurposeOpts(opts) {
|
|
|
2661
2696
|
return { requiredEku: requiredEku, checkPurpose: checkPurpose };
|
|
2662
2697
|
}
|
|
2663
2698
|
|
|
2664
|
-
|
|
2665
|
-
|
|
2699
|
+
// Snapshot a trust-anchor purposes / distrustAfter map (purpose name -> boolean or Date) to a fresh plain
|
|
2700
|
+
// object, reading each own entry from its DATA DESCRIPTOR -- never by property access. Reading by value would
|
|
2701
|
+
// invoke an accessor entry (a `get serverAuth()`), which is caller code that could mutate the sibling map
|
|
2702
|
+
// before it is captured, so an accessor entry is REFUSED instead. A later getter cannot mutate the fresh copy.
|
|
2703
|
+
// A distrustAfter entry is a Date: copy it BY VALUE (guard.time.instantOf via the intrinsic) so a later
|
|
2704
|
+
// setTime() cannot move the cutoff. Entry enumerability is preserved so the purpose gates see the same shape.
|
|
2705
|
+
function _snapshotConstraintMap(m, E, code, who) {
|
|
2706
|
+
var out = {};
|
|
2707
|
+
// getOwnPropertyNames below reads only OWN keys, so a restriction reached through the prototype --
|
|
2708
|
+
// Object.create({ serverAuth: pastDate }) -- would be silently dropped and its cutoff lost. Require the map to
|
|
2709
|
+
// be a plain object of THIS realm: its prototype is Object.prototype (the common case) or null. Anything else
|
|
2710
|
+
// -- a custom prototype, a null-prototype object carrying entries, a Proxy prototype, or a map from another
|
|
2711
|
+
// realm -- is refused, so a prototype-reached restriction is never silently dropped. Only IDENTITY is sound
|
|
2712
|
+
// here: a hostile object can mimic every STRUCTURAL signal of a genuine Object.prototype (a non-enumerable
|
|
2713
|
+
// data entry is indistinguishable from a built-in), so a structural "looks plain" test always leaves a bypass.
|
|
2714
|
+
// A cross-realm map is therefore refused fail-closed -- the operator passes a same-realm plain object (what
|
|
2715
|
+
// pki.trust.parseCertdata emits, or a literal in their own code), which is the norm for this security-critical
|
|
2716
|
+
// input. A name planted on Object.prototype ITSELF is still tolerated (proto === ObjectProto): the map is plain
|
|
2717
|
+
// and consumption reads OWN entries only (the hasOwn gates), so global pollution is ignored, not read. m is
|
|
2718
|
+
// non-Proxy (refused at capture) and this is a reference comparison, so no getPrototypeOf / ownKeys trap runs
|
|
2719
|
+
// even when the prototype is a Proxy.
|
|
2720
|
+
var proto = intrinsic.getPrototypeOf(m);
|
|
2721
|
+
if (proto !== null && proto !== intrinsic.ObjectProto) throw E(code, who + ": a trustAnchor constraint map must be a plain object (its own entries only), not one with a custom or cross-realm prototype");
|
|
2722
|
+
var keys = intrinsic.getOwnPropertyNames(m);
|
|
2723
|
+
// A constraint map is plain STRING-keyed data. A symbol-keyed entry (getOwnPropertyNames omits symbols) or an
|
|
2724
|
+
// own __proto__ data property (skipped below so it can't pollute the fresh map) would be DROPPED from the
|
|
2725
|
+
// snapshot -- and, keyed by neither a checkPurpose OID name, never applied. Refuse the map rather than silently
|
|
2726
|
+
// drop a caller entry, so the snapshot faithfully carries every restriction or the anchor is refused. ownKeys
|
|
2727
|
+
// (Reflect.ownKeys) counts strings + symbols; more than the string-only names means a symbol key is present.
|
|
2728
|
+
if (intrinsic.ownKeys(m).length !== keys.length) throw E(code, who + ": a trustAnchor constraint map must not have symbol-keyed entries");
|
|
2729
|
+
for (var i = 0; i < keys.length; i++) {
|
|
2730
|
+
if (keys[i] === "__proto__") throw E(code, who + ": a trustAnchor constraint map must not have an own __proto__ entry");
|
|
2731
|
+
var d = intrinsic.getOwnPropertyDescriptor(m, keys[i]);
|
|
2732
|
+
if (!d || !intrinsic.hasOwn(d, "value")) throw E(code, who + ": a trustAnchor constraint-map entry must be a data property, not an accessor");
|
|
2733
|
+
var v = d.value;
|
|
2734
|
+
if (intrinsic.types.isDate(v)) v = new intrinsic.Date(guard.time.instantOf(v));
|
|
2735
|
+
// Materialize every entry ENUMERABLE. A constraint map is plain data (purpose -> boolean / Date); a caller
|
|
2736
|
+
// entry defined non-enumerable would otherwise be copied non-enumerable and then be invisible to the
|
|
2737
|
+
// Object.keys-based purpose-scoped-metadata gate, letting a restriction slip the "supply checkPurpose" check.
|
|
2738
|
+
intrinsic.defineProperty(out, keys[i], { value: v, enumerable: true, configurable: true, writable: true });
|
|
2739
|
+
}
|
|
2740
|
+
return out;
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
// Normalize a captured constraint MAP field for storage. An absent map (null / undefined) carries no
|
|
2744
|
+
// restriction and passes through. A present map MUST be a plain object and is snapshotted; a present non-object
|
|
2745
|
+
// -- a primitive, a Buffer, or (refused inside the snapshot by the plain-prototype check) an array -- cannot hold
|
|
2746
|
+
// a purpose -> boolean/Date restriction, so it is refused rather than passed through as an unusable value that
|
|
2747
|
+
// would silently apply no restriction.
|
|
2748
|
+
function _normalizeConstraintField(v, E, code, who) {
|
|
2749
|
+
if (v === null || v === undefined) return v;
|
|
2750
|
+
if (typeof v !== "object" || intrinsic.isBuffer(v)) throw E(code, who + ": a trustAnchor constraint map must be a plain object");
|
|
2751
|
+
return _snapshotConstraintMap(v, E, code, who);
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
// Capture a trust-anchor constraint MAP field (purposes / distrustAfter) WITHOUT invoking a getter: read its
|
|
2755
|
+
// OWN DATA descriptor. An accessor-backed map (a `get purposes()`) is caller code that could mutate the sibling
|
|
2756
|
+
// map when read, so it is refused; an INHERITED map is refused too (own-only capture), never silently dropped,
|
|
2757
|
+
// so a restriction is never lost. A truly absent field is no restriction (undefined).
|
|
2758
|
+
function _captureConstraintMap(entry, key, E, code, who) {
|
|
2759
|
+
var d = intrinsic.getOwnPropertyDescriptor(entry, key);
|
|
2760
|
+
if (d === undefined) {
|
|
2761
|
+
if (key in entry) throw E(code, who + ": a trustAnchor " + key + " must be an own data property, not inherited");
|
|
2762
|
+
return undefined;
|
|
2763
|
+
}
|
|
2764
|
+
if (!intrinsic.hasOwn(d, "value")) throw E(code, who + ": a trustAnchor " + key + " must be a data property, not an accessor");
|
|
2765
|
+
// A constraint map that is a Proxy is refused, the same way the top-level anchor is: its ownKeys / descriptor
|
|
2766
|
+
// traps could answer inconsistently -- an empty key list drops a restriction the caller attached, and an
|
|
2767
|
+
// isBuffer-answering trap could skip the snapshot entirely -- so the map cannot be trusted to describe itself.
|
|
2768
|
+
if (intrinsic.types.isProxy(d.value)) throw E(code, who + ": a trustAnchor " + key + " map must not be a Proxy");
|
|
2769
|
+
return d.value;
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
// Read a field's OWN DATA value from its descriptor, WITHOUT invoking a getter -- an accessor or an inherited
|
|
2773
|
+
// field yields undefined. Used for the optional identity metadata (subjectDer / label / mozillaCaPolicy): a
|
|
2774
|
+
// getter there, invoked before the key is pinned, could overwrite the key Buffer or mutate the name, so it is
|
|
2775
|
+
// never invoked. This metadata does not enter a validation decision, so an accessor / inherited form is simply
|
|
2776
|
+
// not carried (unlike a constraint map, which is refused).
|
|
2777
|
+
function _captureOwnData(entry, key) {
|
|
2778
|
+
var d = intrinsic.getOwnPropertyDescriptor(entry, key);
|
|
2779
|
+
return (d && intrinsic.hasOwn(d, "value")) ? d.value : undefined;
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
// A fresh copy of a distinguished-name attribute record { type, value, name }, so the normalized anchor does
|
|
2783
|
+
// not share it with the caller. The consumed fields -- type + value drive the RFC 5280 sec. 7.1 dnEqual
|
|
2784
|
+
// comparison, name is the optional registry label -- are read by PROPERTY ACCESS, which follows the prototype
|
|
2785
|
+
// chain and reads an accessor once. The anchor API deliberately accepts an inherited / accessor name.rdns
|
|
2786
|
+
// (Object.create(base), a prototype getter), so an inherited attribute value must be SNAPSHOT here, not
|
|
2787
|
+
// dropped; a field the caller did not supply is omitted.
|
|
2788
|
+
function _copyAtv(atv) {
|
|
2789
|
+
var out = {};
|
|
2790
|
+
var t = atv.type;
|
|
2791
|
+
if (t !== undefined) intrinsic.defineProperty(out, "type", { value: t, enumerable: true, configurable: true, writable: true });
|
|
2792
|
+
var v = atv.value;
|
|
2793
|
+
if (v !== undefined) intrinsic.defineProperty(out, "value", { value: v, enumerable: true, configurable: true, writable: true });
|
|
2794
|
+
var nm = atv.name;
|
|
2795
|
+
if (nm !== undefined) intrinsic.defineProperty(out, "name", { value: nm, enumerable: true, configurable: true, writable: true });
|
|
2796
|
+
return out;
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
// DEEP-copy a caller's rdns so the normalized anchor's DN is independent of the caller's object. A single
|
|
2800
|
+
// arraySlice copies only the outer RDN sequence, leaving each nested RDN array and attribute record shared, so
|
|
2801
|
+
// a caller mutating name.rdns[i][j].value after normalization (or while an async validate awaits signature
|
|
2802
|
+
// verification) would change the issuer identity name chaining re-reads. Copy the outer array, each RDN array,
|
|
2803
|
+
// and each attribute record (its type/value are strings, so an own-data copy is enough) -- matching the
|
|
2804
|
+
// trust-store name copy (lib/trust.js _copyName).
|
|
2805
|
+
function _copyRdnsDeep(rdns) {
|
|
2806
|
+
if (!intrinsic.isArray(rdns)) return rdns;
|
|
2807
|
+
// Copy only OWN indexed elements and PRESERVE holes -- never read an inherited Array.prototype[i] slot. A
|
|
2808
|
+
// slice reads inherited indices (HasProperty walks the chain), so on a SPARSE rdns a polluted Array.prototype
|
|
2809
|
+
// slot would be materialized as an own element, filling a hole that guard.name.dnEqual explicitly rejects and
|
|
2810
|
+
// letting a malformed name pass chaining. Keeping the array sparse leaves the hole for the downstream guard.
|
|
2811
|
+
// Read own indices only (hasOwn) and CREATE each copied index with defineProperty, never a plain assignment:
|
|
2812
|
+
// both the read side (a slice walks the chain) and the write side (`out[i] = v` respects an inherited
|
|
2813
|
+
// non-writable data property or accessor -- it can throw, fail silently, or invoke an inherited setter) touch
|
|
2814
|
+
// the prototype, so a polluted Array.prototype could fill a hole or corrupt a dense rdns into a sparse one.
|
|
2815
|
+
// defineProperty is CreateDataProperty: it always makes an own data property regardless of the prototype.
|
|
2816
|
+
var out = [];
|
|
2817
|
+
out.length = rdns.length;
|
|
2818
|
+
for (var i = 0; i < rdns.length; i++) {
|
|
2819
|
+
if (!intrinsic.hasOwn(rdns, i)) continue; // leave a hole where the source has one
|
|
2820
|
+
var rdn = rdns[i], val;
|
|
2821
|
+
if (intrinsic.isArray(rdn)) {
|
|
2822
|
+
var rdnOut = [];
|
|
2823
|
+
rdnOut.length = rdn.length;
|
|
2824
|
+
for (var j = 0; j < rdn.length; j++) {
|
|
2825
|
+
if (!intrinsic.hasOwn(rdn, j)) continue; // preserve inner holes for the same reason
|
|
2826
|
+
var atv = (rdn[j] !== null && typeof rdn[j] === "object") ? _copyAtv(rdn[j]) : rdn[j];
|
|
2827
|
+
intrinsic.defineProperty(rdnOut, j, { value: atv, enumerable: true, configurable: true, writable: true });
|
|
2828
|
+
}
|
|
2829
|
+
val = rdnOut;
|
|
2830
|
+
} else { val = rdn; }
|
|
2831
|
+
intrinsic.defineProperty(out, i, { value: val, enumerable: true, configurable: true, writable: true });
|
|
2832
|
+
}
|
|
2833
|
+
return out;
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
function toAnchor(entry, verb) {
|
|
2837
|
+
var who = verb || "build";
|
|
2838
|
+
// Decide tuple-vs-certificate by the discriminator fields, read under try/catch: a throwing name /
|
|
2839
|
+
// publicKey / algorithm accessor is a malformed anchor and must surface as the documented path/bad-input
|
|
2840
|
+
// entry refusal, not a raw exception leaked past the typed guards. `alg` and the raw publicKey are PINNED
|
|
2841
|
+
// to ONE read here and used throughout, so a stateful accessor that returns a value now and throws (or
|
|
2842
|
+
// returns a different value) on a later read cannot slip past this guard at the shape check or the snapshot.
|
|
2843
|
+
var isTuple = false, alg = null, pubRaw = null, nameRaw = null, pk = null, purposesSnap, distrustSnap;
|
|
2844
|
+
var subjectDerRaw, labelRaw, mozRaw;
|
|
2845
|
+
// A Proxy anchor is refused outright. Every field read below -- whether by property access or from an own
|
|
2846
|
+
// data descriptor -- is answered by the anchor's own reflection, and a Proxy's traps let the same read
|
|
2847
|
+
// return different values on different lookups or report a field absent while forwarding its siblings, so a
|
|
2848
|
+
// Proxy could hide a purposes / distrustAfter restriction the operator attached and validate against a
|
|
2849
|
+
// weaker anchor than the one supplied. A legitimate anchor is a plain tuple, a parsed certificate, or an
|
|
2850
|
+
// Object.create(base) inheriting from one -- none is a Proxy -- so refusing it here costs no real use and
|
|
2851
|
+
// removes the whole class of trap-driven anchor forgery. isProxy is an internal-slot check that runs no
|
|
2852
|
+
// caller code, so it sits outside the accessor try below and its message is not re-wrapped as an accessor throw.
|
|
2853
|
+
if (intrinsic.types.isProxy(entry)) {
|
|
2854
|
+
throw E("path/bad-input", who + ": a trustAnchor must not be a Proxy");
|
|
2855
|
+
}
|
|
2856
|
+
// A Proxy anywhere in the anchor's prototype CHAIN is refused too. entry may legitimately inherit its fields
|
|
2857
|
+
// (an Object.create(base) anchor is supported), and those inherited reads -- the `in` tests and the property
|
|
2858
|
+
// accesses below -- walk the chain, so a Proxy prototype would run its has / get traps: the has trap could
|
|
2859
|
+
// report purposes / distrustAfter absent while the get trap still supplies inherited name / publicKey /
|
|
2860
|
+
// algorithm, dropping the restriction and validating a weaker anchor. Walk the chain refusing any Proxy,
|
|
2861
|
+
// checking isProxy BEFORE reading its prototype so a Proxy never runs a getPrototypeOf trap here. A legitimate
|
|
2862
|
+
// anchor -- a tuple, a certificate, or an Object.create(base) over a plain object -- has a Proxy-free chain.
|
|
2863
|
+
if (entry !== null && typeof entry === "object") {
|
|
2864
|
+
var proto = intrinsic.getPrototypeOf(entry);
|
|
2865
|
+
while (proto !== null) {
|
|
2866
|
+
if (intrinsic.types.isProxy(proto)) throw E("path/bad-input", who + ": a trustAnchor must not inherit from a Proxy");
|
|
2867
|
+
proto = intrinsic.getPrototypeOf(proto);
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
try {
|
|
2871
|
+
// Recognize a parsed CERTIFICATE first and route it to coerceCert below -- by PROVENANCE, not by reading
|
|
2872
|
+
// its fields. guard.parsed.isRecorded is a getter-free WeakMap lookup: a certificate from
|
|
2873
|
+
// pki.schema.x509.parse carries a provenance record, a hand-built tuple does not. A certificate does not
|
|
2874
|
+
// carry name / publicKey / algorithm as own properties, but the tuple discriminator reads those by
|
|
2875
|
+
// property access, which FOLLOWS the prototype chain -- so a polluted Object.prototype supplying those
|
|
2876
|
+
// three would otherwise misclassify a real certificate as a tuple and bind the INHERITED (attacker) key
|
|
2877
|
+
// instead of the certificate's own. Recognizing the certificate by its forge-proof provenance mark routes
|
|
2878
|
+
// it to coerceCert and never binds an inherited key. Crucially the check is a WeakMap lookup that invokes
|
|
2879
|
+
// NO caller getter -- so, unlike a structural probe that would read tbsBytes / subject / ... , it cannot run
|
|
2880
|
+
// a tuple-controlled accessor (e.g. a polluted Object.prototype.tbsBytes getter) before publicKey is pinned
|
|
2881
|
+
// below, which would otherwise defeat the key-pinning guarantee.
|
|
2882
|
+
if (entry && typeof entry === "object" && !Buffer.isBuffer(entry) && !guard.parsed.isRecorded(entry)) {
|
|
2883
|
+
// Not a recorded certificate (isRecorded is a getter-free WeakMap lookup), so treat entry as a tuple
|
|
2884
|
+
// candidate. entry is not a Proxy (refused at the door above), so its own reflection is truthful: a
|
|
2885
|
+
// getOwnPropertyDescriptor read reports its real own data, and an absent field is really absent rather
|
|
2886
|
+
// than a trap hiding a forwarded one. The discriminator fields (publicKey / name / algorithm) are read
|
|
2887
|
+
// by PROPERTY ACCESS (follows the prototype chain, so an Object.create(base) inherited anchor is
|
|
2888
|
+
// recognized), each exactly once, so a stateful own accessor answers each at most once. The prototype
|
|
2889
|
+
// chain is never WALKED (Buffer.isBuffer / isRecorded above invoke getPrototypeOf a bounded number of
|
|
2890
|
+
// times only), so a hostile getPrototypeOf cannot be driven into a loop.
|
|
2891
|
+
//
|
|
2892
|
+
// Snapshot the constraint maps FIRST -- before ANY discriminator field (publicKey / name / algorithm) is
|
|
2893
|
+
// read -- so no discriminator getter can flip a denied purpose to allowed or move a distrust cutoff
|
|
2894
|
+
// between the map's capture and validation. A getter for publicKey or name runs when that field is read;
|
|
2895
|
+
// capturing purposes / distrustAfter beforehand puts them out of its reach (the same protection the
|
|
2896
|
+
// ordering already gave against the later algorithm getter, now extended to the earlier ones). The
|
|
2897
|
+
// identity passthrough (subjectDer, label, mozillaCaPolicy -- the closed producer-derived set) is read
|
|
2898
|
+
// once each here too. entry is not a recorded certificate, so these reads do not touch a cert's accessors.
|
|
2899
|
+
var pRaw = _captureConstraintMap(entry, "purposes", E, "path/bad-input", who);
|
|
2900
|
+
purposesSnap = _normalizeConstraintField(pRaw, E, "path/bad-input", who);
|
|
2901
|
+
var dRaw = _captureConstraintMap(entry, "distrustAfter", E, "path/bad-input", who);
|
|
2902
|
+
distrustSnap = _normalizeConstraintField(dRaw, E, "path/bad-input", who);
|
|
2903
|
+
// The identity passthrough (subjectDer, label, mozillaCaPolicy) is captured from its OWN DATA descriptor
|
|
2904
|
+
// too, never by property access: an accessor identity field, read before the key is pinned below, could
|
|
2905
|
+
// overwrite the key Buffer or mutate the name, so its getter is never invoked. An accessor / inherited
|
|
2906
|
+
// identity field is not carried (it is non-validation metadata), not refused.
|
|
2907
|
+
subjectDerRaw = _captureOwnData(entry, "subjectDer");
|
|
2908
|
+
labelRaw = _captureOwnData(entry, "label");
|
|
2909
|
+
mozRaw = _captureOwnData(entry, "mozillaCaPolicy");
|
|
2910
|
+
// Every mutable, validation-relevant field above (the constraint maps and the identity metadata) was
|
|
2911
|
+
// captured from a descriptor WITHOUT invoking a getter, so the publicKey read below -- and the name /
|
|
2912
|
+
// algorithm reads after it -- cannot have mutated them. publicKey is read (property access, so an
|
|
2913
|
+
// inherited publicKey is supported) and pinned to a private Buffer IMMEDIATELY, before the name /
|
|
2914
|
+
// algorithm accessors run, so a later getter cannot overwrite the key Buffer after it is read but before
|
|
2915
|
+
// it is captured. A non-tuple leaves pk null / isTuple false and falls through to coerceCert below.
|
|
2916
|
+
pubRaw = entry.publicKey;
|
|
2917
|
+
if (guard.bytes.isByteSource(pubRaw)) {
|
|
2918
|
+
pk = guard.bytes.snapshot(pubRaw, PathError, "path/bad-input", who + ": trustAnchor publicKey");
|
|
2919
|
+
}
|
|
2920
|
+
nameRaw = entry.name;
|
|
2921
|
+
alg = entry.algorithm;
|
|
2922
|
+
isTuple = !!(nameRaw && pubRaw && alg);
|
|
2923
|
+
}
|
|
2924
|
+
} catch (e) {
|
|
2925
|
+
throw E("path/bad-input", who + ": a trustAnchor accessor threw", e);
|
|
2926
|
+
}
|
|
2927
|
+
if (isTuple) {
|
|
2928
|
+
// publicKey was read FIRST and pinned to `pk` in the discriminator above, before the algorithm and name
|
|
2929
|
+
// accessors ran, so no caller getter can have mutated the key Buffer between its read and its capture.
|
|
2930
|
+
// (A non-Buffer publicKey leaves pk null and is refused by the Buffer.isBuffer shape check below.)
|
|
2666
2931
|
// A ready anchor tuple: validate the shape build + validate consume -- name.rdns
|
|
2667
2932
|
// (name matching), publicKey bytes and the algorithm OID (the sec. 6.1.4 key
|
|
2668
2933
|
// hand-off). trustAnchors is a caller option, so a malformed tuple is a config
|
|
2669
2934
|
// error and fails closed at entry, not a downstream no-path / soft verdict.
|
|
2670
|
-
|
|
2671
|
-
|
|
2935
|
+
// `algorithm` is the anchor key's own algorithm: an OID string, or the SubjectPublicKeyInfo
|
|
2936
|
+
// `algorithm` object a parsed certificate carries ({ oid, parameters }), which validate has always
|
|
2937
|
+
// accepted. Built-in verification derives the key algorithm from the (valid) publicKey SPKI and never
|
|
2938
|
+
// consults this value, so any algorithm that slips through -- an object with no `oid`, or an oid that
|
|
2939
|
+
// is not a real OID -- would validate the path on the SPKI alone and leave the very fail-open this
|
|
2940
|
+
// check exists to close. A number, an object without an oid, or another non-shape is a malformed tuple
|
|
2941
|
+
// and fails closed; a MISSING algorithm skips this branch and is caught below as not-a-certificate.
|
|
2942
|
+
// Materialize the anchor name to a fresh plain object carrying only its CONSUMED sub-fields: rdns (used
|
|
2943
|
+
// for DN matching -- read through the chain, so an inherited / Object.create(base) rdns is kept, then
|
|
2944
|
+
// DEEP-copied (the outer array, each RDN array, and each attribute record) so a later mutation of the
|
|
2945
|
+
// caller's nested RDN cannot change the issuer DN name chaining re-reads) and bytes (the DN DER a
|
|
2946
|
+
// cert-derived name carries, kept for a lossless round-trip; not consumed in a name-matching decision, so
|
|
2947
|
+
// it is held by reference). Each is read ONCE as DATA; the name object's own shape is never enumerated. path
|
|
2948
|
+
// validation re-reads
|
|
2949
|
+
// workingIssuerName.rdns during name chaining, so a stateful rdns accessor that answered the shape check
|
|
2950
|
+
// with one array must not hand chaining a different issuer DN; the single read stores that one value. A
|
|
2951
|
+
// throwing name / name.rdns accessor is a malformed anchor -> path/bad-input, not a raw throw.
|
|
2952
|
+
// Capture the two NESTED tuple fields -- algorithm.oid and name.rdns / name.bytes -- GETTER-FREE, from their
|
|
2953
|
+
// own data descriptors, never by property access. An ACCESSOR nested field runs caller code that could mutate
|
|
2954
|
+
// the sibling before it is captured: a name.rdns getter rewriting a wrong algorithm.oid to the SPKI's real one
|
|
2955
|
+
// past the mismatch refusal, or an algorithm.oid getter rewriting the anchor DN. Because BOTH directions
|
|
2956
|
+
// cross-mutate, no read ORDER is safe -- so an accessor nested field is refused and no getter ever runs. A
|
|
2957
|
+
// Proxy-hosted algorithm / name object is refused before its traps can answer. A legitimate anchor -- a tuple
|
|
2958
|
+
// with a plain { oid } algorithm and a plain { rdns, bytes } name, or a parsed certificate's own -- carries
|
|
2959
|
+
// plain data and is unaffected.
|
|
2960
|
+
var algStr;
|
|
2961
|
+
try {
|
|
2962
|
+
if (typeof alg === "string") {
|
|
2963
|
+
algStr = alg;
|
|
2964
|
+
} else if (alg !== null && typeof alg === "object") {
|
|
2965
|
+
if (intrinsic.types.isProxy(alg)) throw E("path/bad-input", who + ": a trustAnchor algorithm must not be a Proxy");
|
|
2966
|
+
var algOidD = intrinsic.getOwnPropertyDescriptor(alg, "oid");
|
|
2967
|
+
if (algOidD && !intrinsic.hasOwn(algOidD, "value")) throw E("path/bad-input", who + ": a trustAnchor algorithm.oid must be a data property, not an accessor");
|
|
2968
|
+
var algOid = algOidD ? algOidD.value : undefined;
|
|
2969
|
+
algStr = typeof algOid === "string" ? algOid : null;
|
|
2970
|
+
} else {
|
|
2971
|
+
algStr = null;
|
|
2972
|
+
}
|
|
2973
|
+
} catch (e) { throw E("path/bad-input", who + ": a trustAnchor algorithm.oid accessor threw", e); }
|
|
2974
|
+
var nameOut = {};
|
|
2975
|
+
try {
|
|
2976
|
+
var nsrc = (nameRaw !== null && typeof nameRaw === "object") ? nameRaw : null;
|
|
2977
|
+
if (nsrc !== null && intrinsic.types.isProxy(nsrc)) throw E("path/bad-input", who + ": a trustAnchor name must not be a Proxy");
|
|
2978
|
+
var rdnsD = nsrc ? intrinsic.getOwnPropertyDescriptor(nsrc, "rdns") : undefined;
|
|
2979
|
+
if (rdnsD && !intrinsic.hasOwn(rdnsD, "value")) throw E("path/bad-input", who + ": a trustAnchor name.rdns must be a data property, not an accessor");
|
|
2980
|
+
var rdnsVal = rdnsD ? rdnsD.value : undefined;
|
|
2981
|
+
if (rdnsVal !== undefined) {
|
|
2982
|
+
intrinsic.defineProperty(nameOut, "rdns", { value: _copyRdnsDeep(rdnsVal), enumerable: true, configurable: true, writable: true });
|
|
2983
|
+
}
|
|
2984
|
+
var bytesD = nsrc ? intrinsic.getOwnPropertyDescriptor(nsrc, "bytes") : undefined;
|
|
2985
|
+
if (bytesD && !intrinsic.hasOwn(bytesD, "value")) throw E("path/bad-input", who + ": a trustAnchor name.bytes must be a data property, not an accessor");
|
|
2986
|
+
var nBytes = bytesD ? bytesD.value : undefined;
|
|
2987
|
+
if (nBytes !== undefined) {
|
|
2988
|
+
intrinsic.defineProperty(nameOut, "bytes", { value: nBytes, enumerable: true, configurable: true, writable: true });
|
|
2989
|
+
}
|
|
2990
|
+
// Preserve the parsed Name's dn (the rendered "CN=..., O=..." string a parser emits, used to display /
|
|
2991
|
+
// identify the selected root), so a pki.trust anchor or a cert-derived name round-trips its full shape --
|
|
2992
|
+
// { rdns, dn, bytes } -- through normalization. Only a STRING dn is carried: identity never flows through
|
|
2993
|
+
// this display string (name chaining compares rdns / bytes DER under RFC 5280 sec. 7.1), so an object- or
|
|
2994
|
+
// otherwise non-string-valued dn -- which could run coercion code in a display consumer -- is dropped, not
|
|
2995
|
+
// carried. Captured getter-free like the others (an accessor dn could otherwise mutate a sibling).
|
|
2996
|
+
var dnD = nsrc ? intrinsic.getOwnPropertyDescriptor(nsrc, "dn") : undefined;
|
|
2997
|
+
if (dnD && !intrinsic.hasOwn(dnD, "value")) throw E("path/bad-input", who + ": a trustAnchor name.dn must be a data property, not an accessor");
|
|
2998
|
+
var nDn = dnD ? dnD.value : undefined;
|
|
2999
|
+
if (typeof nDn === "string") {
|
|
3000
|
+
intrinsic.defineProperty(nameOut, "dn", { value: nDn, enumerable: true, configurable: true, writable: true });
|
|
3001
|
+
}
|
|
3002
|
+
} catch (e) { throw E("path/bad-input", who + ": trustAnchor name could not be materialized", e); }
|
|
3003
|
+
// hasOwn-gate the read: nameOut is materialized here, so its rdns is either an OWN data property (set above
|
|
3004
|
+
// from the caller's own rdns) or absent. Testing the OWN property -- not a plain nameOut.rdns, which would
|
|
3005
|
+
// resolve a polluted Object.prototype.rdns on a name that supplied none (and could invoke an inherited
|
|
3006
|
+
// throwing getter) -- means a name with no own rdns is refused, never accepted on an inherited array.
|
|
3007
|
+
var nameHasRdns = intrinsic.hasOwn(nameOut, "rdns") && intrinsic.isArray(nameOut.rdns);
|
|
3008
|
+
if (!nameHasRdns || !Buffer.isBuffer(pubRaw) || algStr === null) {
|
|
3009
|
+
throw E("path/bad-input", who + ": a trustAnchor tuple must be { name: { rdns: [...] }, publicKey: Buffer, algorithm: an OID string or a SubjectPublicKeyInfo algorithm carrying an oid }");
|
|
3010
|
+
}
|
|
3011
|
+
// Hold the stated algorithm to a canonical dotted-decimal OID: a shape-valid but non-canonical value
|
|
3012
|
+
// ("not-an-oid", { oid: "garbage" }) would otherwise pass and never match the SPKI-derived algorithm.
|
|
3013
|
+
guard.identifier.assertCanonicalOid(algStr, E, "path/bad-input", who + ": trustAnchor algorithm " + algStr);
|
|
3014
|
+
// The declared algorithm is redundant with the self-describing publicKey SPKI, and built-in verification
|
|
3015
|
+
// reads the SPKI's own key algorithm, never this field. A canonical but WRONG OID (declaring one key
|
|
3016
|
+
// algorithm for a key that is actually another) would otherwise be accepted and the path validate on the
|
|
3017
|
+
// SPKI alone. Reject a declared OID that does not equal the SubjectPublicKeyInfo's key-algorithm OID.
|
|
3018
|
+
// (publicKey was pinned to `pk` at the top of this branch, before any caller getter could mutate it.)
|
|
3019
|
+
//
|
|
3020
|
+
// Walk the whole SubjectPublicKeyInfo through the shared PKIX schema, not just its first OID: a structure
|
|
3021
|
+
// that carries a decodable AlgorithmIdentifier but no key BIT STRING would otherwise pass this gate and
|
|
3022
|
+
// slip to a soft valid:false at key-import time, bypassing the promised path/bad-input entry refusal.
|
|
3023
|
+
var spkiAlg;
|
|
3024
|
+
try { spkiAlg = schema.walk(ANCHOR_SPKI_SCHEMA, asn1.decode(pk), NS).result.algorithm; }
|
|
3025
|
+
catch (e) { throw E("path/bad-input", who + ": trustAnchor publicKey is not a valid SubjectPublicKeyInfo", e); }
|
|
3026
|
+
if (spkiAlg.oid !== algStr) {
|
|
3027
|
+
throw E("path/bad-input", who + ": trustAnchor algorithm " + algStr + " does not match its publicKey key algorithm " + spkiAlg.oid);
|
|
2672
3028
|
}
|
|
2673
|
-
|
|
3029
|
+
// Return a SELF-CONTAINED anchor: a fresh plain object carrying ONLY the closed, producer-derived anchor
|
|
3030
|
+
// field set -- name, publicKey, algorithm, parameters, purposes, distrustAfter, subjectDer, label,
|
|
3031
|
+
// mozillaCaPolicy -- the shape pki.trust.parseCertdata documents and path validation + trust dedup
|
|
3032
|
+
// consume. Every field was read exactly once above (or is derived from the validated SPKI / pinned key),
|
|
3033
|
+
// so a caller getter or Proxy trap runs at most once per field and no second read can disagree with the
|
|
3034
|
+
// first; the anchor's own shape is NEVER enumerated (no ownKeys / getOwnPropertyDescriptor / `in`) and the
|
|
3035
|
+
// prototype chain is never walked, so a Proxy cannot use an enumeration trap to mutate state and a hostile
|
|
3036
|
+
// getPrototypeOf cannot be driven into a loop (Buffer.isBuffer invokes it at most once). A field the caller
|
|
3037
|
+
// attached OUTSIDE this set is not carried -- an anchor has a DEFINED shape, not an arbitrary passthrough --
|
|
3038
|
+
// and `__proto__` is never assigned, so no caller field reaches Object.prototype's setter.
|
|
3039
|
+
// - name, purposes, distrustAfter: materialized above from ONE read each as DATA (an inherited
|
|
3040
|
+
// restriction is kept, since those reads followed the prototype chain). Storing DATA is load-bearing --
|
|
3041
|
+
// assertAnchorConstraints reads distrustAfter several times (a truthy gate, a hasOwn gate, then the
|
|
3042
|
+
// value) and _hasPurposeScopedMetadata / the purpose block read purposes at more than one site, so a
|
|
3043
|
+
// live accessor could answer those reads inconsistently and silently drop a distrust or purpose control.
|
|
3044
|
+
// - subjectDer, label, mozillaCaPolicy: the trust-store identity fields, read once each in the
|
|
3045
|
+
// discriminator, so a store's own entry round-trips onto build's result.trustAnchor.
|
|
3046
|
+
// - publicKey is the pinned copy; algorithm and parameters come from the validated SPKI, never a
|
|
3047
|
+
// caller-declared field. Seeding the working key from a string OID keeps the sec. 6.1.4 key-algorithm
|
|
3048
|
+
// comparison working (an object never equals a cert's keyAlg.oid); taking parameters from the key's own
|
|
3049
|
+
// AlgorithmIdentifier stops a declared value that disagrees with it (e.g. P-384 params on a P-256
|
|
3050
|
+
// anchor) from being promoted into workingPublicKeyParameters and inherited by a child that omits its
|
|
3051
|
+
// own, which would validate a chain RFC 5280 inheritance from the real params rejects.
|
|
3052
|
+
var flat = {};
|
|
3053
|
+
intrinsic.defineProperty(flat, "name", { value: nameOut, enumerable: true, configurable: true, writable: true });
|
|
3054
|
+
if (purposesSnap !== undefined) intrinsic.defineProperty(flat, "purposes", { value: purposesSnap, enumerable: true, configurable: true, writable: true });
|
|
3055
|
+
if (distrustSnap !== undefined) intrinsic.defineProperty(flat, "distrustAfter", { value: distrustSnap, enumerable: true, configurable: true, writable: true });
|
|
3056
|
+
if (subjectDerRaw !== undefined) intrinsic.defineProperty(flat, "subjectDer", { value: subjectDerRaw, enumerable: true, configurable: true, writable: true });
|
|
3057
|
+
if (labelRaw !== undefined) intrinsic.defineProperty(flat, "label", { value: labelRaw, enumerable: true, configurable: true, writable: true });
|
|
3058
|
+
if (mozRaw !== undefined) intrinsic.defineProperty(flat, "mozillaCaPolicy", { value: mozRaw, enumerable: true, configurable: true, writable: true });
|
|
3059
|
+
intrinsic.defineProperty(flat, "publicKey", { value: pk, enumerable: true, configurable: true, writable: true });
|
|
3060
|
+
intrinsic.defineProperty(flat, "algorithm", { value: spkiAlg.oid, enumerable: true, configurable: true, writable: true });
|
|
3061
|
+
intrinsic.defineProperty(flat, "parameters", { value: spkiAlg.parameters, enumerable: true, configurable: true, writable: true });
|
|
3062
|
+
return flat;
|
|
3063
|
+
}
|
|
3064
|
+
// A certificate anchor carries no per-purpose constraints: the certificate branch below returns { name,
|
|
3065
|
+
// publicKey, algorithm, ... } only. Per-purpose purposes / distrustAfter come from a trust-anchor TUPLE (what
|
|
3066
|
+
// pki.trust emits), never from properties attached to a parsed certificate -- attaching them there would
|
|
3067
|
+
// silently drop the restriction and validate a path the caller meant to forbid. Refuse rather than drop. The
|
|
3068
|
+
// `in` check catches an OWN or an INHERITED purposes / distrustAfter (a name reached through the prototype, e.g.
|
|
3069
|
+
// a polluted Object.prototype), matching how the tuple branch refuses an inherited constraint field; it invokes
|
|
3070
|
+
// no getter (an existence test), and entry is not a Proxy (refused at the door), so no trap runs. A normal
|
|
3071
|
+
// certificate has neither on its chain, so this only catches the footgun of hanging a constraint on a cert.
|
|
3072
|
+
if (entry && typeof entry === "object" && (("purposes" in entry) || ("distrustAfter" in entry))) {
|
|
3073
|
+
throw E("path/bad-input", who + ": a trustAnchor certificate must not carry purposes / distrustAfter -- put per-purpose constraints on a { name, publicKey, algorithm, purposes, distrustAfter } tuple (or a pki.trust anchor), not on a parsed certificate");
|
|
2674
3074
|
}
|
|
2675
3075
|
var cert;
|
|
2676
3076
|
try { cert = coerceCert(entry); }
|
|
2677
|
-
catch (e) { throw E("path/bad-input", "
|
|
3077
|
+
catch (e) { throw E("path/bad-input", who + ": a trustAnchor entry must be a { name, publicKey, algorithm } tuple or a certificate", e); }
|
|
2678
3078
|
var spki = cert.subjectPublicKeyInfo;
|
|
2679
3079
|
return { name: cert.subject, publicKey: spki.bytes, algorithm: spki.algorithm.oid, parameters: spki.algorithm.parameters, subjectDer: cert.subject.bytes };
|
|
2680
3080
|
}
|
|
2681
3081
|
|
|
3082
|
+
/**
|
|
3083
|
+
* @primitive pki.path.anchorFromCert
|
|
3084
|
+
* @signature pki.path.anchorFromCert(cert) -> { name, publicKey, algorithm, parameters, subjectDer }
|
|
3085
|
+
* @status stable
|
|
3086
|
+
* @spec RFC 5280 sec. 6.1.1
|
|
3087
|
+
*
|
|
3088
|
+
* Turn a parsed certificate into the trust-anchor tuple `pki.path.validate` (`opts.trustAnchor`) and
|
|
3089
|
+
* `pki.path.build` (`opts.trustAnchors`) consume, so a root can be pinned directly instead of
|
|
3090
|
+
* hand-building `{ name, publicKey, algorithm }`. The subject becomes the anchor name and its
|
|
3091
|
+
* SubjectPublicKeyInfo the anchor key and algorithm. A value that is neither a parsed certificate nor a
|
|
3092
|
+
* ready anchor tuple is refused with `path/bad-input`. `validate` and `build` already normalize a
|
|
3093
|
+
* certificate passed directly; this exposes the same conversion for a caller that wants the tuple.
|
|
3094
|
+
*
|
|
3095
|
+
* @example
|
|
3096
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
3097
|
+
* var rootDer = await pki.x509.sign({ subject: "Root CA", subjectPublicKey: await pki.key.export(pair.publicKey),
|
|
3098
|
+
* notBefore: new Date(0), notAfter: new Date("2999-01-01") }, { key: pair.privateKey });
|
|
3099
|
+
* var anchor = pki.path.anchorFromCert(pki.schema.x509.parse(rootDer));
|
|
3100
|
+
* // anchor is { name, publicKey, algorithm, ... } -- pass it as opts.trustAnchor to pki.path.validate
|
|
3101
|
+
*/
|
|
3102
|
+
function anchorFromCert(cert) { return toAnchor(cert, "anchorFromCert"); }
|
|
3103
|
+
|
|
2682
3104
|
// RFC 4158 sec. 2.4.2 loop key: the (subject DN + subjectAltName + subject
|
|
2683
3105
|
// public key) tuple, NOT the DN alone. DN-alone keying wrongly prunes a
|
|
2684
3106
|
// legitimate distinct cross-cert (cross-certs share a DN) and misses a same-key
|
|
@@ -2976,7 +3398,7 @@ async function build(leaf, opts) {
|
|
|
2976
3398
|
for (var pdk = 0; pdk < pool.length; pdk++) intrinsic.setAdd(poolDerKeys, certDerKey(pool[pdk]));
|
|
2977
3399
|
|
|
2978
3400
|
if (!intrinsic.isArray(opts.trustAnchors) || opts.trustAnchors.length === 0) throw E("path/bad-input", "build: opts.trustAnchors must be a non-empty array of anchor tuples or root certificates");
|
|
2979
|
-
var anchors = opts.trustAnchors.map(toAnchor);
|
|
3401
|
+
var anchors = opts.trustAnchors.map(function (a) { return toAnchor(a, "build"); });
|
|
2980
3402
|
|
|
2981
3403
|
guard.time.assertValid(opts.time, E, "path/bad-input", "build: opts.time (the validity-window check date, forwarded to validate)");
|
|
2982
3404
|
// The chain-length bound is tied to the EFFECTIVE maxPathCerts the interleaved
|
|
@@ -3214,6 +3636,7 @@ async function build(leaf, opts) {
|
|
|
3214
3636
|
module.exports = {
|
|
3215
3637
|
validate: validate,
|
|
3216
3638
|
build: build,
|
|
3639
|
+
anchorFromCert: anchorFromCert,
|
|
3217
3640
|
crlChecker: crlChecker,
|
|
3218
3641
|
ocspChecker: ocspChecker,
|
|
3219
3642
|
verifyOcspResponse: verifyOcspResponse,
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:6fc4808e-4377-42ae-a5a4-080dbff3e1df",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-08-
|
|
8
|
+
"timestamp": "2026-08-24T18:48:42.954Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/pki@0.5.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.5.29",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.5.
|
|
25
|
+
"version": "0.5.29",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/pki@0.5.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.5.29",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/pki@0.5.
|
|
57
|
+
"ref": "@blamejs/pki@0.5.29",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|