@blamejs/pki 0.8.12 → 0.8.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/README.md +2 -1
- package/index.js +6 -0
- package/lib/acme.js +13 -5
- package/lib/attrcert-sign.js +3 -3
- package/lib/cmc-build.js +9 -9
- package/lib/cmc-verify.js +5 -5
- package/lib/cmp-build.js +3 -4
- package/lib/cmp-session.js +6 -6
- package/lib/cms-verify.js +4 -4
- package/lib/crl-sign.js +1 -1
- package/lib/crmf-sign.js +1 -1
- package/lib/ct.js +2 -2
- package/lib/est.js +12 -13
- package/lib/framework-error.js +3 -0
- package/lib/guard-header.js +15 -1
- package/lib/guard-identifier.js +34 -0
- package/lib/hpke.js +7 -2
- package/lib/http-transport.js +5 -6
- package/lib/identity-match.js +4 -3
- package/lib/ocsp.js +5 -3
- package/lib/oid.js +1 -1
- package/lib/path-validate.js +155 -16
- package/lib/pki-build.js +1 -1
- package/lib/revocation-fetch.js +1 -1
- package/lib/scep.js +21 -9
- package/lib/schema-all.js +10 -1
- package/lib/schema-engine.js +7 -2
- package/lib/schema-pkix.js +40 -0
- package/lib/schema-trustanchor.js +469 -0
- package/lib/schema-x509.js +66 -33
- package/lib/smime.js +5 -1
- package/lib/trust.js +502 -11
- package/lib/trustanchor-build.js +278 -0
- package/lib/validator-tpm.js +1 -1
- package/lib/webauthn-mds.js +1 -1
- package/lib/webauthn.js +9 -10
- package/lib/webcrypto.js +3 -3
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/trust.js
CHANGED
|
@@ -46,11 +46,16 @@ var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
|
|
|
46
46
|
var _strSlice = intrinsic.uncurry(String.prototype.slice);
|
|
47
47
|
var x509 = require("./schema-x509");
|
|
48
48
|
var pkix = require("./schema-pkix");
|
|
49
|
+
var oid = require("./oid");
|
|
50
|
+
var nodeFs = require("fs");
|
|
51
|
+
var nodeTls = require("tls");
|
|
52
|
+
var trustanchorSchema = require("./schema-trustanchor");
|
|
49
53
|
|
|
50
54
|
var TrustError = errors.TrustError;
|
|
51
55
|
var LIMITS = constants.LIMITS;
|
|
52
56
|
|
|
53
57
|
function E(code, message, cause) { return new TrustError(code, message, cause); }
|
|
58
|
+
var _NS = pkix.makeNS("trust", TrustError, oid);
|
|
54
59
|
|
|
55
60
|
var PURPOSES = ["serverAuth", "emailProtection", "codeSigning"];
|
|
56
61
|
|
|
@@ -351,14 +356,30 @@ function _mkAnchor(cert, meta) {
|
|
|
351
356
|
label: meta.label,
|
|
352
357
|
mozillaCaPolicy: meta.mozillaCaPolicy,
|
|
353
358
|
};
|
|
354
|
-
|
|
359
|
+
/** @internal The certificate's own basic constraints are associated with the anchor, so its
|
|
360
|
+
* pathLenConstraint bounds the path below it (RFC 5937 sec. 3.2). */
|
|
361
|
+
var certPathLen = pkix.anchorPathLenFromExtensions(cert.extensions, _NS, "the trust anchor certificate");
|
|
362
|
+
if (certPathLen !== null) {
|
|
363
|
+
var cc = intrinsic.create(null);
|
|
364
|
+
cc.pathLenConstraint = certPathLen;
|
|
365
|
+
guard.verdict.set(entry, "constraints", cc);
|
|
366
|
+
}
|
|
367
|
+
var derived = {
|
|
355
368
|
name: _copyName(cert.subject),
|
|
356
369
|
publicKey: Buffer.from(spki.bytes),
|
|
357
370
|
algorithm: spki.algorithm.oid,
|
|
358
371
|
parameters: spki.algorithm.parameters == null ? spki.algorithm.parameters : Buffer.from(spki.algorithm.parameters),
|
|
359
|
-
purposes:
|
|
372
|
+
purposes: _purposesOrNull(meta.purposes),
|
|
360
373
|
distrustAfter: _copyDistrustAfter(meta.distrustAfter),
|
|
361
|
-
}
|
|
374
|
+
};
|
|
375
|
+
/** @internal An independent copy, so editing the entry's own constraints cannot change what
|
|
376
|
+
* `anchor()` enforces; it reads this record where there is one. */
|
|
377
|
+
if (certPathLen !== null) {
|
|
378
|
+
var dc = intrinsic.create(null);
|
|
379
|
+
dc.pathLenConstraint = certPathLen;
|
|
380
|
+
derived.constraints = dc;
|
|
381
|
+
}
|
|
382
|
+
_DERIVED_FROM.set(entry, derived);
|
|
362
383
|
return entry;
|
|
363
384
|
}
|
|
364
385
|
var _DERIVED_FROM = new WeakMap();
|
|
@@ -371,27 +392,39 @@ function _copyPurposes(src) {
|
|
|
371
392
|
});
|
|
372
393
|
}
|
|
373
394
|
|
|
395
|
+
/** @internal An EXPLICIT null purposes set stays null; an absent one still defaults to all-false,
|
|
396
|
+
* which is the fail-closed reading of a store entry that named no purpose.
|
|
397
|
+
*
|
|
398
|
+
* The two differ because the formats do. A store that lists a root and grants it nothing says
|
|
399
|
+
* something, and all-false is how that is written. A format carrying no purpose bits at all, such
|
|
400
|
+
* as an RFC 5914 trust anchor, says nothing: turning that into all-false would read as "trusted
|
|
401
|
+
* for no purpose" and make `pki.path.validate` demand a purpose to check a constraint the anchor
|
|
402
|
+
* never stated. The `anchor` entry gate directly below reads a null the same way, refusing an
|
|
403
|
+
* entry whose `purposes` is `!= null` and letting a null through as absent. */
|
|
404
|
+
function _purposesOrNull(src) { return src === null ? null : _copyPurposes(src); }
|
|
405
|
+
|
|
374
406
|
function _copyDistrustAfter(src) {
|
|
375
407
|
var out = intrinsic.create(null);
|
|
376
408
|
if (!src || typeof src !== "object") return out;
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
409
|
+
var names = guard.identifier.optionNames(src);
|
|
410
|
+
for (var i = 0; i < names.length; i++) {
|
|
411
|
+
out[names[i]] = guard.time.isDate(src[names[i]]) ? new Date(guard.time.instantOf(src[names[i]])) : src[names[i]];
|
|
412
|
+
}
|
|
380
413
|
return out;
|
|
381
414
|
}
|
|
382
415
|
|
|
383
416
|
function _copyName(name) {
|
|
384
417
|
if (!name || !Array.isArray(name.rdns)) return name;
|
|
385
|
-
var out =
|
|
418
|
+
var out = guard.identifier.ownOptions(name);
|
|
386
419
|
out.rdns = guard.list.copyMap(name.rdns, function (rdn) {
|
|
387
|
-
return Array.isArray(rdn) ? guard.list.copyMap(rdn, function (atv) { return
|
|
420
|
+
return Array.isArray(rdn) ? guard.list.copyMap(rdn, function (atv) { return guard.identifier.ownOptions(atv); }) : rdn;
|
|
388
421
|
});
|
|
389
422
|
out.bytes = Buffer.isBuffer(name.bytes) ? Buffer.from(name.bytes) : name.bytes;
|
|
390
423
|
return out;
|
|
391
424
|
}
|
|
392
425
|
|
|
393
426
|
function _datesEqual(x, y) {
|
|
394
|
-
var kx =
|
|
427
|
+
var kx = guard.identifier.optionNames(x).sort(), ky = guard.identifier.optionNames(y).sort();
|
|
395
428
|
if (kx.join(",") !== ky.join(",")) return false;
|
|
396
429
|
return kx.every(function (k) { return guard.time.instantOf(x[k]) === guard.time.instantOf(y[k]); });
|
|
397
430
|
}
|
|
@@ -929,7 +962,7 @@ function anchor(entry, opts) {
|
|
|
929
962
|
throw E("trust/purpose-not-trusted", "this root is not a trusted delegator for " + opts.purpose);
|
|
930
963
|
}
|
|
931
964
|
}
|
|
932
|
-
if (!derived && (entry.purposes != null || (entry.distrustAfter &&
|
|
965
|
+
if (!derived && (entry.purposes != null || (entry.distrustAfter && guard.identifier.optionNames(entry.distrustAfter).length))) {
|
|
933
966
|
throw E("trust/bad-input", "this entry carries a trust store's per-purpose metadata but is not the entry the store produced -- it has been rebuilt, and the metadata is a statement about the key the store read, not about whichever key the copy now names. Pass pki.trust.parseCertdata / parseCcadbCsv output unmodified");
|
|
934
967
|
}
|
|
935
968
|
var nameConstraints = _copyNameConstraints(opts.nameConstraints);
|
|
@@ -940,18 +973,476 @@ function anchor(entry, opts) {
|
|
|
940
973
|
parameters: Buffer.isBuffer(meta.parameters) ? Buffer.from(meta.parameters)
|
|
941
974
|
: (meta.parameters !== undefined ? meta.parameters : null),
|
|
942
975
|
distrustAfter: _copyDistrustAfter(meta.distrustAfter),
|
|
943
|
-
purposes:
|
|
976
|
+
purposes: _purposesOrNull(meta.purposes),
|
|
944
977
|
};
|
|
945
978
|
/** @internal Set only when an overlay was supplied, so an anchor without one is the object this
|
|
946
979
|
* verb returned before the option existed, and defined rather than assigned: an accessor planted
|
|
947
980
|
* on Object.prototype under this name would otherwise take the assignment and leave the anchor
|
|
948
981
|
* carrying no restriction at all. */
|
|
949
982
|
if (nameConstraints) guard.verdict.set(out, "nameConstraints", nameConstraints);
|
|
983
|
+
/** @internal The RFC 5914 anchor's own constraints, kept separate from the `nameConstraints`
|
|
984
|
+
* overlay a caller may also supply: RFC 5937 sec. 3.2 intersects every permitting set rather than
|
|
985
|
+
* letting one replace another, so the two have to reach the validator as two statements.
|
|
986
|
+
*
|
|
987
|
+
* Read from `meta`, which is the derived record when there is one and the caller's entry when
|
|
988
|
+
* there is not, so a copy of an entry keeps the restrictions the original carried. They can only
|
|
989
|
+
* narrow a validation, so an entry that states them is safe to honor whoever built it. */
|
|
990
|
+
if (meta.constraints) guard.verdict.set(out, "constraints", _copyConstraints(meta.constraints));
|
|
991
|
+
return out;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/** @internal Copied entry by entry, for the same reason every other anchor field is: what the
|
|
995
|
+
* validator enforces must be what this verb read, not what a caller's object answers later. */
|
|
996
|
+
function _copyConstraints(c) {
|
|
997
|
+
var out = intrinsic.create(null);
|
|
998
|
+
if (c.nameConstraints) {
|
|
999
|
+
out.nameConstraints = {
|
|
1000
|
+
permitted: _copySubtrees(c.nameConstraints.permitted, "permitted"),
|
|
1001
|
+
excluded: _copySubtrees(c.nameConstraints.excluded, "excluded"),
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
if (c.policySet) out.policySet = intrinsic.map(c.policySet, function (p) { return p; });
|
|
1005
|
+
if (c.policyFlags) {
|
|
1006
|
+
var f = intrinsic.create(null);
|
|
1007
|
+
intrinsic.forEach(trustanchorSchema.policyFlagBits, function (k) { f[k] = c.policyFlags[k] === true; });
|
|
1008
|
+
out.policyFlags = f;
|
|
1009
|
+
}
|
|
1010
|
+
if (c.pathLenConstraint !== undefined) out.pathLenConstraint = c.pathLenConstraint;
|
|
1011
|
+
return out;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/** @internal An RFC 5914 anchor's constraints, in the shape `pki.path.validate` reads them. Each
|
|
1015
|
+
* field is absent rather than null when the anchor states nothing, so an anchor that constrains
|
|
1016
|
+
* nothing is the object this module produced before the format existed. */
|
|
1017
|
+
function _anchorConstraintsOf(taInfo) {
|
|
1018
|
+
var cp = taInfo.certPath;
|
|
1019
|
+
/** @internal `exts` is where RFC 5914 sec. 2.6 says an anchor associates any extension outside
|
|
1020
|
+
* the four `certPath` already carries, and basic constraints is one of them, so a path length
|
|
1021
|
+
* stated there binds as much as one stated in `certPath` (RFC 5937 sec. 3.2). Where both state
|
|
1022
|
+
* one, the stricter wins: an anchor may only narrow. */
|
|
1023
|
+
var extsPathLen = pkix.anchorPathLenFromExtensions(taInfo.exts, _NS, "the TrustAnchorInfo exts");
|
|
1024
|
+
if (cp === null) {
|
|
1025
|
+
if (extsPathLen === null) return null;
|
|
1026
|
+
var only = intrinsic.create(null);
|
|
1027
|
+
only.pathLenConstraint = extsPathLen;
|
|
1028
|
+
return only;
|
|
1029
|
+
}
|
|
1030
|
+
var out = intrinsic.create(null);
|
|
1031
|
+
var any = false;
|
|
1032
|
+
if (cp.nameConstr !== null) {
|
|
1033
|
+
out.nameConstraints = {
|
|
1034
|
+
permitted: intrinsic.map(cp.nameConstr.permittedSubtrees || [], _subtreeOf),
|
|
1035
|
+
excluded: intrinsic.map(cp.nameConstr.excludedSubtrees || [], _subtreeOf),
|
|
1036
|
+
};
|
|
1037
|
+
any = true;
|
|
1038
|
+
}
|
|
1039
|
+
if (cp.policySet !== null) {
|
|
1040
|
+
out.policySet = intrinsic.map(cp.policySet, function (p) { return p.policyIdentifier; });
|
|
1041
|
+
any = true;
|
|
1042
|
+
}
|
|
1043
|
+
if (cp.policyFlags !== null) { out.policyFlags = cp.policyFlags; any = true; }
|
|
1044
|
+
var pathLen = pkix.lowerPathLen(cp.pathLenConstraint, extsPathLen);
|
|
1045
|
+
if (pathLen !== null) { out.pathLenConstraint = pathLen; any = true; }
|
|
1046
|
+
return any ? out : null;
|
|
1047
|
+
}
|
|
1048
|
+
function _subtreeOf(st) { return { tag: st.base.tagNumber, base: st.base.value }; }
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* @primitive pki.trust.parseTrustAnchorList
|
|
1052
|
+
* @signature pki.trust.parseTrustAnchorList(input, opts?) -> { anchors }
|
|
1053
|
+
* @since 0.8.13
|
|
1054
|
+
* @status stable
|
|
1055
|
+
* @spec RFC 5914, RFC 5937 sec. 3.2
|
|
1056
|
+
* @related pki.trust.anchor, pki.schema.trustanchor.parse, pki.path.validate
|
|
1057
|
+
*
|
|
1058
|
+
* Read an RFC 5914 `TrustAnchorList` into the trust-store entries `pki.trust.anchor` takes, the
|
|
1059
|
+
* same shape `parseCertdata` and `parseCcadbCsv` produce. Each entry carries the anchor's name,
|
|
1060
|
+
* public key and algorithm, so a list published by a root program drives `pki.path.validate`
|
|
1061
|
+
* without the operator mapping it by hand.
|
|
1062
|
+
*
|
|
1063
|
+
* A `taInfo` anchor's constraints ride on the entry and are applied as RFC 5937 section 3.2 states,
|
|
1064
|
+
* which is that the stricter of the anchor's value and the caller's always wins: its name
|
|
1065
|
+
* constraints intersect the caller's permitted subtrees and union its excluded ones, its
|
|
1066
|
+
* `policySet` intersects `opts.userInitialPolicySet`, each of its `policyFlags` may turn the
|
|
1067
|
+
* matching `initial*` option on and never off, and its `pathLenConstraint` bounds the path.
|
|
1068
|
+
*
|
|
1069
|
+
* The `certificate` and `tbsCert` arms carry no constraints of their own, so an entry from either
|
|
1070
|
+
* is the anchor that certificate's subject and key describe.
|
|
1071
|
+
*
|
|
1072
|
+
* @opts
|
|
1073
|
+
* - `maxBytes` / `maxDepth` / `maxItems` (integer) -- decode caps for the parse.
|
|
1074
|
+
* @example
|
|
1075
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
1076
|
+
* var der = pki.trustanchor.build({ anchors: [{ taInfo: {
|
|
1077
|
+
* pubKey: await pki.key.export(pair.publicKey), keyId: Buffer.alloc(20, 1),
|
|
1078
|
+
* certPath: { taName: "CN=Example Root" } } }] });
|
|
1079
|
+
* pki.trust.parseTrustAnchorList(der).anchors.length; // -> 1
|
|
1080
|
+
*/
|
|
1081
|
+
function parseTrustAnchorList(input, opts) {
|
|
1082
|
+
var list = trustanchorSchema.parse(input, opts);
|
|
1083
|
+
var out = [];
|
|
1084
|
+
intrinsic.forEach(list.anchors, function (a, i) {
|
|
1085
|
+
guard.list.append(out, _entryFromAnchor(a, i));
|
|
1086
|
+
});
|
|
1087
|
+
return { anchors: out };
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function _entryFromAnchor(a, i) {
|
|
1091
|
+
if (a.kind === "certificate" || a.kind === "tbsCert") {
|
|
1092
|
+
var cert = a.kind === "certificate" ? a.certificate : a.tbsCert;
|
|
1093
|
+
return _mkAnchor(cert, { distrustAfter: {}, purposes: null, label: null, mozillaCaPolicy: false });
|
|
1094
|
+
}
|
|
1095
|
+
var info = a.taInfo;
|
|
1096
|
+
if (info.certPath === null) {
|
|
1097
|
+
throw E("trust/bad-input", "anchors[" + i + "] is a TrustAnchorInfo carrying no certPath, so it names no " +
|
|
1098
|
+
"distinguished name to chain to; a trust anchor needs one (RFC 5914 sec. 2)");
|
|
1099
|
+
}
|
|
1100
|
+
var spki = info.pubKey;
|
|
1101
|
+
var constraints = _anchorConstraintsOf(info);
|
|
1102
|
+
var entry = {
|
|
1103
|
+
name: info.certPath.taName,
|
|
1104
|
+
publicKey: spki.bytes,
|
|
1105
|
+
algorithm: spki.algorithm.oid,
|
|
1106
|
+
parameters: spki.algorithm.parameters,
|
|
1107
|
+
subjectDer: info.certPath.taName.bytes,
|
|
1108
|
+
distrustAfter: {},
|
|
1109
|
+
purposes: null,
|
|
1110
|
+
label: info.taTitle,
|
|
1111
|
+
mozillaCaPolicy: false,
|
|
1112
|
+
};
|
|
1113
|
+
/** @internal On the ENTRY, not only in the derived record behind it. The entry carries a name, a
|
|
1114
|
+
* public key and an algorithm, which is exactly the anchor tuple `pki.path.validate` accepts, so
|
|
1115
|
+
* an operator passing `parseTrustAnchorList(der).anchors` straight to `trustAnchors` is doing the
|
|
1116
|
+
* obvious thing. Keeping the constraints out of reach of that call would hand them an
|
|
1117
|
+
* UNCONSTRAINED anchor and silently drop every restriction the list stated. */
|
|
1118
|
+
if (constraints) guard.verdict.set(entry, "constraints", constraints);
|
|
1119
|
+
_DERIVED_FROM.set(entry, {
|
|
1120
|
+
name: _copyName(info.certPath.taName),
|
|
1121
|
+
publicKey: intrinsic.bufferFrom(spki.bytes),
|
|
1122
|
+
algorithm: spki.algorithm.oid,
|
|
1123
|
+
parameters: spki.algorithm.parameters == null ? spki.algorithm.parameters : intrinsic.bufferFrom(spki.algorithm.parameters),
|
|
1124
|
+
purposes: null,
|
|
1125
|
+
distrustAfter: _copyDistrustAfter({}),
|
|
1126
|
+
/** @internal Its OWN copy, not the object the entry carries. The derived record is what
|
|
1127
|
+
* `anchor` trusts in preference to the entry, precisely because the entry is reachable: a
|
|
1128
|
+
* caller who edits `entry.constraints` would otherwise be editing what the anchor enforces,
|
|
1129
|
+
* which is the one thing this record exists to prevent for the name and the key. */
|
|
1130
|
+
constraints: constraints === null ? null : _copyConstraints(constraints),
|
|
1131
|
+
});
|
|
1132
|
+
return entry;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/** @internal The bundle paths a Unix host keeps its CA set in, in the order a reader should try
|
|
1136
|
+
* them. Each is a concatenated PEM file the distribution maintains. macOS and Windows keep theirs
|
|
1137
|
+
* in a binary keychain and a registry-backed store, so neither has a path here. */
|
|
1138
|
+
var _BUNDLE_PATHS = [
|
|
1139
|
+
"/etc/ssl/certs/ca-certificates.crt",
|
|
1140
|
+
"/etc/pki/tls/certs/ca-bundle.crt",
|
|
1141
|
+
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
|
|
1142
|
+
"/etc/ssl/ca-bundle.pem",
|
|
1143
|
+
"/etc/ssl/cert.pem",
|
|
1144
|
+
"/usr/local/share/certs/ca-root-nss.crt",
|
|
1145
|
+
];
|
|
1146
|
+
|
|
1147
|
+
var _SYSTEM_ANCHOR_OPTS = intrinsic.assign(intrinsic.create(null), {
|
|
1148
|
+
path: 1, allowNodeBundle: 1, maxBytes: 1,
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
/** @internal What an operator runs to produce a bundle this verb can read, per platform. Named in
|
|
1152
|
+
* the refusal, because the alternative an operator reaches for otherwise is Node's compiled-in
|
|
1153
|
+
* set, which is not what their host trusts. */
|
|
1154
|
+
function _exportHint() {
|
|
1155
|
+
if (process.platform === "darwin") {
|
|
1156
|
+
return "export the host's roots with: security find-certificate -a -p " +
|
|
1157
|
+
"/System/Library/Keychains/SystemRootCertificates.keychain > roots.pem";
|
|
1158
|
+
}
|
|
1159
|
+
if (process.platform === "win32") {
|
|
1160
|
+
return "export the host's roots with: certutil -store ROOT";
|
|
1161
|
+
}
|
|
1162
|
+
return "point opts.path at the CA bundle this host keeps";
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* @primitive pki.trust.discoversBundles
|
|
1167
|
+
* @signature pki.trust.discoversBundles(platform) -> boolean
|
|
1168
|
+
* @since 0.8.15
|
|
1169
|
+
* @status stable
|
|
1170
|
+
* @spec RFC 5280
|
|
1171
|
+
* @defends trust-store-misattribution (CWE-345)
|
|
1172
|
+
* @related pki.trust.systemAnchors
|
|
1173
|
+
*
|
|
1174
|
+
* Whether `systemAnchors` searches for a CA bundle on this platform. A Unix host's bundle IS the
|
|
1175
|
+
* set its TLS stack trusts. macOS keeps its trust decisions in Keychain and Windows in a
|
|
1176
|
+
* registry-backed store, and on macOS `/etc/ssl/cert.pem` can exist as OpenSSL's own copy, which
|
|
1177
|
+
* is not the host's trust and omits both locally installed roots and Keychain overrides. Reporting
|
|
1178
|
+
* that file as the host's anchors is the misattribution this verb exists to prevent, so those
|
|
1179
|
+
* platforms are refused rather than searched. `opts.path` still reads an exported bundle anywhere.
|
|
1180
|
+
*
|
|
1181
|
+
* @example
|
|
1182
|
+
* pki.trust.discoversBundles("linux"); // -> true
|
|
1183
|
+
* pki.trust.discoversBundles("darwin"); // -> false
|
|
1184
|
+
*/
|
|
1185
|
+
function discoversBundles(platform) {
|
|
1186
|
+
if (typeof platform !== "string" || platform.length === 0) {
|
|
1187
|
+
throw E("trust/bad-input", "discoversBundles takes a process.platform string");
|
|
1188
|
+
}
|
|
1189
|
+
return platform !== "darwin" && platform !== "win32";
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/** @internal Reads at most `maxBytes + 1` so the cap bounds what is ALLOCATED rather than being
|
|
1193
|
+
* checked after an unbounded read: a file far above the cap must not reach memory first. */
|
|
1194
|
+
function _readBundleText(chosen, maxBytes) {
|
|
1195
|
+
var fd = null, raw, read;
|
|
1196
|
+
try {
|
|
1197
|
+
fd = nodeFs.openSync(chosen, "r");
|
|
1198
|
+
var buf = intrinsic.bufferAlloc(maxBytes + 1);
|
|
1199
|
+
/** @internal readSync may return fewer bytes than asked for, so the prefix of one call is not
|
|
1200
|
+
* the file. Taking it as the whole bundle would drop the roots below the break with nothing
|
|
1201
|
+
* reported, and would let a file above the cap pass the check underneath it. */
|
|
1202
|
+
read = 0;
|
|
1203
|
+
for (;;) {
|
|
1204
|
+
var got = nodeFs.readSync(fd, buf, read, (maxBytes + 1) - read, read);
|
|
1205
|
+
if (got === 0) break;
|
|
1206
|
+
read += got;
|
|
1207
|
+
if (read >= maxBytes + 1) break;
|
|
1208
|
+
}
|
|
1209
|
+
raw = buf.subarray(0, read);
|
|
1210
|
+
} catch (e) {
|
|
1211
|
+
throw E("trust/no-readable-store", "no CA bundle could be read at " + guard.text.showValue(chosen) +
|
|
1212
|
+
"; " + _exportHint() + ", or pass opts.allowNodeBundle to use the Mozilla set compiled into " +
|
|
1213
|
+
"Node, which is NOT what this host trusts", e);
|
|
1214
|
+
} finally {
|
|
1215
|
+
if (fd !== null) {
|
|
1216
|
+
try { nodeFs.closeSync(fd); }
|
|
1217
|
+
// allow:swallow-unverified the descriptor is released on a best-effort basis
|
|
1218
|
+
catch (_e) { void 0; }
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
if (read > maxBytes) {
|
|
1222
|
+
throw E("trust/too-large", "the CA bundle at " + guard.text.showValue(chosen) +
|
|
1223
|
+
" is larger than the " + maxBytes + "-byte cap this read admits");
|
|
1224
|
+
}
|
|
1225
|
+
return raw;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/**
|
|
1229
|
+
* @primitive pki.trust.systemAnchors
|
|
1230
|
+
* @signature pki.trust.systemAnchors(opts?) -> { anchors, source, path, skipped }
|
|
1231
|
+
* @since 0.8.15
|
|
1232
|
+
* @status stable
|
|
1233
|
+
* @spec RFC 7468 (PEM), RFC 5280
|
|
1234
|
+
* @defends trust-store-misattribution (CWE-345), trust-store-parser-DoS (CWE-770)
|
|
1235
|
+
* @related pki.trust.parseCertdata, pki.trust.parseCcadbCsv, pki.trust.anchor, pki.path.validate
|
|
1236
|
+
*
|
|
1237
|
+
* Read the host's CA bundle into the trust anchors `pki.path.validate` consumes. `source` names
|
|
1238
|
+
* which set was read: `"ca-bundle"` for a file on this host, with `path` naming it, or
|
|
1239
|
+
* `"node-bundled"` for the Mozilla set compiled into Node, whose `path` is `null`. A block the
|
|
1240
|
+
* bundle carries that is not a certificate is listed in `skipped` with the label that named it,
|
|
1241
|
+
* rather than dropped.
|
|
1242
|
+
*
|
|
1243
|
+
* A host whose roots are not in a readable PEM file is refused, and the refusal names the command
|
|
1244
|
+
* that exports them. macOS keeps its roots in a binary keychain and Windows in a registry-backed
|
|
1245
|
+
* store, and reading either means running another program, which this toolkit does not do. Falling
|
|
1246
|
+
* back to Node's set instead would report anchors the host does not use, so that takes the
|
|
1247
|
+
* explicit `opts.allowNodeBundle`.
|
|
1248
|
+
*
|
|
1249
|
+
* @opts
|
|
1250
|
+
* - `path` -- read this file instead of searching the per-platform list.
|
|
1251
|
+
* - `allowNodeBundle` -- return Node's compiled-in set when no bundle can be read.
|
|
1252
|
+
* - `maxBytes` -- cap the bundle read (default `C.LIMITS.TRUST_MAX_BYTES`).
|
|
1253
|
+
*
|
|
1254
|
+
* @example
|
|
1255
|
+
* // Read the host's bundle where there is one, and say so when there is not. Check `source`
|
|
1256
|
+
* // before trusting the answer: "node-bundled" is Node's compiled-in set, not this machine's.
|
|
1257
|
+
* var store = pki.trust.systemAnchors({ allowNodeBundle: true });
|
|
1258
|
+
* console.log(store.source); // -> "ca-bundle" on a Unix host, else "node-bundled"
|
|
1259
|
+
* console.log(store.anchors.length > 0); // -> true
|
|
1260
|
+
* console.log(store.skipped.length >= 0); // -> true (entries named, never dropped silently)
|
|
1261
|
+
*/
|
|
1262
|
+
function systemAnchors(opts) {
|
|
1263
|
+
opts = opts || {};
|
|
1264
|
+
guard.identifier.assertPlainRecord(opts, E, "trust/bad-input", "systemAnchors opts");
|
|
1265
|
+
guard.identifier.assertKnownKeys(opts, _SYSTEM_ANCHOR_OPTS, E, "trust/bad-input",
|
|
1266
|
+
"unknown systemAnchors option ");
|
|
1267
|
+
var maxBytes = guard.limits.cap(opts.maxBytes, "systemAnchors: opts.maxBytes",
|
|
1268
|
+
LIMITS.TRUST_MAX_BYTES, { E: E, code: "trust/bad-input", min: 1, max: LIMITS.TRUST_MAX_BYTES });
|
|
1269
|
+
|
|
1270
|
+
var chosen = null;
|
|
1271
|
+
if (opts.path !== undefined) {
|
|
1272
|
+
if (typeof opts.path !== "string" || opts.path.length === 0) {
|
|
1273
|
+
throw E("trust/bad-input", "systemAnchors: opts.path must be a non-empty string");
|
|
1274
|
+
}
|
|
1275
|
+
chosen = opts.path;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
var text = null;
|
|
1279
|
+
if (chosen !== null) {
|
|
1280
|
+
/** @internal `allowNodeBundle` says what to do when there is NO readable bundle, so an absent
|
|
1281
|
+
* file falls back to Node's set under it. A bundle that EXISTS and breaks the cap does not:
|
|
1282
|
+
* the caller asked about that file, and a resource refusal must never quietly swap the trust
|
|
1283
|
+
* set a validation runs against for a different one. */
|
|
1284
|
+
try { text = _readBundleText(chosen, maxBytes); }
|
|
1285
|
+
catch (e) {
|
|
1286
|
+
if (opts.allowNodeBundle !== true || !e || e.code !== "trust/no-readable-store") throw e;
|
|
1287
|
+
text = null;
|
|
1288
|
+
chosen = null;
|
|
1289
|
+
}
|
|
1290
|
+
} else if (discoversBundles(process.platform)) {
|
|
1291
|
+
for (var i = 0; i < _BUNDLE_PATHS.length && text === null; i++) {
|
|
1292
|
+
/** @internal Readability is established by READING, not by a stat: a candidate that exists
|
|
1293
|
+
* but cannot be opened must not stop the search and strand a readable one below it. */
|
|
1294
|
+
try { text = _readBundleText(_BUNDLE_PATHS[i], maxBytes); chosen = _BUNDLE_PATHS[i]; }
|
|
1295
|
+
catch (e) {
|
|
1296
|
+
if (e && e.code === "trust/too-large") throw e;
|
|
1297
|
+
// allow:swallow-unverified a candidate this host cannot read is not this host's bundle
|
|
1298
|
+
text = null;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
if (text === null) {
|
|
1304
|
+
if (opts.allowNodeBundle !== true) {
|
|
1305
|
+
throw E("trust/no-readable-store", "this host keeps no CA bundle this toolkit can read" +
|
|
1306
|
+
(chosen === null ? "" : " at " + guard.text.showValue(chosen)) + "; " + _exportHint() +
|
|
1307
|
+
", or pass opts.allowNodeBundle to use the Mozilla set compiled into Node, which is NOT " +
|
|
1308
|
+
"what this host trusts");
|
|
1309
|
+
}
|
|
1310
|
+
return _anchorsFromPems(nodeTls.rootCertificates, "node-bundled", null);
|
|
1311
|
+
}
|
|
1312
|
+
return _anchorsFromBundle(text, chosen);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/** @internal A line that reads as a BEGIN boundary but does not start one, because RFC 7468 sec. 2
|
|
1316
|
+
* puts the boundary at the start of a line and this one is indented or trailed. The decoder is
|
|
1317
|
+
* right to pass over it as explanatory text, and in a trust bundle that is an anchor the operator
|
|
1318
|
+
* believes they installed and does not have. Reported with the offset, never a throw: the file is
|
|
1319
|
+
* readable and the other roots in it are usable. */
|
|
1320
|
+
/** @internal The bytes of "-----BEGIN ", held as codes because this directory scans by character
|
|
1321
|
+
* code rather than by pattern. */
|
|
1322
|
+
var _BEGIN_MARK = [0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x42, 0x45, 0x47, 0x49, 0x4e, 0x20];
|
|
1323
|
+
|
|
1324
|
+
function _reportUnusableBoundaries(bytes, rows, skipped) {
|
|
1325
|
+
var used = intrinsic.create(null);
|
|
1326
|
+
for (var r = 0; r < rows.length; r++) used[rows[r].offset] = true;
|
|
1327
|
+
var lineStart = 0;
|
|
1328
|
+
for (var i = 0; i <= bytes.length; i++) {
|
|
1329
|
+
if (i !== bytes.length && bytes[i] !== 0x0a) continue;
|
|
1330
|
+
var at = lineStart;
|
|
1331
|
+
while (at < i && (bytes[at] === 0x20 || bytes[at] === 0x09)) at += 1;
|
|
1332
|
+
var indented = at > lineStart;
|
|
1333
|
+
var matches = indented && at + _BEGIN_MARK.length <= i;
|
|
1334
|
+
for (var m = 0; matches && m < _BEGIN_MARK.length; m++) {
|
|
1335
|
+
if (bytes[at + m] !== _BEGIN_MARK[m]) matches = false;
|
|
1336
|
+
}
|
|
1337
|
+
if (matches && used[at] !== true) {
|
|
1338
|
+
guard.list.append(skipped, {
|
|
1339
|
+
index: null, offset: at, label: null,
|
|
1340
|
+
reason: "pem/unusable-boundary: a BEGIN boundary at byte " + at + " does not start its " +
|
|
1341
|
+
"line, so RFC 7468 sec. 2 reads it as text and the object it opens is not decoded",
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
lineStart = i + 1;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/** @internal Every certificate block becomes an anchor and every other block is reported, because
|
|
1349
|
+
* a bundle an operator assembled by hand is exactly where a key or a CRL ends up, and a reader
|
|
1350
|
+
* that drops one silently leaves them believing it was read. */
|
|
1351
|
+
function _anchorsFromBundle(text, chosen) {
|
|
1352
|
+
var rows;
|
|
1353
|
+
try { rows = pkix.pemDecodeBundle(text, {}, errors.PemError); }
|
|
1354
|
+
catch (e) {
|
|
1355
|
+
if (e && e.code === "pem/no-block") {
|
|
1356
|
+
throw E("trust/no-anchors", "the CA bundle at " + guard.text.showValue(chosen) +
|
|
1357
|
+
" carries no PEM block, so it names no trust anchor", e);
|
|
1358
|
+
}
|
|
1359
|
+
throw E("trust/bad-bundle", "the CA bundle at " + guard.text.showValue(chosen) +
|
|
1360
|
+
" is not readable as PEM: " + guard.text.describeThrown(e), e);
|
|
1361
|
+
}
|
|
1362
|
+
var anchors = [], skipped = [];
|
|
1363
|
+
_reportUnusableBoundaries(text, rows, skipped);
|
|
1364
|
+
for (var i = 0; i < rows.length; i++) {
|
|
1365
|
+
var row = rows[i];
|
|
1366
|
+
if (row.label === "TRUSTED CERTIFICATE") {
|
|
1367
|
+
/** @internal OpenSSL's TRUSTED CERTIFICATE is a Certificate followed by an X509_CERT_AUX
|
|
1368
|
+
* carrying the purposes the operator trusted it FOR. Reading the leading certificate and
|
|
1369
|
+
* dropping the aux would hand back a root restricted to one purpose as an unrestricted
|
|
1370
|
+
* anchor, which is a widening, so the block is reported until the aux is read. */
|
|
1371
|
+
guard.list.append(skipped, {
|
|
1372
|
+
index: row.index, offset: row.offset, label: row.label,
|
|
1373
|
+
reason: "trust/aux-not-read: an OpenSSL trusted certificate carries the purposes it is " +
|
|
1374
|
+
"trusted for in a trailing X509_CERT_AUX this reader does not decode, and ingesting it " +
|
|
1375
|
+
"without them would widen the trust it states; convert it with " +
|
|
1376
|
+
"`openssl x509 -in <file> -out <file>.pem` and pass the result",
|
|
1377
|
+
});
|
|
1378
|
+
continue;
|
|
1379
|
+
}
|
|
1380
|
+
if (row.label !== "CERTIFICATE") {
|
|
1381
|
+
guard.list.append(skipped, {
|
|
1382
|
+
index: row.index, offset: row.offset, label: row.label,
|
|
1383
|
+
reason: "trust/not-a-certificate-block: a " + guard.text.showValue(row.label) +
|
|
1384
|
+
" block names no trust anchor",
|
|
1385
|
+
});
|
|
1386
|
+
continue;
|
|
1387
|
+
}
|
|
1388
|
+
var cert = null, why = null;
|
|
1389
|
+
try { cert = x509.parse(row.der); }
|
|
1390
|
+
catch (e) { why = (e && e.code ? e.code : "trust/not-a-certificate") + ": " + guard.text.describeThrown(e); }
|
|
1391
|
+
if (cert === null) {
|
|
1392
|
+
/** @internal Reported, not thrown. A host CA bundle is distribution-managed and real ones
|
|
1393
|
+
* carry certificates this toolkit refuses: the Mozilla set Node ships has a root encoding
|
|
1394
|
+
* pre-2050 dates as GeneralizedTime against the RFC 5280 sec. 4.1.2.5 MUST. Refusing the
|
|
1395
|
+
* whole file would leave the operator with no anchors over a root they do not control,
|
|
1396
|
+
* and relaxing the reader to admit it would weaken every other caller. The entry is named
|
|
1397
|
+
* with the reason instead, so a missing anchor is visible rather than silent. */
|
|
1398
|
+
guard.list.append(skipped, { index: row.index, offset: row.offset, label: row.label, reason: why });
|
|
1399
|
+
continue;
|
|
1400
|
+
}
|
|
1401
|
+
guard.list.append(anchors, _mkAnchor(cert, {
|
|
1402
|
+
distrustAfter: {}, purposes: null, label: null, mozillaCaPolicy: false,
|
|
1403
|
+
}));
|
|
1404
|
+
}
|
|
1405
|
+
if (anchors.length === 0) {
|
|
1406
|
+
throw E("trust/no-anchors", "the CA bundle at " + guard.text.showValue(chosen) +
|
|
1407
|
+
" carries no certificate, so it names no trust anchor");
|
|
1408
|
+
}
|
|
1409
|
+
return _systemResult(anchors, "ca-bundle", chosen, skipped);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
function _anchorsFromPems(pems, source, chosen) {
|
|
1413
|
+
var anchors = [], skipped = [];
|
|
1414
|
+
for (var i = 0; i < pems.length; i++) {
|
|
1415
|
+
var cert = null, why = null;
|
|
1416
|
+
try { cert = x509.parse(x509.pemDecode(pems[i], "CERTIFICATE")); }
|
|
1417
|
+
catch (e) { why = (e && e.code ? e.code : "trust/not-a-certificate") + ": " + guard.text.describeThrown(e); }
|
|
1418
|
+
if (cert === null) {
|
|
1419
|
+
guard.list.append(skipped, { index: i, offset: null, label: "CERTIFICATE", reason: why });
|
|
1420
|
+
continue;
|
|
1421
|
+
}
|
|
1422
|
+
guard.list.append(anchors, _mkAnchor(cert, {
|
|
1423
|
+
distrustAfter: {}, purposes: null, label: null, mozillaCaPolicy: false,
|
|
1424
|
+
}));
|
|
1425
|
+
}
|
|
1426
|
+
if (anchors.length === 0) throw E("trust/no-anchors", "the " + source + " set carries no certificate");
|
|
1427
|
+
return _systemResult(anchors, source, chosen, skipped);
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
/** @internal Defined rather than assigned, and a fresh list each call: the caller owns what it is
|
|
1431
|
+
* handed, so emptying it must not change what the next call returns. */
|
|
1432
|
+
function _systemResult(anchors, source, chosen, skipped) {
|
|
1433
|
+
var out = intrinsic.create(null);
|
|
1434
|
+
guard.verdict.set(out, "anchors", anchors);
|
|
1435
|
+
guard.verdict.set(out, "source", source);
|
|
1436
|
+
guard.verdict.set(out, "path", chosen);
|
|
1437
|
+
guard.verdict.set(out, "skipped", skipped);
|
|
950
1438
|
return out;
|
|
951
1439
|
}
|
|
952
1440
|
|
|
953
1441
|
module.exports = {
|
|
954
1442
|
parseCertdata: parseCertdata,
|
|
955
1443
|
parseCcadbCsv: parseCcadbCsv,
|
|
1444
|
+
parseTrustAnchorList: parseTrustAnchorList,
|
|
1445
|
+
systemAnchors: systemAnchors,
|
|
1446
|
+
discoversBundles: discoversBundles,
|
|
956
1447
|
anchor: anchor,
|
|
957
1448
|
};
|