@blamejs/pki 0.1.32 → 0.2.1
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 +38 -0
- package/README.md +3 -1
- package/index.js +13 -0
- package/lib/asn1-der.js +31 -22
- package/lib/constants.js +33 -0
- package/lib/ct.js +13 -12
- package/lib/est.js +10 -10
- package/lib/framework-error.js +13 -0
- package/lib/guard-all.js +15 -0
- package/lib/guard-crypto.js +31 -1
- package/lib/guard-encoding.js +90 -0
- package/lib/guard-identifier.js +56 -0
- package/lib/guard-json.js +149 -0
- package/lib/guard-limits.js +58 -8
- package/lib/guard-range.js +40 -7
- package/lib/guard-text.js +7 -0
- package/lib/jose.js +17 -128
- package/lib/merkle.js +7 -16
- package/lib/oid.js +33 -10
- package/lib/path-validate.js +173 -16
- package/lib/schema-attrcert.js +5 -0
- package/lib/schema-cms.js +22 -1
- package/lib/schema-crmf.js +3 -5
- package/lib/schema-ocsp.js +5 -1
- package/lib/schema-pkix.js +92 -12
- package/lib/shbs.js +348 -0
- package/lib/trust.js +707 -0
- package/lib/webcrypto.js +18 -7
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/trust.js
ADDED
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @module pki.trust
|
|
6
|
+
* @nav Path validation
|
|
7
|
+
* @title Trust-store ingestion
|
|
8
|
+
* @order 230
|
|
9
|
+
* @slug trust
|
|
10
|
+
*
|
|
11
|
+
* @intro
|
|
12
|
+
* Mozilla / CCADB root-program ingestion into constraint-carrying trust
|
|
13
|
+
* anchors. The bare root list (`tls.rootCertificates`) throws away exactly
|
|
14
|
+
* the metadata that decides WHICH roots may vouch for WHAT: the per-purpose
|
|
15
|
+
* trust bits (a root trusted for TLS is not thereby trusted for S/MIME) and
|
|
16
|
+
* the per-purpose distrust-after dates (a sunsetting root keeps validating
|
|
17
|
+
* already-issued certificates while certificates issued after the cutoff are
|
|
18
|
+
* rejected). `parseCertdata` reads the NSS `certdata.txt` object stream and
|
|
19
|
+
* `parseCcadbCsv` the CCADB CSV export into one identical `Anchor` shape, so
|
|
20
|
+
* enforcement downstream is source-agnostic; `anchor()` hands an entry to
|
|
21
|
+
* `pki.path.validate({ trustAnchor, checkPurpose })`.
|
|
22
|
+
*
|
|
23
|
+
* Everything is fail-closed and offline: the caller supplies the text (no
|
|
24
|
+
* network fetch); every malformed or oversized input throws a typed
|
|
25
|
+
* `trust/*` error before the offending allocation; a certificate object and
|
|
26
|
+
* its trust object are paired by byte-exact (CKA_ISSUER, CKA_SERIAL_NUMBER)
|
|
27
|
+
* -- never by adjacency -- and cross-checked against the parsed DER, so
|
|
28
|
+
* trust metadata can never attach to the wrong root; only
|
|
29
|
+
* `CKT_NSS_TRUSTED_DELEGATOR` grants a purpose (everything else, including
|
|
30
|
+
* an absent bit, is untrusted).
|
|
31
|
+
*
|
|
32
|
+
* @card
|
|
33
|
+
* `parseCertdata` / `parseCcadbCsv` -> constraint-carrying trust anchors
|
|
34
|
+
* (per-purpose trust bits + distrust-after dates); `anchor()` feeds
|
|
35
|
+
* `pki.path.validate`. Fail-closed, offline, source-agnostic.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
var constants = require("./constants");
|
|
39
|
+
var errors = require("./framework-error");
|
|
40
|
+
var asn1 = require("./asn1-der");
|
|
41
|
+
var guard = require("./guard-all");
|
|
42
|
+
var x509 = require("./schema-x509");
|
|
43
|
+
|
|
44
|
+
var TrustError = errors.TrustError;
|
|
45
|
+
var LIMITS = constants.LIMITS;
|
|
46
|
+
|
|
47
|
+
function E(code, message, cause) { return new TrustError(code, message, cause); }
|
|
48
|
+
|
|
49
|
+
// The three NSS purposes an anchor carries; the keys double as the
|
|
50
|
+
// `opts.checkPurpose` vocabulary `pki.path.validate` consumes.
|
|
51
|
+
var PURPOSES = ["serverAuth", "emailProtection", "codeSigning"];
|
|
52
|
+
|
|
53
|
+
// CKA_TRUST_* attribute -> purpose key.
|
|
54
|
+
var PURPOSE_ATTRS = [
|
|
55
|
+
["CKA_TRUST_SERVER_AUTH", "serverAuth"],
|
|
56
|
+
["CKA_TRUST_EMAIL_PROTECTION", "emailProtection"],
|
|
57
|
+
["CKA_TRUST_CODE_SIGNING", "codeSigning"],
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
// The recognized CK_TRUST vocabulary. ONLY CKT_NSS_TRUSTED_DELEGATOR grants a
|
|
61
|
+
// purpose; the rest are recognized-but-untrusted (fail-closed: an absent or
|
|
62
|
+
// non-delegator bit never defaults to trusted). A token outside this set is a
|
|
63
|
+
// typed trust/bad-trust-value, never a silent false.
|
|
64
|
+
var CKT_RECOGNIZED = Object.create(null);
|
|
65
|
+
["CKT_NSS_TRUSTED_DELEGATOR", "CKT_NSS_TRUSTED", "CKT_NSS_MUST_VERIFY_TRUST",
|
|
66
|
+
"CKT_NSS_TRUST_UNKNOWN", "CKT_NSS_NOT_TRUSTED", "CKT_NSS_VALID_DELEGATOR"]
|
|
67
|
+
.forEach(function (t) { CKT_RECOGNIZED[t] = true; });
|
|
68
|
+
var CKT_DELEGATOR = "CKT_NSS_TRUSTED_DELEGATOR";
|
|
69
|
+
|
|
70
|
+
// An attribute line: `CKA_<NAME> <type> [<value>]`. The value grammar is
|
|
71
|
+
// per-type (a quoted UTF8 string, a single token, or nothing for
|
|
72
|
+
// MULTILINE_OCTAL whose payload follows on its own lines).
|
|
73
|
+
var ATTR_RE = /^(CKA_[A-Z0-9_]+)(?:\s+(\S+)(?:\s+(.*))?)?$/;
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// certdata.txt lexer -- line-oriented, C.LIMITS-bounded, fail-closed
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
// Decode a MULTILINE_OCTAL body: whitespace-separated runs of `\ooo` escapes
|
|
80
|
+
// (exactly a backslash + three octal digits, \000..\377), terminated by a line
|
|
81
|
+
// that is exactly `END`. Every fault is refused before the byte lands: an
|
|
82
|
+
// escape past \377, a short or non-octal escape, a stray token, a blank line
|
|
83
|
+
// or EOF before END, or a blob past TRUST_MAX_OCTAL_BYTES.
|
|
84
|
+
function _readOctalBody(lines, idx) {
|
|
85
|
+
var out = [];
|
|
86
|
+
var cap = LIMITS.TRUST_MAX_OCTAL_BYTES;
|
|
87
|
+
for (var i = idx; i < lines.length; i++) {
|
|
88
|
+
var t = lines[i].trim();
|
|
89
|
+
if (t === "END") return { bytes: Buffer.from(out), next: i + 1 };
|
|
90
|
+
if (t === "") throw E("trust/bad-octal", "MULTILINE_OCTAL body interrupted by a blank line before END");
|
|
91
|
+
var chunks = t.split(/\s+/);
|
|
92
|
+
for (var c = 0; c < chunks.length; c++) {
|
|
93
|
+
var chunk = chunks[c];
|
|
94
|
+
for (var k = 0; k < chunk.length; k += 4) {
|
|
95
|
+
if (chunk.charCodeAt(k) !== 92 /* backslash */) {
|
|
96
|
+
throw E("trust/bad-octal", "MULTILINE_OCTAL expects \\ooo escapes, got " + JSON.stringify(chunk.slice(k, k + 4)));
|
|
97
|
+
}
|
|
98
|
+
var d = chunk.slice(k + 1, k + 4);
|
|
99
|
+
if (!/^[0-7]{3}$/.test(d)) {
|
|
100
|
+
throw E("trust/bad-octal", "a MULTILINE_OCTAL escape is a backslash + exactly three octal digits, got " + JSON.stringify("\\" + d));
|
|
101
|
+
}
|
|
102
|
+
if (d.charCodeAt(0) > 51 /* '3' */) {
|
|
103
|
+
throw E("trust/bad-octal", "octal escape \\" + d + " exceeds \\377 (one byte)");
|
|
104
|
+
}
|
|
105
|
+
if (out.length >= cap) throw E("trust/bad-block", "MULTILINE_OCTAL blob exceeds LIMITS.TRUST_MAX_OCTAL_BYTES");
|
|
106
|
+
out.push(parseInt(d, 8));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
throw E("trust/bad-octal", "MULTILINE_OCTAL body reached end of input before END");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Lex the object stream: a `#` line is a comment, the preamble ends at
|
|
114
|
+
// BEGINDATA, blank lines delimit object blocks, each block is typed
|
|
115
|
+
// `CKA_* <type> <value>` attribute lines. Returns [{ attrs }] where attrs maps
|
|
116
|
+
// the attribute name to { type, value | bytes }.
|
|
117
|
+
function _lexObjects(text) {
|
|
118
|
+
var lines = text.split(/\r\n|\n|\r/);
|
|
119
|
+
var n = lines.length;
|
|
120
|
+
var i = 0, sawBegin = false;
|
|
121
|
+
for (; i < n; i++) {
|
|
122
|
+
if (lines[i].trim() === "BEGINDATA") { sawBegin = true; i++; break; }
|
|
123
|
+
}
|
|
124
|
+
if (!sawBegin) throw E("trust/bad-input", "not a certdata stream: no BEGINDATA line");
|
|
125
|
+
|
|
126
|
+
var objects = [];
|
|
127
|
+
var cur = null;
|
|
128
|
+
while (i < n) {
|
|
129
|
+
var t = lines[i].trim();
|
|
130
|
+
if (t === "") {
|
|
131
|
+
if (cur) { objects.push(cur); cur = null; }
|
|
132
|
+
i++; continue;
|
|
133
|
+
}
|
|
134
|
+
if (t.charAt(0) === "#") { i++; continue; }
|
|
135
|
+
var m = ATTR_RE.exec(t);
|
|
136
|
+
if (!m) throw E("trust/bad-block", "unexpected line in the certdata object stream: " + JSON.stringify(t.slice(0, 64)));
|
|
137
|
+
var name = m[1], type = m[2];
|
|
138
|
+
var rawVal = m[3] !== undefined ? m[3].trim() : undefined;
|
|
139
|
+
if (!type) throw E("trust/bad-block", "attribute line is missing its type token: " + name);
|
|
140
|
+
if (!cur) {
|
|
141
|
+
// Refuse the block bomb BEFORE lexing another block's payloads.
|
|
142
|
+
if (objects.length >= LIMITS.TRUST_MAX_OBJECTS) throw E("trust/bad-block", "certdata object count exceeds LIMITS.TRUST_MAX_OBJECTS");
|
|
143
|
+
cur = { attrs: Object.create(null) };
|
|
144
|
+
}
|
|
145
|
+
if (cur.attrs[name]) throw E("trust/bad-block", "duplicate attribute " + name + " within one object block");
|
|
146
|
+
|
|
147
|
+
var entry;
|
|
148
|
+
if (type === "MULTILINE_OCTAL") {
|
|
149
|
+
if (rawVal !== undefined && rawVal !== "") throw E("trust/bad-block", name + " MULTILINE_OCTAL carries an unexpected inline value");
|
|
150
|
+
var r = _readOctalBody(lines, i + 1);
|
|
151
|
+
cur.attrs[name] = { type: type, bytes: r.bytes };
|
|
152
|
+
i = r.next;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (type === "CK_BBOOL") {
|
|
156
|
+
if (rawVal !== "CK_TRUE" && rawVal !== "CK_FALSE") throw E("trust/bad-block", name + " CK_BBOOL must be CK_TRUE or CK_FALSE");
|
|
157
|
+
entry = { type: type, value: rawVal === "CK_TRUE" };
|
|
158
|
+
} else if (type === "UTF8") {
|
|
159
|
+
var q = rawVal !== undefined ? /^"([^"]*)"$/.exec(rawVal) : null;
|
|
160
|
+
if (!q) throw E("trust/bad-block", name + " UTF8 must carry one double-quoted value");
|
|
161
|
+
entry = { type: type, value: q[1] };
|
|
162
|
+
} else if (type === "CK_TRUST") {
|
|
163
|
+
if (rawVal === undefined || /\s/.test(rawVal)) throw E("trust/bad-block", name + " CK_TRUST must carry a single token value");
|
|
164
|
+
if (!CKT_RECOGNIZED[rawVal]) throw E("trust/bad-trust-value", "unrecognized CK_TRUST value " + JSON.stringify(rawVal) + " on " + name);
|
|
165
|
+
entry = { type: type, value: rawVal };
|
|
166
|
+
} else if (type === "CK_OBJECT_CLASS" || type === "CK_CERTIFICATE_TYPE") {
|
|
167
|
+
if (rawVal === undefined || /\s/.test(rawVal)) throw E("trust/bad-block", name + " " + type + " must carry a single token value");
|
|
168
|
+
entry = { type: type, value: rawVal };
|
|
169
|
+
} else {
|
|
170
|
+
// An unknown value TYPE cannot be lexed safely (its value grammar is
|
|
171
|
+
// unknown) -- fail closed rather than guess past it.
|
|
172
|
+
throw E("trust/bad-block", "unrecognized attribute type " + JSON.stringify(type) + " on " + name);
|
|
173
|
+
}
|
|
174
|
+
cur.attrs[name] = entry;
|
|
175
|
+
i++;
|
|
176
|
+
}
|
|
177
|
+
if (cur) objects.push(cur);
|
|
178
|
+
return objects;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// certdata semantics -- pairing, distrust-after, purpose bits
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
function _requireOctal(obj, name, what) {
|
|
186
|
+
var a = obj.attrs[name];
|
|
187
|
+
if (!a || a.type !== "MULTILINE_OCTAL") {
|
|
188
|
+
throw E("trust/bad-block", what + " object is missing its " + name + " MULTILINE_OCTAL attribute");
|
|
189
|
+
}
|
|
190
|
+
return a.bytes;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// The NSS distrust-after payload is the BARE ASCII of a YYMMDDHHMMSSZ UTCTime
|
|
194
|
+
// (13 bytes) or a YYYYMMDDHHMMSSZ GeneralizedTime (15 bytes) -- no DER tag or
|
|
195
|
+
// length. Synthesize the minimal TLV and route it through the strict asn1
|
|
196
|
+
// time reader so the Z-terminator / mandatory-seconds / component-rollover /
|
|
197
|
+
// RFC 5280 sec. 4.1.2.5.1 2050-pivot rules are enforced once, never re-derived.
|
|
198
|
+
function _strictTime(payload, code, label) {
|
|
199
|
+
var tlv;
|
|
200
|
+
if (payload.length === 13) tlv = Buffer.concat([Buffer.from([0x17, 0x0d]), payload]);
|
|
201
|
+
else if (payload.length === 15) tlv = Buffer.concat([Buffer.from([0x18, 0x0f]), payload]);
|
|
202
|
+
else throw E(code, label + " must be a 13-byte UTCTime or a 15-byte GeneralizedTime payload, got " + payload.length + " bytes");
|
|
203
|
+
try { return asn1.read.time(asn1.decode(tlv)); }
|
|
204
|
+
catch (e) { throw E(code, label + " does not decode as a strict time", e); }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// CKA_NSS_*_DISTRUST_AFTER: either `CK_BBOOL CK_FALSE` (no distrust-after in
|
|
208
|
+
// force) or a MULTILINE_OCTAL bare-ASCII time. Anything else fails closed.
|
|
209
|
+
function _distrustDate(obj, name) {
|
|
210
|
+
var a = obj.attrs[name];
|
|
211
|
+
if (!a) return null;
|
|
212
|
+
if (a.type === "CK_BBOOL") {
|
|
213
|
+
if (a.value === false) return null;
|
|
214
|
+
throw E("trust/bad-distrust-after", name + " CK_BBOOL may only be CK_FALSE");
|
|
215
|
+
}
|
|
216
|
+
if (a.type !== "MULTILINE_OCTAL") throw E("trust/bad-distrust-after", name + " must be CK_BBOOL CK_FALSE or MULTILINE_OCTAL");
|
|
217
|
+
return _strictTime(a.bytes, "trust/bad-distrust-after", name);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function _certEntry(obj) {
|
|
221
|
+
var der = _requireOctal(obj, "CKA_VALUE", "certificate");
|
|
222
|
+
var cert;
|
|
223
|
+
try { cert = x509.parse(der); }
|
|
224
|
+
catch (e) { throw E("trust/not-a-certificate", "CKA_VALUE does not parse as an X.509 certificate", e); }
|
|
225
|
+
|
|
226
|
+
var issuer = _requireOctal(obj, "CKA_ISSUER", "certificate");
|
|
227
|
+
var serial = _requireOctal(obj, "CKA_SERIAL_NUMBER", "certificate");
|
|
228
|
+
|
|
229
|
+
// Fail-closed cross-check (the pairing key vs the parsed DER): a block whose
|
|
230
|
+
// own identity octets disagree with its certificate would let trust metadata
|
|
231
|
+
// attach to the wrong root -- a trust-elevation bug, not a tolerable skew.
|
|
232
|
+
if (!issuer.equals(cert.issuer.bytes)) {
|
|
233
|
+
throw E("trust/pairing-mismatch", "CKA_ISSUER disagrees with the certificate's issuer DER");
|
|
234
|
+
}
|
|
235
|
+
var serialContentHex;
|
|
236
|
+
try {
|
|
237
|
+
var sn = asn1.decode(serial);
|
|
238
|
+
asn1.read.integer(sn); // strict INTEGER (tag + minimal content)
|
|
239
|
+
serialContentHex = sn.content.toString("hex");
|
|
240
|
+
} catch (e) {
|
|
241
|
+
throw E("trust/pairing-mismatch", "CKA_SERIAL_NUMBER is not a DER INTEGER", e);
|
|
242
|
+
}
|
|
243
|
+
if (serialContentHex !== cert.serialNumberHex) {
|
|
244
|
+
throw E("trust/pairing-mismatch", "CKA_SERIAL_NUMBER disagrees with the certificate's serial");
|
|
245
|
+
}
|
|
246
|
+
var subject = obj.attrs["CKA_SUBJECT"];
|
|
247
|
+
if (subject && (subject.type !== "MULTILINE_OCTAL" || !subject.bytes.equals(cert.subject.bytes))) {
|
|
248
|
+
throw E("trust/pairing-mismatch", "CKA_SUBJECT disagrees with the certificate's subject DER");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
var label = obj.attrs["CKA_LABEL"];
|
|
252
|
+
if (label && label.type !== "UTF8") throw E("trust/bad-block", "CKA_LABEL must be UTF8");
|
|
253
|
+
var policy = obj.attrs["CKA_NSS_MOZILLA_CA_POLICY"];
|
|
254
|
+
if (policy && policy.type !== "CK_BBOOL") throw E("trust/bad-block", "CKA_NSS_MOZILLA_CA_POLICY must be CK_BBOOL");
|
|
255
|
+
|
|
256
|
+
var distrustAfter = {};
|
|
257
|
+
var server = _distrustDate(obj, "CKA_NSS_SERVER_DISTRUST_AFTER");
|
|
258
|
+
if (server) distrustAfter.serverAuth = server;
|
|
259
|
+
var email = _distrustDate(obj, "CKA_NSS_EMAIL_DISTRUST_AFTER");
|
|
260
|
+
if (email) distrustAfter.emailProtection = email;
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
key: issuer.toString("hex") + "/" + serial.toString("hex"),
|
|
264
|
+
der: der,
|
|
265
|
+
cert: cert,
|
|
266
|
+
label: label ? label.value : null,
|
|
267
|
+
mozillaCaPolicy: !!(policy && policy.value === true),
|
|
268
|
+
distrustAfter: distrustAfter,
|
|
269
|
+
purposes: null, // attached by the paired CKO_NSS_TRUST object
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function _trustEntry(obj) {
|
|
274
|
+
var issuer = _requireOctal(obj, "CKA_ISSUER", "trust");
|
|
275
|
+
var serial = _requireOctal(obj, "CKA_SERIAL_NUMBER", "trust");
|
|
276
|
+
var purposes = { serverAuth: false, emailProtection: false, codeSigning: false };
|
|
277
|
+
PURPOSE_ATTRS.forEach(function (pair) {
|
|
278
|
+
var a = obj.attrs[pair[0]];
|
|
279
|
+
if (!a) return; // an absent bit is untrusted (fail-closed)
|
|
280
|
+
if (a.type !== "CK_TRUST") throw E("trust/bad-block", pair[0] + " must be CK_TRUST");
|
|
281
|
+
purposes[pair[1]] = a.value === CKT_DELEGATOR;
|
|
282
|
+
});
|
|
283
|
+
return { key: issuer.toString("hex") + "/" + serial.toString("hex"), purposes: purposes };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
// Anchor assembly + dedup (shared by both sources)
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
function _mkAnchor(cert, meta) {
|
|
291
|
+
var spki = cert.subjectPublicKeyInfo;
|
|
292
|
+
return {
|
|
293
|
+
name: cert.subject, // the object with .rdns (name chaining)
|
|
294
|
+
publicKey: spki.bytes, // the full SPKI SEQUENCE TLV
|
|
295
|
+
algorithm: spki.algorithm.oid, // the SPKI public-key algorithm OID
|
|
296
|
+
parameters: spki.algorithm.parameters, // e.g. an EC namedCurve (Buffer|null)
|
|
297
|
+
subjectDer: cert.subject.bytes,
|
|
298
|
+
distrustAfter: meta.distrustAfter,
|
|
299
|
+
purposes: meta.purposes,
|
|
300
|
+
label: meta.label,
|
|
301
|
+
mozillaCaPolicy: meta.mozillaCaPolicy,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function _datesEqual(x, y) {
|
|
306
|
+
var kx = Object.keys(x).sort(), ky = Object.keys(y).sort();
|
|
307
|
+
if (kx.join(",") !== ky.join(",")) return false;
|
|
308
|
+
return kx.every(function (k) { return x[k].getTime() === y[k].getTime(); });
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function _purposesEqual(x, y) {
|
|
312
|
+
return PURPOSES.every(function (p) { return x[p] === y[p]; });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function _paramsEqual(x, y) {
|
|
316
|
+
if (x === null || y === null) return x === y;
|
|
317
|
+
return Buffer.isBuffer(x) && Buffer.isBuffer(y) && x.equals(y);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function _anchorsAgree(x, y) {
|
|
321
|
+
return x.label === y.label &&
|
|
322
|
+
x.mozillaCaPolicy === y.mozillaCaPolicy &&
|
|
323
|
+
x.algorithm === y.algorithm &&
|
|
324
|
+
_paramsEqual(x.parameters, y.parameters) &&
|
|
325
|
+
_purposesEqual(x.purposes, y.purposes) &&
|
|
326
|
+
_datesEqual(x.distrustAfter, y.distrustAfter);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Dedup by (subjectDer, publicKey): the SAME root appearing twice (a
|
|
330
|
+
// duplicated block, or certdata + CCADB merged by the operator) collapses to
|
|
331
|
+
// one anchor; a DISTINCT root that merely shares a subject DN has a different
|
|
332
|
+
// key and survives. Two entries for one root that DISAGREE on trust metadata
|
|
333
|
+
// are ambiguous -- fail closed rather than silently pick one.
|
|
334
|
+
function _dedupAnchors(anchors) {
|
|
335
|
+
var seen = Object.create(null);
|
|
336
|
+
var out = [];
|
|
337
|
+
anchors.forEach(function (a) {
|
|
338
|
+
var key = a.subjectDer.toString("hex") + "/" + a.publicKey.toString("hex");
|
|
339
|
+
var prev = seen[key];
|
|
340
|
+
if (!prev) { seen[key] = a; out.push(a); return; }
|
|
341
|
+
if (!_anchorsAgree(prev, a)) {
|
|
342
|
+
throw E("trust/pairing-mismatch", "two entries for one root (same subject + key) disagree on trust metadata");
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
return out;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
// Public surface
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* @primitive pki.trust.parseCertdata
|
|
354
|
+
* @signature pki.trust.parseCertdata(text) -> { anchors }
|
|
355
|
+
* @since 0.2.0
|
|
356
|
+
* @status experimental
|
|
357
|
+
* @spec RFC 5280 (NSS certdata.txt object stream)
|
|
358
|
+
* @defends trust-metadata-misattribution (CWE-345), trust-store-parser-DoS (CWE-770)
|
|
359
|
+
* @related pki.trust.parseCcadbCsv, pki.trust.anchor, pki.path.validate
|
|
360
|
+
*
|
|
361
|
+
* Parse the Mozilla/NSS `certdata.txt` root-store object stream into
|
|
362
|
+
* constraint-carrying trust anchors. Each CKO_CERTIFICATE object's
|
|
363
|
+
* MULTILINE_OCTAL `CKA_VALUE` is decoded and parsed as a DER certificate; the
|
|
364
|
+
* paired CKO_NSS_TRUST object -- joined by byte-exact CKA_ISSUER +
|
|
365
|
+
* CKA_SERIAL_NUMBER, never adjacency, and cross-checked against the parsed
|
|
366
|
+
* DER -- contributes the purpose trust bits (only CKT_NSS_TRUSTED_DELEGATOR
|
|
367
|
+
* grants a purpose); the per-purpose distrust-after dates ride in the
|
|
368
|
+
* certificate object as bare ASCII times routed through the strict DER time
|
|
369
|
+
* reader. Every anchor carries the exact `{ name, publicKey, algorithm,
|
|
370
|
+
* parameters }` shape `pki.path.validate` consumes plus `distrustAfter`,
|
|
371
|
+
* `purposes`, `subjectDer`, `label`, `mozillaCaPolicy`. A certificate with no
|
|
372
|
+
* trust object becomes an anchor trusted for nothing (never silently
|
|
373
|
+
* dropped); a trust object with no certificate grants nothing. Malformed
|
|
374
|
+
* octal, an oversized block, an unrecognized trust value, a mispaired or
|
|
375
|
+
* ambiguous-duplicate block, or an undecodable distrust-after time throws a
|
|
376
|
+
* typed `trust/*` error -- never a silently truncated or misattributed root.
|
|
377
|
+
*
|
|
378
|
+
* @example
|
|
379
|
+
* // Real input is the NSS certdata.txt read from disk; a one-root stream is
|
|
380
|
+
* // synthesized here from a DER certificate to show the object shape.
|
|
381
|
+
* var cert = pki.schema.x509.parse(der);
|
|
382
|
+
* var oct = function (buf) { return Array.prototype.map.call(buf, function (b) { return "\\" + ("000" + b.toString(8)).slice(-3); }).join(""); };
|
|
383
|
+
* var blk = function (n, v) { return n + " MULTILINE_OCTAL\n" + oct(v) + "\nEND\n"; };
|
|
384
|
+
* var store = pki.trust.parseCertdata("BEGINDATA\n\n" +
|
|
385
|
+
* "CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE\n" +
|
|
386
|
+
* blk("CKA_ISSUER", cert.issuer.bytes) +
|
|
387
|
+
* blk("CKA_SERIAL_NUMBER", pki.asn1.build.integer(cert.serialNumber)) +
|
|
388
|
+
* blk("CKA_VALUE", der));
|
|
389
|
+
* store.anchors[0].purposes.serverAuth; // -> false (no trust object: trusted for nothing)
|
|
390
|
+
*/
|
|
391
|
+
function parseCertdata(text) {
|
|
392
|
+
text = guard.text.decode(text, LIMITS.TRUST_MAX_BYTES, TrustError, {
|
|
393
|
+
charset: "latin1", tooLarge: "trust/bad-block", badInput: "trust/bad-input", label: "certdata input",
|
|
394
|
+
});
|
|
395
|
+
var objects = _lexObjects(text);
|
|
396
|
+
|
|
397
|
+
var certs = [];
|
|
398
|
+
var byKey = Object.create(null);
|
|
399
|
+
var trusts = [];
|
|
400
|
+
objects.forEach(function (obj) {
|
|
401
|
+
var cls = obj.attrs["CKA_CLASS"];
|
|
402
|
+
if (!cls || cls.type !== "CK_OBJECT_CLASS") {
|
|
403
|
+
throw E("trust/bad-block", "object block is missing CKA_CLASS CK_OBJECT_CLASS");
|
|
404
|
+
}
|
|
405
|
+
if (cls.value === "CKO_CERTIFICATE") {
|
|
406
|
+
var e = _certEntry(obj);
|
|
407
|
+
var prev = byKey[e.key];
|
|
408
|
+
if (prev) {
|
|
409
|
+
// A byte-identical duplicated block collapses; two certificate
|
|
410
|
+
// objects claiming one (issuer, serial) with different content are a
|
|
411
|
+
// forged identity -- fail closed.
|
|
412
|
+
if (!prev.der.equals(e.der) || prev.label !== e.label ||
|
|
413
|
+
prev.mozillaCaPolicy !== e.mozillaCaPolicy ||
|
|
414
|
+
!_datesEqual(prev.distrustAfter, e.distrustAfter)) {
|
|
415
|
+
throw E("trust/pairing-mismatch", "two certificate objects share an (issuer, serial) but disagree");
|
|
416
|
+
}
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
byKey[e.key] = e;
|
|
420
|
+
certs.push(e);
|
|
421
|
+
} else if (cls.value === "CKO_NSS_TRUST") {
|
|
422
|
+
trusts.push(_trustEntry(obj));
|
|
423
|
+
}
|
|
424
|
+
// Any other class (CKO_NSS_BUILTIN_ROOT_LIST, ...) is not a root: skipped.
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
trusts.forEach(function (t) {
|
|
428
|
+
var c = byKey[t.key];
|
|
429
|
+
if (!c) return; // a trust object with no certificate object grants nothing
|
|
430
|
+
if (c.purposes) {
|
|
431
|
+
if (!_purposesEqual(c.purposes, t.purposes)) {
|
|
432
|
+
throw E("trust/pairing-mismatch", "two trust objects for one certificate disagree on purpose bits");
|
|
433
|
+
}
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
c.purposes = t.purposes;
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
anchors: _dedupAnchors(certs.map(function (c) {
|
|
441
|
+
return _mkAnchor(c.cert, {
|
|
442
|
+
distrustAfter: c.distrustAfter,
|
|
443
|
+
purposes: c.purposes || { serverAuth: false, emailProtection: false, codeSigning: false },
|
|
444
|
+
label: c.label,
|
|
445
|
+
mozillaCaPolicy: c.mozillaCaPolicy,
|
|
446
|
+
});
|
|
447
|
+
})),
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ---------------------------------------------------------------------------
|
|
452
|
+
// CCADB CSV -- a small RFC 4180 sub-lexer + header-keyed column mapping
|
|
453
|
+
// ---------------------------------------------------------------------------
|
|
454
|
+
|
|
455
|
+
var CSV_REQUIRED = [
|
|
456
|
+
"Common Name or Certificate Name",
|
|
457
|
+
"Trust Bits",
|
|
458
|
+
"Distrust for TLS After Date",
|
|
459
|
+
"Distrust for S/MIME After Date",
|
|
460
|
+
"PEM Info",
|
|
461
|
+
];
|
|
462
|
+
|
|
463
|
+
// RFC 4180 field lexer: quoted fields, embedded commas / newlines, `""`
|
|
464
|
+
// escapes. Strict: a quote may only open at the start of a field, nothing may
|
|
465
|
+
// follow a closing quote but a separator, and an unterminated quote at EOF is
|
|
466
|
+
// a framing violation. Row and field ceilings are refused as they are hit.
|
|
467
|
+
function _csvRows(text) {
|
|
468
|
+
var rows = [], row = [], field = "";
|
|
469
|
+
var inQuotes = false, quoted = false, closed = false;
|
|
470
|
+
var fieldCap = LIMITS.TRUST_MAX_CSV_FIELD_BYTES;
|
|
471
|
+
|
|
472
|
+
function appendChar(c) {
|
|
473
|
+
if (field.length >= fieldCap) throw E("trust/bad-csv", "a CSV field exceeds LIMITS.TRUST_MAX_CSV_FIELD_BYTES");
|
|
474
|
+
field += c;
|
|
475
|
+
}
|
|
476
|
+
function endField() { row.push(field); field = ""; quoted = false; closed = false; }
|
|
477
|
+
function endRow() {
|
|
478
|
+
endField();
|
|
479
|
+
if (rows.length >= LIMITS.TRUST_MAX_CSV_ROWS) throw E("trust/bad-csv", "CSV row count exceeds LIMITS.TRUST_MAX_CSV_ROWS");
|
|
480
|
+
rows.push(row); row = [];
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
var i = 0, n = text.length;
|
|
484
|
+
while (i < n) {
|
|
485
|
+
var ch = text.charAt(i);
|
|
486
|
+
if (inQuotes) {
|
|
487
|
+
if (ch === "\"") {
|
|
488
|
+
if (text.charAt(i + 1) === "\"") { appendChar("\""); i += 2; continue; }
|
|
489
|
+
inQuotes = false; closed = true; i++; continue;
|
|
490
|
+
}
|
|
491
|
+
appendChar(ch); i++; continue;
|
|
492
|
+
}
|
|
493
|
+
if (ch === "\"") {
|
|
494
|
+
if (field !== "" || quoted) throw E("trust/bad-csv", "a quote may only open at the start of a field (RFC 4180)");
|
|
495
|
+
inQuotes = true; quoted = true; i++; continue;
|
|
496
|
+
}
|
|
497
|
+
if (ch === ",") { endField(); i++; continue; }
|
|
498
|
+
if (ch === "\r" && text.charAt(i + 1) === "\n") { endRow(); i += 2; continue; }
|
|
499
|
+
if (ch === "\n" || ch === "\r") { endRow(); i++; continue; }
|
|
500
|
+
if (closed) throw E("trust/bad-csv", "content after a closing quote (RFC 4180)");
|
|
501
|
+
appendChar(ch); i++;
|
|
502
|
+
}
|
|
503
|
+
if (inQuotes) throw E("trust/bad-csv", "unterminated quoted field at end of input");
|
|
504
|
+
if (field !== "" || quoted || row.length > 0) endRow();
|
|
505
|
+
return rows;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// `Trust Bits` is set-valued (`Websites; Email`) and CCADB spells it in two
|
|
509
|
+
// vocabularies: the Mozilla-report labels (Websites / Email / Code) and the
|
|
510
|
+
// EKU-derived trust-bit names current exports carry (Server Authentication /
|
|
511
|
+
// Secure Email / Code Signing). Both map to the same purposes. The KNOWN CCADB
|
|
512
|
+
// purposes the anchor model does not track (Client Authentication, Document
|
|
513
|
+
// Signing, Time Stamping, OCSP Signing) are tolerated and grant NOTHING -- a
|
|
514
|
+
// purpose is only ever granted by a recognized delegator token, so ignoring an
|
|
515
|
+
// unmapped one fails closed. An empty cell grants nothing; a token outside the
|
|
516
|
+
// known universe is still surfaced (a silently-skipped NEW bit would hide
|
|
517
|
+
// vocabulary drift), never guessed.
|
|
518
|
+
var _TRUST_BIT_TOKENS = {
|
|
519
|
+
"websites": "serverAuth", "server authentication": "serverAuth",
|
|
520
|
+
"email": "emailProtection", "secure email": "emailProtection",
|
|
521
|
+
"code": "codeSigning", "code signing": "codeSigning",
|
|
522
|
+
"client authentication": null, "document signing": null,
|
|
523
|
+
"time stamping": null, "ocsp signing": null,
|
|
524
|
+
};
|
|
525
|
+
function _trustBits(cell) {
|
|
526
|
+
var purposes = { serverAuth: false, emailProtection: false, codeSigning: false };
|
|
527
|
+
String(cell).split(";").forEach(function (tok) {
|
|
528
|
+
var t = tok.trim();
|
|
529
|
+
if (t === "") return;
|
|
530
|
+
var l = t.toLowerCase();
|
|
531
|
+
if (!Object.prototype.hasOwnProperty.call(_TRUST_BIT_TOKENS, l)) {
|
|
532
|
+
throw E("trust/bad-csv", "unrecognized Trust Bits token " + JSON.stringify(t));
|
|
533
|
+
}
|
|
534
|
+
var p = _TRUST_BIT_TOKENS[l];
|
|
535
|
+
if (p) purposes[p] = true;
|
|
536
|
+
});
|
|
537
|
+
return purposes;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// The CCADB distrust columns are DATE-only. Expand Y-M-D to the end-of-day
|
|
541
|
+
// ...T23:59:59Z instant -- the same instant NSS encodes as `...235959Z` -- by
|
|
542
|
+
// synthesizing a GeneralizedTime and routing through the strict reader (which
|
|
543
|
+
// also rejects a rolled-over calendar date). Dotted, dashed, and slashed
|
|
544
|
+
// separators are accepted (the export format has drifted between them).
|
|
545
|
+
function _csvDate(cell, label) {
|
|
546
|
+
var s = String(cell).trim();
|
|
547
|
+
if (s === "") return null;
|
|
548
|
+
var m = /^(\d{4})[./-](\d{1,2})[./-](\d{1,2})$/.exec(s);
|
|
549
|
+
if (!m) throw E("trust/bad-csv", label + " must be a Y-M-D date, got " + JSON.stringify(s));
|
|
550
|
+
var pad = function (x) { return x.length === 1 ? "0" + x : x; };
|
|
551
|
+
var payload = Buffer.from(m[1] + pad(m[2]) + pad(m[3]) + "235959Z", "latin1");
|
|
552
|
+
try { return _strictTime(payload, "trust/bad-csv", label); }
|
|
553
|
+
catch (e) {
|
|
554
|
+
if (e && e.code === "trust/bad-csv") throw e;
|
|
555
|
+
throw E("trust/bad-csv", label + " is not a real calendar date: " + JSON.stringify(s), e);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// A CCADB `PEM Info` cell: the PEM certificate, commonly wrapped in one layer
|
|
560
|
+
// of single quotes (a spreadsheet-import guard) -- strip that layer, then
|
|
561
|
+
// parse strictly.
|
|
562
|
+
function _pemCell(cell) {
|
|
563
|
+
var s = String(cell).trim();
|
|
564
|
+
if (s.charAt(0) === "'") s = s.slice(1);
|
|
565
|
+
if (s.charAt(s.length - 1) === "'") s = s.slice(0, -1);
|
|
566
|
+
s = s.trim();
|
|
567
|
+
if (s === "") throw E("trust/bad-csv", "PEM Info cell is empty");
|
|
568
|
+
try { return x509.parse(s); }
|
|
569
|
+
catch (e) { throw E("trust/not-a-certificate", "PEM Info does not parse as an X.509 certificate", e); }
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* @primitive pki.trust.parseCcadbCsv
|
|
574
|
+
* @signature pki.trust.parseCcadbCsv(text) -> { anchors }
|
|
575
|
+
* @since 0.2.0
|
|
576
|
+
* @status experimental
|
|
577
|
+
* @spec RFC 4180, RFC 5280 (CCADB certificate-records CSV)
|
|
578
|
+
* @defends trust-metadata-misattribution (CWE-345), trust-store-parser-DoS (CWE-770)
|
|
579
|
+
* @related pki.trust.parseCertdata, pki.trust.anchor, pki.path.validate
|
|
580
|
+
*
|
|
581
|
+
* Parse a CCADB certificate-records CSV export into the SAME `Anchor` shape
|
|
582
|
+
* `parseCertdata` produces, so downstream enforcement is source-agnostic.
|
|
583
|
+
* Columns are located by header NAME -- never by position -- and unknown,
|
|
584
|
+
* reordered, or extra columns are tolerated; a MISSING required column
|
|
585
|
+
* (`Common Name or Certificate Name`, `Trust Bits`, `Distrust for TLS After
|
|
586
|
+
* Date`, `Distrust for S/MIME After Date`, `PEM Info`) fails closed with
|
|
587
|
+
* `trust/bad-csv`. Fields follow RFC 4180 quoting (embedded commas, newlines,
|
|
588
|
+
* doubled-quote escapes -- the PEM Info column depends on it). `Trust Bits`
|
|
589
|
+
* is set-valued (Websites -> serverAuth, Email -> emailProtection, Code ->
|
|
590
|
+
* codeSigning; absent -> untrusted). A DATE-only distrust cell expands to the
|
|
591
|
+
* end-of-day `...T23:59:59Z` instant, matching the NSS `...235959Z`
|
|
592
|
+
* encoding, through the same strict time reader.
|
|
593
|
+
*
|
|
594
|
+
* @example
|
|
595
|
+
* var csv = "Common Name or Certificate Name,Trust Bits," +
|
|
596
|
+
* "Distrust for TLS After Date,Distrust for S/MIME After Date,PEM Info\n" +
|
|
597
|
+
* 'Example Root,"Websites; Email",2027.06.01,,"' + pemText + '"';
|
|
598
|
+
* var store = pki.trust.parseCcadbCsv(csv);
|
|
599
|
+
* store.anchors[0].purposes.serverAuth; // -> true
|
|
600
|
+
* store.anchors[0].distrustAfter.serverAuth.toISOString(); // -> "2027-06-01T23:59:59.000Z"
|
|
601
|
+
*/
|
|
602
|
+
function parseCcadbCsv(text) {
|
|
603
|
+
text = guard.text.decode(text, LIMITS.TRUST_MAX_BYTES, TrustError, {
|
|
604
|
+
charset: "utf-8", fatal: true,
|
|
605
|
+
tooLarge: "trust/bad-block", badDecode: "trust/bad-csv", badInput: "trust/bad-input",
|
|
606
|
+
label: "CCADB CSV input",
|
|
607
|
+
});
|
|
608
|
+
var rows = _csvRows(text);
|
|
609
|
+
if (rows.length === 0) throw E("trust/bad-csv", "CSV input carries no header row");
|
|
610
|
+
|
|
611
|
+
// trim() also strips a leading U+FEFF, so a BOM-prefixed export resolves
|
|
612
|
+
// its first header cell normally.
|
|
613
|
+
var header = rows[0].map(function (h) { return h.trim(); });
|
|
614
|
+
var col = Object.create(null);
|
|
615
|
+
header.forEach(function (h, idx) {
|
|
616
|
+
if (col[h] !== undefined) {
|
|
617
|
+
// Two columns with one REQUIRED name make the mapping ambiguous (which
|
|
618
|
+
// Trust Bits column binds?) -- fail closed rather than silently pick.
|
|
619
|
+
if (CSV_REQUIRED.indexOf(h) !== -1) throw E("trust/bad-csv", "duplicate required column " + JSON.stringify(h));
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
col[h] = idx;
|
|
623
|
+
});
|
|
624
|
+
CSV_REQUIRED.forEach(function (r) {
|
|
625
|
+
if (col[r] === undefined) throw E("trust/bad-csv", "CSV is missing the required column " + JSON.stringify(r));
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
var anchors = [];
|
|
629
|
+
for (var i = 1; i < rows.length; i++) {
|
|
630
|
+
var row = rows[i];
|
|
631
|
+
if (row.length !== header.length) {
|
|
632
|
+
throw E("trust/bad-csv", "CSV row " + (i + 1) + " has " + row.length + " fields; the header has " + header.length);
|
|
633
|
+
}
|
|
634
|
+
var distrustAfter = {};
|
|
635
|
+
var tls = _csvDate(row[col["Distrust for TLS After Date"]], "Distrust for TLS After Date");
|
|
636
|
+
if (tls) distrustAfter.serverAuth = tls;
|
|
637
|
+
var smime = _csvDate(row[col["Distrust for S/MIME After Date"]], "Distrust for S/MIME After Date");
|
|
638
|
+
if (smime) distrustAfter.emailProtection = smime;
|
|
639
|
+
var label = row[col["Common Name or Certificate Name"]].trim();
|
|
640
|
+
anchors.push(_mkAnchor(_pemCell(row[col["PEM Info"]]), {
|
|
641
|
+
distrustAfter: distrustAfter,
|
|
642
|
+
purposes: _trustBits(row[col["Trust Bits"]]),
|
|
643
|
+
label: label === "" ? null : label,
|
|
644
|
+
mozillaCaPolicy: false, // certdata-only metadata
|
|
645
|
+
}));
|
|
646
|
+
}
|
|
647
|
+
return { anchors: _dedupAnchors(anchors) };
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* @primitive pki.trust.anchor
|
|
652
|
+
* @signature pki.trust.anchor(entry, opts?) -> trustAnchor
|
|
653
|
+
* @since 0.2.0
|
|
654
|
+
* @status experimental
|
|
655
|
+
* @spec RFC 5280 sec. 6.1.1 (NSS trust-bit semantics)
|
|
656
|
+
* @related pki.trust.parseCertdata, pki.trust.parseCcadbCsv, pki.path.validate
|
|
657
|
+
*
|
|
658
|
+
* Turn a parsed trust-store entry into the `trustAnchor` object
|
|
659
|
+
* `pki.path.validate` consumes: `{ name, publicKey, algorithm, parameters,
|
|
660
|
+
* distrustAfter, purposes }` -- a straight hand-off (validate reads
|
|
661
|
+
* `distrustAfter` as a per-purpose map and `purposes` as the delegator set,
|
|
662
|
+
* selected by its own `opts.checkPurpose`). With `opts.purpose` it
|
|
663
|
+
* fail-fasts: an entry that is not a trusted delegator for that purpose
|
|
664
|
+
* throws `trust/purpose-not-trusted` at build time, so an operator wiring a
|
|
665
|
+
* store catches the wrong root before a single validation runs (the
|
|
666
|
+
* authoritative gate stays inside `validate`).
|
|
667
|
+
*
|
|
668
|
+
* @opts
|
|
669
|
+
* purpose: string // "serverAuth" | "emailProtection" | "codeSigning" -- fail-fast purpose check
|
|
670
|
+
*
|
|
671
|
+
* @example
|
|
672
|
+
* var csv = "Common Name or Certificate Name,Trust Bits," +
|
|
673
|
+
* "Distrust for TLS After Date,Distrust for S/MIME After Date,PEM Info\n" +
|
|
674
|
+
* 'Example Root,Websites,,,"' + pemText + '"';
|
|
675
|
+
* var entry = pki.trust.parseCcadbCsv(csv).anchors[0];
|
|
676
|
+
* var anchor = pki.trust.anchor(entry, { purpose: "serverAuth" });
|
|
677
|
+
* await pki.path.validate([pki.schema.x509.parse(der)], { time: new Date(), trustAnchor: anchor, checkPurpose: "serverAuth" });
|
|
678
|
+
*/
|
|
679
|
+
function anchor(entry, opts) {
|
|
680
|
+
if (!entry || typeof entry !== "object" || !Buffer.isBuffer(entry.publicKey) ||
|
|
681
|
+
typeof entry.algorithm !== "string" || !entry.name || !Array.isArray(entry.name.rdns)) {
|
|
682
|
+
throw E("trust/bad-input", "anchor expects a trust-store entry ({ name, publicKey, algorithm, ... })");
|
|
683
|
+
}
|
|
684
|
+
opts = opts || {};
|
|
685
|
+
if (opts.purpose !== undefined) {
|
|
686
|
+
if (PURPOSES.indexOf(opts.purpose) === -1) {
|
|
687
|
+
throw E("trust/bad-input", "anchor: opts.purpose must be one of " + PURPOSES.join(" | "));
|
|
688
|
+
}
|
|
689
|
+
if (!entry.purposes || entry.purposes[opts.purpose] !== true) {
|
|
690
|
+
throw E("trust/purpose-not-trusted", "this root is not a trusted delegator for " + opts.purpose);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return {
|
|
694
|
+
name: entry.name,
|
|
695
|
+
publicKey: entry.publicKey,
|
|
696
|
+
algorithm: entry.algorithm,
|
|
697
|
+
parameters: entry.parameters !== undefined ? entry.parameters : null,
|
|
698
|
+
distrustAfter: entry.distrustAfter || {},
|
|
699
|
+
purposes: entry.purposes || { serverAuth: false, emailProtection: false, codeSigning: false },
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
module.exports = {
|
|
704
|
+
parseCertdata: parseCertdata,
|
|
705
|
+
parseCcadbCsv: parseCcadbCsv,
|
|
706
|
+
anchor: anchor,
|
|
707
|
+
};
|