@blamejs/pki 0.1.22 → 0.1.24

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/lib/est.js ADDED
@@ -0,0 +1,717 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ /**
5
+ * @module pki.est
6
+ * @nav Schema
7
+ * @title EST
8
+ * @order 190
9
+ * @slug est
10
+ *
11
+ * @intro
12
+ * Enrollment over Secure Transport (RFC 7030, updated by RFC 8951 and
13
+ * RFC 9908) -- the transport-agnostic codecs, validators, and request builders
14
+ * an EST client composes over the shipped CMS / CSR / PKCS#8 / X.509 parsers.
15
+ * No socket is opened here: `transferDecode` / `transferEncode` are the RFC 8951
16
+ * sec. 3 base64 transfer codec (RFC 4648, and DELIBERATELY blind to any
17
+ * Content-Transfer-Encoding header -- errata 5904/5107); `splitMultipartMixed`
18
+ * is the /serverkeygen `multipart/mixed` splitter; `parseCertsOnly` validates a
19
+ * certs-only Simple PKI Response (RFC 5272 sec. 4.1) OVER `cms.parse` output;
20
+ * `parseServerKeygenResponse` dispatches the two-part key + certificate
21
+ * response with recipient-arm coherence; `classifyResponse` is the HTTP status
22
+ * / content-type / Retry-After state machine (202 accepted-not-ready surfaces
23
+ * `retryAfterSeconds` -- never an internal sleep; 204/404 on /csrattrs is a
24
+ * "none available" verdict, not an error). The builders assemble the CSR
25
+ * attributes EST adds -- a channel-binding challengePassword, the
26
+ * out-of-band-key identifiers, SMIMECapabilities, and the RFC 9908
27
+ * template-priority enroll plan.
28
+ *
29
+ * Altitude MATCHES the toolkit: structural validation, no crypto verdicts.
30
+ * Certificates come back RAW and UNORDERED ("Clients MUST NOT assume the
31
+ * certificates are in any order", RFC 5272 sec. 4.1) -- `findIssuedCert` picks
32
+ * the issued certificate by a public-key match, never a positional guess. The
33
+ * serverkeygen encrypted-key part's EnvelopedData is surfaced structurally
34
+ * (ciphertext raw, decryption external). /fullcmc is recognized and rejected
35
+ * with a precise `est/fullcmc-not-supported` (deferred to the CMC format
36
+ * module). DER-only where DER, fail-closed everywhere.
37
+ *
38
+ * @card
39
+ * EST (RFC 7030 / 8951 / 9908) client codecs -- base64 transfer, multipart
40
+ * splitter, certs-only + serverkeygen validators over CMS, the enroll-attribute
41
+ * builders, and the HTTP response classifier. Transport-agnostic, fail-closed.
42
+ */
43
+
44
+ var asn1 = require("./asn1-der");
45
+ var oid = require("./oid");
46
+ var constants = require("./constants");
47
+ var cms = require("./schema-cms");
48
+ var x509 = require("./schema-x509");
49
+ var crl = require("./schema-crl");
50
+ var pkcs8 = require("./schema-pkcs8");
51
+ var csr = require("./schema-csr");
52
+ var frameworkError = require("./framework-error");
53
+
54
+ var EstError = frameworkError.EstError;
55
+ function E(code, message, cause) { return new EstError(code, message, cause); }
56
+
57
+ var ID_DATA = oid.byName("data");
58
+ var ID_SIGNED_DATA = oid.byName("signedData");
59
+ var OID_CHALLENGE_PASSWORD = oid.byName("challengePassword");
60
+ var OID_DECRYPT_KEY_ID = oid.byName("decryptKeyID");
61
+ var OID_ASYMM_DECRYPT_KEY_ID = oid.byName("asymmDecryptKeyID");
62
+ var OID_SMIME_CAPABILITIES = oid.byName("smimeCapabilities");
63
+ var OID_TEMPLATE = oid.byName("certificationRequestInfoTemplate");
64
+
65
+ var OPERATIONS = ["cacerts", "simpleenroll", "simplereenroll", "fullcmc", "serverkeygen", "csrattrs"];
66
+
67
+ // The largest Retry-After delay this classifier will surface as a number: a
68
+ // generous one-year ceiling that keeps the value a safe integer and rejects an
69
+ // overflowing / nonsensical delay-seconds fail-closed rather than returning it.
70
+ var MAX_RETRY_AFTER_SECONDS = constants.TIME.days(365) / constants.TIME.seconds(1);
71
+
72
+ // The three HTTP-date forms (RFC 7231 sec. 7.1.1.1): IMF-fixdate (the required
73
+ // form), and the obsolete rfc850-date / asctime-date a recipient must still
74
+ // accept. Gating Date.parse on this grammar keeps its permissiveness (an ISO
75
+ // string like "2026-07-10") from passing as a valid Retry-After header.
76
+ var _MONTHS = "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" ");
77
+ var _MONTH = "(?:" + _MONTHS.join("|") + ")";
78
+ var HTTP_DATE_FORMS = [
79
+ new RegExp("^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), \\d{2} " + _MONTH + " \\d{4} \\d{2}:\\d{2}:\\d{2} GMT$"),
80
+ new RegExp("^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), \\d{2}-" + _MONTH + "-\\d{2} \\d{2}:\\d{2}:\\d{2} GMT$"),
81
+ new RegExp("^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) " + _MONTH + " [ \\d]\\d \\d{2}:\\d{2}:\\d{2} \\d{4}$"),
82
+ ];
83
+
84
+ // Parse an HTTP-date to epoch ms, or NaN when its grammar, calendar, or time is
85
+ // invalid. Built from the extracted fields via Date.UTC (never Date.parse): every
86
+ // HTTP-date is UTC -- the asctime form carries no GMT token, so delegating would
87
+ // parse it in LOCAL time -- and the round-trip check rejects an impossible date /
88
+ // time (Date.UTC normalizes Feb 31 -> Mar 2 just as Date.parse would).
89
+ function _httpDateMs(s) {
90
+ if (!HTTP_DATE_FORMS.some(function (re) { return re.test(s); })) return NaN;
91
+ var day, monName, year;
92
+ var asc = /^\w{3} (\w{3}) ([ \d]\d) /.exec(s); // asctime: month then day
93
+ if (asc) { monName = asc[1]; day = parseInt(asc[2], 10); year = parseInt(/(\d{4})$/.exec(s)[1], 10); }
94
+ else {
95
+ var dmy = /(\d{2})[ -](\w{3})[ -](\d{2,4})/.exec(s); // IMF / rfc850: day month year
96
+ if (!dmy) return NaN;
97
+ day = parseInt(dmy[1], 10); monName = dmy[2]; year = parseInt(dmy[3], 10);
98
+ if (year < 100) year += (year < 70 ? 2000 : 1900); // rfc850 two-digit year
99
+ }
100
+ var t = /(\d{2}):(\d{2}):(\d{2})/.exec(s);
101
+ var mon = _MONTHS.indexOf(monName);
102
+ if (!t || mon < 0) return NaN;
103
+ var hh = parseInt(t[1], 10), mi = parseInt(t[2], 10), ss = parseInt(t[3], 10);
104
+ var when = Date.UTC(year, mon, day, hh, mi, ss);
105
+ var d = new Date(when);
106
+ if (d.getUTCDate() !== day || d.getUTCMonth() !== mon || d.getUTCFullYear() !== year ||
107
+ d.getUTCHours() !== hh || d.getUTCMinutes() !== mi || d.getUTCSeconds() !== ss) return NaN;
108
+ return when;
109
+ }
110
+
111
+ // ---- the RFC 8951 sec. 3/3.1 transfer codec (CTE-header-blind) -----------
112
+
113
+ /**
114
+ * @primitive pki.est.transferDecode
115
+ * @signature pki.est.transferDecode(body) -> Buffer
116
+ * @since 0.1.24
117
+ * @status experimental
118
+ * @spec RFC 8951, RFC 4648
119
+ * @related pki.est.transferEncode
120
+ *
121
+ * Decode an EST payload body (a base64 string or Buffer) to DER. CR/LF/space/tab
122
+ * are stripped anywhere (RFC 8951 sec. 3.1); any other non-alphabet byte fails
123
+ * closed with `est/bad-base64`. A Content-Transfer-Encoding header is NEVER read
124
+ * (errata 5904/5107). Bounded twice -- the raw length before decode and the
125
+ * decoded DER against `DER_MAX_BYTES` (`est/too-large`).
126
+ *
127
+ * @example
128
+ * var roundTripped = pki.est.transferDecode(pki.est.transferEncode(der));
129
+ */
130
+ function transferDecode(body) {
131
+ var s = Buffer.isBuffer(body) ? body.toString("latin1") : String(body);
132
+ // The pre-decode ceiling: the largest base64 that could yield a DER_MAX_BYTES
133
+ // document, plus a generous line-wrapping allowance (CRLF at 64/76-char lines is
134
+ // ~3%; 1/8 leaves ample margin) so a normally-wrapped near-limit body is not
135
+ // rejected before the real DER_MAX_BYTES limit is enforced on the decode below.
136
+ var b64Len = Math.ceil(constants.LIMITS.DER_MAX_BYTES * 4 / 3);
137
+ var cap = b64Len + Math.ceil(b64Len / 8) + constants.BYTES.kib(64);
138
+ if (s.length > cap) throw E("est/too-large", "the EST payload exceeds the transfer size cap");
139
+ var stripped = s.replace(/[\r\n \t]/g, "");
140
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(stripped)) throw E("est/bad-base64", "the EST payload is not RFC 4648 base64 (RFC 8951 sec. 3.1)");
141
+ var der = Buffer.from(stripped, "base64");
142
+ // Buffer.from silently truncates a non-canonical body (a length not a multiple
143
+ // of 4, e.g. "A", or non-zero trailing bits). Re-encode and require an exact
144
+ // round-trip so a malformed payload fails closed instead of decoding to a
145
+ // different, shorter DER (the PEM decoder applies the same canonical check).
146
+ if (der.toString("base64") !== stripped) throw E("est/bad-base64", "the EST payload is not canonical RFC 4648 base64 (RFC 8951 sec. 3.1)");
147
+ if (der.length > constants.LIMITS.DER_MAX_BYTES) throw E("est/too-large", "the decoded EST DER exceeds the size cap");
148
+ return der;
149
+ }
150
+
151
+ /**
152
+ * @primitive pki.est.transferEncode
153
+ * @signature pki.est.transferEncode(der) -> string
154
+ * @since 0.1.24
155
+ * @status experimental
156
+ * @spec RFC 8951, RFC 4648
157
+ * @related pki.est.transferDecode
158
+ *
159
+ * Encode DER as an EST payload body: bare RFC 4648 base64, no line wrapping
160
+ * (senders need not insert whitespace, RFC 8951 sec. 3.1).
161
+ *
162
+ * @example
163
+ * var body = pki.est.transferEncode(der);
164
+ */
165
+ function transferEncode(der) {
166
+ if (!Buffer.isBuffer(der)) throw E("est/bad-input", "transferEncode requires a DER Buffer");
167
+ return der.toString("base64");
168
+ }
169
+
170
+ // ---- the multipart/mixed splitter (/serverkeygen) -----------------------
171
+
172
+ // Extract the boundary from a `multipart/mixed; boundary=...` content-type,
173
+ // tolerating whitespace before the semicolon (erratum 5779 REJECTED the ban).
174
+ function _multipartBoundary(contentType) {
175
+ var ct = String(contentType || "");
176
+ if (!/^multipart\/mixed\s*(;|$)/i.test(ct)) return null;
177
+ var m = /;\s*boundary\s*=\s*("([^"]+)"|([^;\s]+))/i.exec(ct);
178
+ return m ? (m[2] !== undefined ? m[2] : m[3]) : null;
179
+ }
180
+
181
+ // Split a multipart/mixed body into its parts, each { headers, contentType,
182
+ // body }. A boundary delimiter is `--boundary` only at the START OF A LINE (body
183
+ // start or after CRLF), optional transport-padding, then CRLF (a part) or `--`
184
+ // (the close delimiter) -- matching the raw substring would treat `--boundaryX`,
185
+ // which is NOT a delimiter (RFC 2046), as one. The preamble/epilogue are ignored.
186
+ function splitMultipartMixed(body, contentType) {
187
+ var boundary = _multipartBoundary(contentType);
188
+ if (!boundary) throw E("est/bad-multipart", "a serverkeygen response must be multipart/mixed with a boundary (RFC 7030 sec. 4.4.2)");
189
+ var text = Buffer.isBuffer(body) ? body.toString("latin1") : String(body);
190
+ if (text.length > constants.LIMITS.DER_MAX_BYTES * 2) throw E("est/too-large", "the multipart body exceeds the size cap");
191
+ var esc = boundary.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
192
+ var delimRe = new RegExp("(?:^|\\r?\\n)--" + esc + "(--)?[ \\t]*(?:\\r?\\n|$)", "g");
193
+ var marks = [], m;
194
+ while ((m = delimRe.exec(text)) !== null) {
195
+ marks.push({ at: m.index, bodyStart: delimRe.lastIndex, close: m[1] === "--" });
196
+ if (delimRe.lastIndex === m.index) delimRe.lastIndex += 1; // guard a zero-length match
197
+ }
198
+ var closeAt = -1;
199
+ for (var c = 0; c < marks.length; c++) { if (marks[c].close) { closeAt = c; break; } }
200
+ if (closeAt === -1) throw E("est/bad-multipart", "the multipart body is missing its terminal boundary (RFC 2046)");
201
+ var parts = [];
202
+ for (var i = 0; i < closeAt; i++) {
203
+ var seg = text.slice(marks[i].bodyStart, marks[i + 1].at); // between this delimiter and the next
204
+ var sep = seg.indexOf("\r\n\r\n");
205
+ if (sep === -1) sep = seg.indexOf("\n\n");
206
+ if (sep === -1) throw E("est/bad-multipart", "a multipart part is missing its header/body separator");
207
+ var rawHeaders = seg.slice(0, sep);
208
+ var partBody = seg.slice(sep).replace(/^(\r?\n){2}/, "").replace(/\r?\n$/, "");
209
+ var headers = {};
210
+ rawHeaders.split(/\r?\n/).forEach(function (line) {
211
+ var col = line.indexOf(":");
212
+ if (col > 0) headers[line.slice(0, col).trim().toLowerCase()] = line.slice(col + 1).trim();
213
+ });
214
+ var partCt = headers["content-type"] || "";
215
+ if (/^multipart\//i.test(partCt)) throw E("est/bad-multipart", "a nested multipart part is not permitted");
216
+ parts.push({ headers: headers, contentType: partCt, body: partBody });
217
+ }
218
+ return parts;
219
+ }
220
+
221
+ // ---- the certs-only Simple PKI Response validator -----------------------
222
+
223
+ /**
224
+ * @primitive pki.est.parseCertsOnly
225
+ * @signature pki.est.parseCertsOnly(der) -> { certificates, crls }
226
+ * @since 0.1.24
227
+ * @status experimental
228
+ * @spec RFC 7030, RFC 5272, RFC 5652
229
+ * @related pki.est.findIssuedCert, pki.schema.cms.parse
230
+ *
231
+ * Validate a certs-only CMS Simple PKI Response (RFC 5272 sec. 4.1) over the
232
+ * shipped `cms.parse` output: a SignedData with no eContent and EMPTY
233
+ * signerInfos, carrying at least one plain X.509 certificate (a context-tagged
234
+ * CertificateChoices alternative is rejected `est/bad-certificate-choice`). CRLs
235
+ * MAY be present. Certificates come back RAW and in AS-RECEIVED order (never
236
+ * sorted -- RFC 5272 sec. 4.1). A non-conformant response throws a typed
237
+ * `EstError` (`est/not-certs-only`, `est/no-certificates`).
238
+ *
239
+ * @example
240
+ * var r = pki.est.parseCertsOnly(caCertsDer);
241
+ * r.certificates; // -> [Buffer, ...] raw, unordered
242
+ */
243
+ function parseCertsOnly(der) {
244
+ var r;
245
+ try { r = cms.parse(der); }
246
+ catch (e) { if (e instanceof EstError) throw e; throw E("est/bad-response", "the EST response did not decode as CMS: " + ((e && e.message) || String(e)), e); }
247
+ if (r.contentTypeName !== "signedData") throw E("est/not-certs-only", "an EST certs-only response must be a CMS SignedData (RFC 5272 sec. 4.1)");
248
+ if (r.encapContentInfo.eContentType !== ID_DATA || r.encapContentInfo.eContent !== null) {
249
+ throw E("est/not-certs-only", "a certs-only Simple PKI Response must carry id-data with no eContent (RFC 5272 sec. 4.1)");
250
+ }
251
+ if (r.signerInfos.length !== 0) throw E("est/not-certs-only", "a certs-only Simple PKI Response must have empty signerInfos (RFC 5272 sec. 4.1)");
252
+ if (!r.certificates || r.certificates.length === 0) throw E("est/no-certificates", "an EST certs-only response must contain at least one certificate (RFC 7030 sec. 4.1.3)");
253
+ for (var i = 0; i < r.certificates.length; i++) {
254
+ if (r.certificates[i].tagClass !== "universal") throw E("est/bad-certificate-choice", "EST exchanges plain X.509 certificates; a tagged CertificateChoices alternative is not permitted (RFC 7030)");
255
+ // A universal-SEQUENCE CertificateChoice must be a well-formed X.509
256
+ // Certificate, not merely any SEQUENCE. Parse it structurally (still
257
+ // returning the raw bytes below) so a malformed response fails closed.
258
+ try { x509.parse(r.certificates[i].bytes); }
259
+ catch (e) { if (e instanceof EstError) throw e; throw E("est/bad-certificate", "a certs-only response carried a non-certificate in its certificates field (RFC 5272 sec. 4.1)", e); }
260
+ }
261
+ var crls = r.crls || [];
262
+ for (var j = 0; j < crls.length; j++) {
263
+ // A RevocationInfoChoice is a plain X.509 CertificateList or a [1] otherRevInfo;
264
+ // EST surfaces CRLs, so reject the tagged alternative and structurally validate
265
+ // each universal entry as a CertificateList (mirrors the certificate path).
266
+ if (crls[j].tagClass !== "universal") throw E("est/bad-crl", "an EST response CRL must be a plain X.509 CertificateList, not a tagged otherRevInfo alternative (RFC 5652 sec. 10.2.1)");
267
+ try { crl.parse(crls[j].bytes); }
268
+ catch (e) { if (e instanceof EstError) throw e; throw E("est/bad-crl", "a response carried a non-CRL in its crls field", e); }
269
+ }
270
+ return {
271
+ certificates: r.certificates.map(function (c) { return c.bytes; }),
272
+ crls: crls.map(function (c) { return c.bytes; }),
273
+ };
274
+ }
275
+
276
+ // Pick the issued certificate from a certs-only response by matching its public
277
+ // key against the CSR / SPKI the client submitted -- the ONLY sanctioned
278
+ // identification (positional guessing is forbidden, RFC 5272 sec. 4.1). `target`
279
+ // is an SPKI object (with a `bytes` subjectPublicKey field) or a raw Buffer.
280
+ function findIssuedCert(certs, target) {
281
+ var want = Buffer.isBuffer(target) ? target : (target && target.bytes);
282
+ if (!Buffer.isBuffer(want)) return null;
283
+ for (var i = 0; i < certs.length; i++) {
284
+ var spki;
285
+ try { spki = x509.parse(certs[i]).subjectPublicKeyInfo; }
286
+ catch (_e) { continue; }
287
+ if (spki && Buffer.isBuffer(spki.bytes) && spki.bytes.equals(want)) return certs[i];
288
+ }
289
+ return null;
290
+ }
291
+
292
+ // ---- /serverkeygen ------------------------------------------------------
293
+
294
+ // Collect every key-identifier (subjectKeyIdentifier / KEKIdentifier) a set of
295
+ // RecipientInfos names, across the ktri / kari / kekri / kemri arms -- the
296
+ // byte identifiers a client's advertised decryptKeyID would match.
297
+ function _recipientKeyIds(recipientInfos) {
298
+ var ids = [];
299
+ function push(v) { if (Buffer.isBuffer(v)) ids.push(v); }
300
+ (recipientInfos || []).forEach(function (r) {
301
+ if (r.rid) push(r.rid.subjectKeyIdentifier);
302
+ if (r.kemri && r.kemri.rid) push(r.kemri.rid.subjectKeyIdentifier); // KEMRecipientInfo (RFC 9629, under the ori arm)
303
+ if (r.kekid) push(r.kekid.keyIdentifier);
304
+ (r.recipientEncryptedKeys || []).forEach(function (rek) { if (rek.rid) push(rek.rid.subjectKeyIdentifier); });
305
+ });
306
+ return ids;
307
+ }
308
+
309
+ // The { issuer (raw DN bytes), serialNumber } every issuerAndSerialNumber recipient
310
+ // arm names -- the form a server MAY use after mapping an AsymmetricDecryptKeyIdentifier
311
+ // to a certificate (RFC 7030 sec. 4.4.2), so a key-id-only match would miss it.
312
+ function _recipientIssuerSerials(recipientInfos) {
313
+ var out = [];
314
+ function push(rid) { if (rid && rid.issuer && Buffer.isBuffer(rid.issuer.bytes) && rid.serialNumber != null) out.push({ issuer: rid.issuer.bytes, serialNumber: rid.serialNumber }); }
315
+ (recipientInfos || []).forEach(function (r) {
316
+ push(r.rid);
317
+ if (r.kemri) push(r.kemri.rid);
318
+ (r.recipientEncryptedKeys || []).forEach(function (rek) { push(rek.rid); });
319
+ });
320
+ return out;
321
+ }
322
+
323
+ // The advertised recipient (a decryptKeyID / asymmDecryptKeyID key identifier
324
+ // and/or an issuer+serial) matches SOME RecipientInfo arm -- either identifier
325
+ // form the server may have used to name the same requested key.
326
+ function _recipientMatches(recipientInfos, opts) {
327
+ if (Buffer.isBuffer(opts.expectedRecipientKeyId) &&
328
+ _recipientKeyIds(recipientInfos).some(function (id) { return id.equals(opts.expectedRecipientKeyId); })) return true;
329
+ var ias = opts.expectedRecipientIssuerSerial;
330
+ if (ias && Buffer.isBuffer(ias.issuer) && ias.serialNumber != null) {
331
+ var want = typeof ias.serialNumber === "bigint" ? ias.serialNumber : BigInt(ias.serialNumber);
332
+ if (_recipientIssuerSerials(recipientInfos).some(function (r) { return r.issuer.equals(ias.issuer) && r.serialNumber === want; })) return true;
333
+ }
334
+ return false;
335
+ }
336
+
337
+ // Parse a /serverkeygen response: exactly two parts -- a private-key part
338
+ // (application/pkcs8 cleartext PrivateKeyInfo, or application/pkcs7-mime;
339
+ // smime-type=server-generated-key EnvelopedData) and a certificate part (the
340
+ // enroll-response shape). opts.requestedEncryption asserts the key part is
341
+ // encrypted (a cleartext part -> est/expected-encrypted-key). The advertised
342
+ // recipient may be given as opts.expectedRecipientKeyId (a Buffer, the
343
+ // decryptKeyID / asymmDecryptKeyID) and/or opts.expectedRecipientIssuerSerial
344
+ // ({ issuer: raw-DN Buffer, serialNumber }, the form a server MAY use after
345
+ // mapping that identifier to a certificate); when either is given, a RecipientInfo
346
+ // of the encrypted key must match one of them (else est/recipient-mismatch) -- so
347
+ // a response encrypted to a DIFFERENT recipient fails closed rather than passing.
348
+ // Split a content-type on `;` that are OUTSIDE a quoted-string (RFC 2045),
349
+ // so a `;` inside a parameter value does not create a spurious parameter.
350
+ function _splitContentTypeParams(s) {
351
+ var segs = [], cur = "", inQuote = false;
352
+ for (var i = 0; i < s.length; i++) {
353
+ var ch = s[i];
354
+ if (ch === '"') { inQuote = !inQuote; cur += ch; }
355
+ else if (ch === ";" && !inQuote) { segs.push(cur); cur = ""; }
356
+ else cur += ch;
357
+ }
358
+ segs.push(cur);
359
+ return segs;
360
+ }
361
+
362
+ // Split a part's content-type into its base media type (lowercased) and its
363
+ // smime-type parameter. The media type must be the EXACT token (a look-alike
364
+ // like application/pkcs8evil yields that literal, not application/pkcs8), and
365
+ // parameters are read token-by-token honoring quoted-strings so a smime-type-like
366
+ // substring inside another quoted parameter value is NOT taken as smime-type.
367
+ function _partMediaType(contentType) {
368
+ var segs = _splitContentTypeParams(String(contentType || ""));
369
+ var mediaMatch = /^\s*([a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*)\s*$/i.exec(segs[0]);
370
+ var smimeType = null;
371
+ for (var i = 1; i < segs.length; i++) {
372
+ var eq = segs[i].indexOf("=");
373
+ if (eq === -1) continue;
374
+ if (segs[i].slice(0, eq).trim().toLowerCase() !== "smime-type") continue;
375
+ var val = segs[i].slice(eq + 1).trim();
376
+ if (val.length >= 2 && val.charAt(0) === '"' && val.charAt(val.length - 1) === '"') val = val.slice(1, -1);
377
+ smimeType = val.toLowerCase();
378
+ break;
379
+ }
380
+ return { media: mediaMatch ? mediaMatch[1].toLowerCase() : null, smimeType: smimeType };
381
+ }
382
+
383
+ function parseServerKeygenResponse(body, contentType, opts) {
384
+ opts = opts || {};
385
+ var parts = splitMultipartMixed(body, contentType);
386
+ if (parts.length !== 2) throw E("est/bad-multipart", "a serverkeygen response must have exactly two parts (RFC 7030 sec. 4.4.2)");
387
+ var keyPart = null, certPart = null, encrypted = false;
388
+ for (var i = 0; i < parts.length; i++) {
389
+ var pt = _partMediaType(parts[i].contentType);
390
+ if (pt.media === "application/pkcs8") { keyPart = parts[i]; encrypted = false; }
391
+ else if (pt.media === "application/pkcs7-mime" && pt.smimeType === "server-generated-key") { keyPart = parts[i]; encrypted = true; }
392
+ // The certificate part exactly matches the /simpleenroll response
393
+ // (RFC 7030 sec. 4.4.2), so it too MUST be smime-type=certs-only.
394
+ else if (pt.media === "application/pkcs7-mime" && pt.smimeType === "certs-only") certPart = parts[i];
395
+ else throw E("est/bad-multipart", "unrecognized serverkeygen part content-type " + JSON.stringify(parts[i].contentType));
396
+ }
397
+ if (!keyPart || !certPart) throw E("est/bad-multipart", "a serverkeygen response needs one key part and one certificate part");
398
+ if (opts.requestedEncryption && !encrypted) throw E("est/expected-encrypted-key", "encryption was requested but the private-key part is cleartext (RFC 7030 sec. 4.4.2)");
399
+ var out = { certificates: parseCertsOnly(transferDecode(certPart.body)).certificates };
400
+ if (encrypted) {
401
+ // The server-generated key MUST be a CMS EnvelopedData (RFC 7030 sec. 4.4.2);
402
+ // a SignedData or any other CMS content type under the encrypted label is a
403
+ // structurally invalid key part, not a success. The EnvelopedData is surfaced
404
+ // structurally -- the ciphertext stays opaque and the RecipientInfo arms feed
405
+ // the coherence check the caller makes.
406
+ var parsedKey = cms.parse(transferDecode(keyPart.body));
407
+ if (parsedKey.contentTypeName !== "envelopedData") throw E("est/bad-key-part", "a server-generated encrypted key part must be a CMS EnvelopedData (RFC 7030 sec. 4.4.2), got " + JSON.stringify(parsedKey.contentTypeName));
408
+ // The EnvelopedData MUST encapsulate a CMS SignedData holding the private key
409
+ // (RFC 7030 sec. 4.4.2) -- validate the inner encryptedContentInfo content type,
410
+ // not just the outer ContentInfo, so a key wrapped around id-data or anything
411
+ // else fails closed before the caller decrypts it.
412
+ if (!parsedKey.encryptedContentInfo || parsedKey.encryptedContentInfo.contentType !== ID_SIGNED_DATA) throw E("est/bad-key-part", "a server-generated encrypted key's EnvelopedData must encapsulate a CMS SignedData (RFC 7030 sec. 4.4.2)");
413
+ // A detached EnvelopedData (encryptedContent omitted) carries no ciphertext to
414
+ // decrypt -- there is no key to recover, so it is not a valid key response.
415
+ if (parsedKey.encryptedContentInfo.encryptedContent === null) throw E("est/bad-key-part", "a server-generated encrypted key's EnvelopedData must carry its encrypted content, not be detached (RFC 7030 sec. 4.4.2)");
416
+ if (Buffer.isBuffer(opts.expectedRecipientKeyId) || opts.expectedRecipientIssuerSerial) {
417
+ if (!_recipientMatches(parsedKey.recipientInfos, opts)) throw E("est/recipient-mismatch", "the server-generated key is not encrypted to the advertised recipient (RFC 7030 sec. 4.4.2)");
418
+ }
419
+ out.encryptedKey = parsedKey;
420
+ } else {
421
+ out.privateKey = pkcs8.parse(transferDecode(keyPart.body));
422
+ }
423
+ return out;
424
+ }
425
+
426
+ // ---- the HTTP response classifier --------------------------------------
427
+
428
+ // The required 200-response content-type per operation: the EXACT media-type
429
+ // token plus, where the RFC mandates it, the smime-type parameter. simpleenroll /
430
+ // simplereenroll require smime-type=certs-only (RFC 7030 sec. 4.2.3) so a different
431
+ // PKI message type (CMC-response, ...) is not accepted; cacerts mandates only the
432
+ // media type (sec. 4.1.3). Matched token-wise, not by prefix, so a look-alike like
433
+ // "application/pkcs7-mimeevil" is rejected.
434
+ var CONTENT_TYPE_BY_OP = {
435
+ cacerts: { media: "application/pkcs7-mime" },
436
+ simpleenroll: { media: "application/pkcs7-mime", smimeType: "certs-only" },
437
+ simplereenroll: { media: "application/pkcs7-mime", smimeType: "certs-only" },
438
+ serverkeygen: { media: "multipart/mixed" },
439
+ csrattrs: { media: "application/csrattrs" },
440
+ };
441
+
442
+ // The operations whose responses this client classifies. /fullcmc is a real EST
443
+ // path (paths() emits its URL) but its CMC response is deferred to the CMC
444
+ // module, so classifying one is an explicit unsupported-operation fault rather
445
+ // than a silently-accepted 200 whose content-type went unchecked.
446
+ var CLASSIFIABLE_OPS = ["cacerts", "simpleenroll", "simplereenroll", "serverkeygen", "csrattrs"];
447
+
448
+ /**
449
+ * @primitive pki.est.classifyResponse
450
+ * @signature pki.est.classifyResponse(status, headers, body, opts?) -> verdict
451
+ * @since 0.1.24
452
+ * @status experimental
453
+ * @spec RFC 7030, RFC 8951
454
+ * @related pki.est.paths, pki.est.parseCertsOnly
455
+ *
456
+ * Classify an EST HTTP response into a verdict or a typed fault. A 200 requires
457
+ * the operation's exact content-type (`est/bad-content-type`); a 202 requires a
458
+ * Retry-After (absent -> `est/missing-retry-after`) -- a delay-seconds value is
459
+ * surfaced as bounded `retryAfterSeconds`, an HTTP-date as absolute
460
+ * `retryAfterDate` (epoch ms; `retryAfterSeconds` too when `opts.now` is given),
461
+ * and any other value is `est/bad-retry-after` (never slept on either way);
462
+ * 204/404 on `/csrattrs` is a `none-available` verdict (an error on any other
463
+ * operation); 4xx/5xx surface the capped diagnostic on `est/http-error`.
464
+ *
465
+ * @opts
466
+ * op: string // the EST operation this response answers
467
+ * now: number // the response receipt time (epoch ms), to turn an HTTP-date Retry-After into retryAfterSeconds
468
+ *
469
+ * @example
470
+ * var v = pki.est.classifyResponse(202, { "retry-after": "120" }, "", { op: "simpleenroll" });
471
+ * v.retryAfterSeconds; // -> 120
472
+ */
473
+ function classifyResponse(status, headers, body, opts) {
474
+ opts = opts || {};
475
+ var op = opts.op;
476
+ // Fail closed on an operation whose response this client cannot validate:
477
+ // /fullcmc is recognized-but-deferred, and any other named op is a typo. An
478
+ // absent op is the caller opting out of the content-type gate (generic
479
+ // status handling) and stays permissive.
480
+ if (op === "fullcmc") throw E("est/fullcmc-not-supported", "the /fullcmc operation is recognized but its CMC response is not supported by this client (RFC 7030 sec. 4.3)");
481
+ if (op !== undefined && op !== null && CLASSIFIABLE_OPS.indexOf(op) === -1) throw E("est/unsupported-operation", "unrecognized EST operation " + JSON.stringify(op));
482
+ var h = {};
483
+ Object.keys(headers || {}).forEach(function (k) { h[k.toLowerCase()] = headers[k]; });
484
+ if (status === 200) {
485
+ var spec = CONTENT_TYPE_BY_OP[op];
486
+ var ct = h["content-type"] || "";
487
+ if (spec) {
488
+ var pt = _partMediaType(ct);
489
+ if (pt.media !== spec.media || (spec.smimeType && pt.smimeType !== spec.smimeType)) {
490
+ throw E("est/bad-content-type", "a 200 " + op + " response must carry content-type " + spec.media + (spec.smimeType ? "; smime-type=" + spec.smimeType : "") + ", got " + JSON.stringify(ct));
491
+ }
492
+ }
493
+ return { status: "ok", contentType: ct };
494
+ }
495
+ if (status === 202) {
496
+ var ra = h["retry-after"];
497
+ if (ra === undefined || ra === null || String(ra).trim() === "") throw E("est/missing-retry-after", "an HTTP 202 EST response must include Retry-After (RFC 8951 sec. 3.3)");
498
+ var raStr = String(ra).trim();
499
+ // Retry-After is delay-seconds OR an HTTP-date (RFC 7231 sec. 7.1.3). A
500
+ // delay-seconds becomes retryAfterSeconds directly; an HTTP-date is surfaced
501
+ // as an absolute retryAfterDate (epoch ms) -- and, when the caller passes its
502
+ // receipt time as opts.now (epoch ms), also as a bounded retryAfterSeconds.
503
+ // Neither form -> fail closed rather than a retry verdict with no delay.
504
+ var out202 = { status: "retry", retryAfter: raStr, retryAfterSeconds: null, retryAfterDate: null };
505
+ if (/^\d+$/.test(raStr)) {
506
+ var n = parseInt(raStr, 10);
507
+ if (!Number.isSafeInteger(n) || n > MAX_RETRY_AFTER_SECONDS) throw E("est/bad-retry-after", "the 202 Retry-After delay is out of the supported range (0.." + MAX_RETRY_AFTER_SECONDS + " seconds)");
508
+ out202.retryAfterSeconds = n;
509
+ } else {
510
+ var when = _httpDateMs(raStr);
511
+ if (isNaN(when)) throw E("est/bad-retry-after", "a 202 Retry-After must be delay-seconds or a valid HTTP-date (RFC 7231 sec. 7.1.1.1/7.1.3), got " + JSON.stringify(raStr));
512
+ out202.retryAfterDate = when;
513
+ if (typeof opts.now === "number" && isFinite(opts.now)) {
514
+ var d = Math.max(0, Math.round((when - opts.now) / constants.TIME.seconds(1)));
515
+ // The same one-year ceiling the delay-seconds form enforces (a date far in
516
+ // the future would otherwise surface a huge numeric delay).
517
+ if (d > MAX_RETRY_AFTER_SECONDS) throw E("est/bad-retry-after", "the 202 Retry-After date is beyond the supported horizon (" + MAX_RETRY_AFTER_SECONDS + " seconds)");
518
+ out202.retryAfterSeconds = d;
519
+ }
520
+ }
521
+ return out202;
522
+ }
523
+ if (status === 204 || status === 404) {
524
+ if (op === "csrattrs") return { status: "none-available" };
525
+ throw E("est/http-error", "HTTP " + status + " is not a valid " + op + " response");
526
+ }
527
+ if (status >= 300 && status < 400) return { status: "redirect", location: h["location"] || null };
528
+ if (status >= 400) {
529
+ var text = Buffer.isBuffer(body) ? body.toString("utf8") : String(body || "");
530
+ throw E("est/http-error", "EST server returned HTTP " + status + (text ? ": " + text.slice(0, 512) : ""));
531
+ }
532
+ return { status: "unexpected", httpStatus: status };
533
+ }
534
+
535
+ // ---- operation-path builder ---------------------------------------------
536
+
537
+ /**
538
+ * @primitive pki.est.paths
539
+ * @signature pki.est.paths(baseUrl, opts?) -> { cacerts, simpleenroll, ... }
540
+ * @since 0.1.24
541
+ * @status experimental
542
+ * @spec RFC 7030
543
+ * @related pki.est.classifyResponse
544
+ *
545
+ * Build the RFC 7030 sec. 3.2.2 operation URLs for a base server URL. An OPTIONAL
546
+ * CA label (`opts.label`) MUST be non-empty, carry no `/`, and not collide with
547
+ * an operation name, else `est/bad-label`.
548
+ *
549
+ * @opts
550
+ * label: string // an OPTIONAL CA label path segment
551
+ *
552
+ * @example
553
+ * pki.est.paths("https://ca.example").cacerts;
554
+ * // -> "https://ca.example/.well-known/est/cacerts"
555
+ */
556
+ function paths(baseUrl, opts) {
557
+ opts = opts || {};
558
+ var prefix = String(baseUrl).replace(/\/+$/, "") + "/.well-known/est";
559
+ if (opts.label != null) {
560
+ var label = String(opts.label);
561
+ // A label is ONE URL path segment: unreserved characters only (RFC 3986),
562
+ // never a dot-segment or an operation name. Rejecting rather than
563
+ // percent-encoding keeps a reserved char (`/` `?` `#` `%`) or `..` from
564
+ // silently retargeting the request to a different resource.
565
+ if (label === "" || label === "." || label === ".." || !/^[A-Za-z0-9._~-]+$/.test(label) || OPERATIONS.indexOf(label) !== -1) {
566
+ throw E("est/bad-label", "an EST CA label must be a single path segment of unreserved characters, not '.' / '..' or an operation name (RFC 7030 sec. 3.2.2)");
567
+ }
568
+ prefix += "/" + label;
569
+ }
570
+ var out = {};
571
+ OPERATIONS.forEach(function (op) { out[op] = prefix + "/" + op; });
572
+ return out;
573
+ }
574
+
575
+ // ---- builders: the CSR attributes EST adds ------------------------------
576
+
577
+ function _attr(typeOid, valueNodes) { return asn1.build.sequence([asn1.build.oid(typeOid), asn1.build.set(valueNodes)]); }
578
+
579
+ // the RFC 5929 tls-unique channel-binding bytes -> a challengePassword
580
+ // attribute whose value is their RFC 4648 base64 (SIZE 1..255). The builder
581
+ // takes caller-supplied binding bytes and never fakes one.
582
+ function challengePasswordFromTlsUnique(channelBinding) {
583
+ if (!Buffer.isBuffer(channelBinding) || channelBinding.length === 0) throw E("est/bad-input", "challengePasswordFromTlsUnique requires the channel-binding bytes");
584
+ var b64 = channelBinding.toString("base64");
585
+ if (b64.length > 255) throw E("est/tls-unique-too-long", "the base64 tls-unique value exceeds 255 octets (RFC 7030 sec. 3.5)");
586
+ return _attr(OID_CHALLENGE_PASSWORD, [asn1.build.printable(b64)]);
587
+ }
588
+
589
+ // the out-of-band key-encryption-key identifiers (OCTET STRING values).
590
+ function decryptKeyIdentifierAttr(keyId) {
591
+ if (!Buffer.isBuffer(keyId)) throw E("est/bad-input", "decryptKeyIdentifierAttr requires the key-identifier bytes");
592
+ return _attr(OID_DECRYPT_KEY_ID, [asn1.build.octetString(keyId)]);
593
+ }
594
+ function asymmetricDecryptKeyIdentifierAttr(keyId) {
595
+ if (!Buffer.isBuffer(keyId)) throw E("est/bad-input", "asymmetricDecryptKeyIdentifierAttr requires the key-identifier bytes");
596
+ return _attr(OID_ASYMM_DECRYPT_KEY_ID, [asn1.build.octetString(keyId)]);
597
+ }
598
+ // SMIMECapabilities ::= SEQUENCE OF SMIMECapability { capabilityID OID,
599
+ // parameters ANY OPTIONAL }.
600
+ function smimeCapabilitiesAttr(capabilities) {
601
+ if (!Array.isArray(capabilities) || capabilities.length === 0) throw E("est/bad-input", "smimeCapabilitiesAttr requires a non-empty capability list");
602
+ var caps = capabilities.map(function (c) {
603
+ var seq = [asn1.build.oid(c.capabilityID)];
604
+ if (c.parameters !== undefined && c.parameters !== null) seq.push(Buffer.isBuffer(c.parameters) ? c.parameters : asn1.build.oid(c.parameters));
605
+ return asn1.build.sequence(seq);
606
+ });
607
+ return _attr(OID_SMIME_CAPABILITIES, [asn1.build.sequence(caps)]);
608
+ }
609
+
610
+ // Derive the enroll-request attribute plan from a parsed CsrAttrs.
611
+ // When a template is present the plan derives from the TEMPLATE ONLY and ignores
612
+ // every other element (RFC 9908 sec. 4). The challengePassword OID is an
613
+ // INSTRUCTION ("include tls-unique") -> a channelBindingRequired flag, never a
614
+ // password value (the RFC 7030 example that echoed it is "NOT CORRECT",
615
+ // RFC 9908 sec. 4). Every item the plan does not model into a specific field --
616
+ // including a REGISTERED-but-unmodeled instruction like a bare signature-algorithm
617
+ // OID (RFC 8951) -- is surfaced on `unhandled` ({ kind, oid, name }) so the client
618
+ // never silently drops a server requirement.
619
+ function buildEnrollAttributes(csrattrsParsed) {
620
+ var items = (csrattrsParsed && csrattrsParsed.items) || [];
621
+ var template = null;
622
+ for (var i = 0; i < items.length; i++) {
623
+ if (items[i].kind === "attribute" && items[i].oid === OID_TEMPLATE) { template = items[i].template; break; }
624
+ }
625
+ if (template) return { fromTemplate: true, template: template, channelBindingRequired: false, unhandled: [] };
626
+ var plan = { fromTemplate: false, channelBindingRequired: false, keyType: null, extensions: null, unhandled: [] };
627
+ for (var j = 0; j < items.length; j++) {
628
+ var it = items[j];
629
+ if (it.oid === OID_CHALLENGE_PASSWORD) plan.channelBindingRequired = true;
630
+ else if (it.kind === "attribute" && it.extensions) plan.extensions = it.extensions;
631
+ else if (it.kind === "attribute" && it.isKeyType) {
632
+ // EVERY key type the parser accepts is a key-type constraint (RSA / EC /
633
+ // Ed25519 / ML-DSA / ...). The non-template form carries EXACTLY ONE key-type
634
+ // attribute (RFC 9908 sec. 3.2); a second is an ambiguous server instruction,
635
+ // not a last-one-wins override -- fail closed rather than pick one by order.
636
+ if (plan.keyType) throw E("est/ambiguous-key-type", "a non-template CsrAttrs response must carry exactly one key-type attribute (RFC 9908 sec. 3.2)");
637
+ // Surface the raw values too: a key type this planner does not decode into
638
+ // curve / keySize (Ed25519, ML-DSA, ...) may still carry RFC 9908 parameters,
639
+ // so never silently drop them.
640
+ plan.keyType = { type: it.name, curve: it.curve || null, keySize: it.keySize || null, values: it.values || [] };
641
+ }
642
+ else plan.unhandled.push({ kind: it.kind, oid: it.oid, name: it.name });
643
+ }
644
+ return plan;
645
+ }
646
+
647
+ var SAN_OID = oid.byName("subjectAltName");
648
+
649
+ // Pull the SubjectAltName extension's raw extnValue (the DER GeneralNames) off a
650
+ // parsed extension list -- a certificate's `.extensions` or a CSR's
651
+ // extensionRequest `.extensions` -- as { critical, value }, or null when no SAN
652
+ // is present. Both fields matter: "identical" (RFC 7030 sec. 4.2.2) covers the
653
+ // criticality flag, not just the GeneralNames bytes.
654
+ function _san(extList) {
655
+ if (!Array.isArray(extList)) return null;
656
+ for (var i = 0; i < extList.length; i++) {
657
+ if (extList[i].oid === SAN_OID) return { critical: !!extList[i].critical, value: extList[i].value };
658
+ }
659
+ return null;
660
+ }
661
+
662
+ // The requested extensions a CSR carries in its extensionRequest attribute
663
+ // (RFC 2985 sec. 5.4.2), decoded by the CSR parser, or null when absent. A CSR
664
+ // carrying more than one extensionRequest is structurally legal (no SET-OF
665
+ // uniqueness) but AMBIGUOUS for the re-enroll SAN comparison -- fail closed
666
+ // rather than trusting the DER-first one while a later one requests a different SAN.
667
+ function _csrRequestedExtensions(parsedCsr) {
668
+ var attrs = (parsedCsr && parsedCsr.attributes) || [];
669
+ var extReqOid = oid.byName("extensionRequest");
670
+ var found = null, count = 0;
671
+ for (var i = 0; i < attrs.length; i++) {
672
+ if (attrs[i].type === extReqOid) { count += 1; found = attrs[i].extensions || null; }
673
+ }
674
+ if (count > 1) throw E("est/reenroll-ambiguous-request", "a re-enroll CSR must not carry more than one extensionRequest attribute (RFC 7030 sec. 4.2.2)");
675
+ return found;
676
+ }
677
+
678
+ // A re-enroll CSR MUST reuse the old certificate's Subject and SubjectAltName
679
+ // extension byte-identically (RFC 7030 sec. 4.2.2). Returns the old cert's
680
+ // subject + SAN for verbatim reuse in the new CSR; on a caller-supplied new CSR,
681
+ // compares both by raw DER (a rendered DN two different string encodings can
682
+ // share is NOT enough) and throws est/reenroll-subject-mismatch or
683
+ // est/reenroll-san-mismatch on any divergence (a SAN present on one side and
684
+ // absent on the other is a mismatch, not an accept).
685
+ function reenrollGuard(oldCertDer, newCsrDer) {
686
+ var oldCert = x509.parse(oldCertDer);
687
+ var oldSubject = oldCert.subject.rdns; // the parsed subject; raw region surfaced below
688
+ var oldSubjectDn = oldCert.subject.dn;
689
+ var oldSubjectBytes = oldCert.subject.bytes;
690
+ var oldSan = _san(oldCert.extensions);
691
+ if (newCsrDer === undefined) return { subjectDn: oldSubjectDn, subject: oldSubject, subjectAltName: oldSan };
692
+ var parsedCsr = csr.parse(newCsrDer);
693
+ var subjectMatches = Buffer.isBuffer(oldSubjectBytes) && Buffer.isBuffer(parsedCsr.subject.bytes) && oldSubjectBytes.equals(parsedCsr.subject.bytes);
694
+ if (!subjectMatches) throw E("est/reenroll-subject-mismatch", "a re-enroll CSR subject must be byte-identical to the certificate being renewed (RFC 7030 sec. 4.2.2)");
695
+ var newSan = _san(_csrRequestedExtensions(parsedCsr));
696
+ var sanMatches = (oldSan === null && newSan === null) ||
697
+ (oldSan && newSan && oldSan.critical === newSan.critical && Buffer.isBuffer(oldSan.value) && Buffer.isBuffer(newSan.value) && oldSan.value.equals(newSan.value));
698
+ if (!sanMatches) throw E("est/reenroll-san-mismatch", "a re-enroll CSR subjectAltName (names and criticality) must be identical to the certificate being renewed (RFC 7030 sec. 4.2.2)");
699
+ return { subjectDn: oldSubjectDn, subjectAltName: oldSan };
700
+ }
701
+
702
+ module.exports = {
703
+ transferDecode: transferDecode,
704
+ transferEncode: transferEncode,
705
+ splitMultipartMixed: splitMultipartMixed,
706
+ parseCertsOnly: parseCertsOnly,
707
+ findIssuedCert: findIssuedCert,
708
+ parseServerKeygenResponse: parseServerKeygenResponse,
709
+ classifyResponse: classifyResponse,
710
+ paths: paths,
711
+ challengePasswordFromTlsUnique: challengePasswordFromTlsUnique,
712
+ decryptKeyIdentifierAttr: decryptKeyIdentifierAttr,
713
+ asymmetricDecryptKeyIdentifierAttr: asymmetricDecryptKeyIdentifierAttr,
714
+ smimeCapabilitiesAttr: smimeCapabilitiesAttr,
715
+ buildEnrollAttributes: buildEnrollAttributes,
716
+ reenrollGuard: reenrollGuard,
717
+ };