@blamejs/pki 0.3.26 → 0.3.28

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/cmp-verify.js CHANGED
@@ -94,7 +94,7 @@ function _verdict(m, type, protectionAlg, valid, trusted, code, reason, signer)
94
94
  trusted: trusted,
95
95
  protectionType: type,
96
96
  protectionAlg: protectionAlg ? { oid: protectionAlg.oid, name: protectionAlg.name || null } : null,
97
- signer: signer ? { cert: signer.der, spki: signer.spki, subject: signer.subject } : null,
97
+ signer: signer ? { cert: signer.der, spki: signer.spki, subject: signer.subject, chain: signer.chain || null } : null,
98
98
  transactionID: m.header.transactionID || null,
99
99
  senderNonce: m.header.senderNonce || null,
100
100
  recipNonce: m.header.recipNonce || null,
@@ -496,6 +496,10 @@ async function _verifySignature(m, protectedPart, protectionAlg, protection, opt
496
496
  // the caller to anchor. With a trust store the signer cert gets the FULL RFC 5280 sec. 6.1 path gates.
497
497
  if (opts.trustAnchors == null) return _ok(m, "signature", protectionAlg, false, signer);
498
498
  var trust = await _chainSigner(signer, m, opts, extra);
499
+ // Surface the ACTUAL validated chain (signer + the intermediates path.build used) on a trusted verdict, so a
500
+ // caller can cache exactly the certificates that established trust rather than the UNSIGNED extraCerts a peer
501
+ // can pad -- an appended-but-unused certificate never enters this chain.
502
+ if (trust.chain) signer.chain = trust.chain;
499
503
  return _verdict(m, "signature", protectionAlg, true, trust.trusted, trust.trusted ? null : "cmp/untrusted-signer", trust.reason, signer);
500
504
  }
501
505
 
@@ -545,7 +549,17 @@ async function _chainSigner(signer, m, opts, extra) {
545
549
  try {
546
550
  var res = await _engine.build(signer.der, buildOpts);
547
551
  if (!res || res.valid !== true) return { trusted: false, reason: "the signer certificate did not chain to a supplied trust anchor" };
548
- return { trusted: true, reason: null };
552
+ // res.path is the ordered certificates path.build assembled (signer + the intermediates it used), as PARSED
553
+ // objects whose byte fields are subarrays of the (possibly multi-MB) response allocation. Map each back to its
554
+ // source DER (the signer, or a DER pool entry) and return an INDEPENDENT copy, so a caller caching the chain
555
+ // holds standalone buffers -- never slices pinning the whole response. Only certs on the trusted path enter it,
556
+ // so unsigned extraCerts padding is excluded. (A path cert sourced from an already-parsed intermediate carries
557
+ // no DER to copy and is omitted; for a Buffer/DER pool -- the cmp.session case -- the chain is complete.)
558
+ var byKey = Object.create(null);
559
+ var skey = _certKey(signer.parsed); if (skey) byKey[skey] = signer.der;
560
+ pool.forEach(function (c) { if (Buffer.isBuffer(c) || c instanceof Uint8Array) { var k = _certKey(c); if (k && !byKey[k]) byKey[k] = c; } });
561
+ var chain = res.path.map(function (pc) { var k = _certKey(pc); var d = k ? byKey[k] : null; return d ? Buffer.from(d) : null; }).filter(Boolean);
562
+ return { trusted: true, reason: null, chain: chain };
549
563
  } catch (e) {
550
564
  // A config-tier fault from path.build (an invalid opts.time, an empty / malformed trustAnchors or
551
565
  // intermediate) is a DEPLOYMENT error, not an untrusted signer -- rethrow it as cmp/bad-input so it is
@@ -669,9 +683,12 @@ async function _verify(message, opts) {
669
683
  * Returns a verdict (never a bare boolean): `{ valid, trusted, protectionType, protectionAlg, signer,
670
684
  * transactionID, senderNonce, recipNonce, header, body, code?, reason? }`. `valid` is whether the
671
685
  * protection is cryptographically intact under the declared algorithm; `trusted` is whether a MAC secret
672
- * matched or a signature signer certificate chained to a supplied trust anchor. A well-formed but
673
- * unverifiable message is a `{ valid: false }` verdict carrying a `cmp/*` code, not a throw; only malformed
674
- * input (a non-PKIMessage, a bad required opt, a flavor/credential mismatch) throws a typed `CmpError`.
686
+ * matched or a signature signer certificate chained to a supplied trust anchor. On a trusted signature
687
+ * verdict `signer.chain` is the validated certificate path as independent DER buffers (the signer plus the
688
+ * intermediates that chained it to the anchor) -- the certificates actually used, never the unsigned
689
+ * `extraCerts` a peer can pad, and never a slice pinning the response allocation. A
690
+ * well-formed but unverifiable message is a `{ valid: false }` verdict carrying a `cmp/*` code, not a throw;
691
+ * only malformed input (a non-PKIMessage, a bad required opt, a flavor/credential mismatch) throws a typed `CmpError`.
675
692
  *
676
693
  * @opts
677
694
  * - `sharedSecret` (string|Buffer) -- the PBMAC1 secret; REQUIRED for a MAC-protected message (UTF-8).
@@ -702,4 +719,5 @@ module.exports = {
702
719
  wellKnownUrl: cmpBuild.wellKnownUrl,
703
720
  verify: verify,
704
721
  setEngine: setEngine,
722
+ senderBoundToCert: _senderBoundToCert,
705
723
  };
package/lib/constants.js CHANGED
@@ -161,6 +161,10 @@ var LIMITS = {
161
161
  // document plus multipart/header overhead (16 MiB * 4/3 ~= 21.3 MiB); 24 MiB
162
162
  // clears it. An operator may only tighten it DOWNWARD via opts.maxResponseBytes.
163
163
  HTTP_MAX_RESPONSE_BYTES: BYTES.mib(24),
164
+ // A single untrusted WWW-Authenticate challenge header, bounded BEFORE the string
165
+ // is copied/tokenized so a hostile server cannot drive unbounded work parsing it
166
+ // (CWE-770). 8 KiB clears any realistic multi-scheme Digest challenge.
167
+ HTTP_AUTH_HEADER_MAX_BYTES: BYTES.kib(8),
164
168
  // Deterministic-CBOR codec ceilings (RFC 8949), the DER neighbours' siblings:
165
169
  // a whole-document cap refused before the walk, a nesting cap, and a per-value
166
170
  // bignum ceiling the document cap can't provide. Unlike DER_MAX_INTEGER_BYTES,
package/lib/est.js CHANGED
@@ -57,10 +57,13 @@ var constants = require("./constants");
57
57
  var cms = require("./schema-cms");
58
58
  var x509 = require("./schema-x509");
59
59
  var pkcs8 = require("./schema-pkcs8");
60
+ var key = require("./key");
60
61
  var csr = require("./schema-csr");
62
+ var csrattrsFmt = require("./schema-csrattrs"); // aliased: the `csrattrs` name is the verb + the public export
61
63
  var frameworkError = require("./framework-error");
62
64
  var guard = require("./guard-all");
63
65
  var httpTransport = require("./http-transport");
66
+ var httpDigest = require("./http-digest");
64
67
  var retryAfter = require("./http-retry-after");
65
68
 
66
69
  var EstError = frameworkError.EstError;
@@ -175,7 +178,10 @@ function splitMultipartMixed(body, contentType) {
175
178
  var rawHeaders = seg.slice(0, sep);
176
179
  var partBody = seg.slice(sep).replace(/^(\r?\n){2}/, "").replace(/\r?\n$/, "");
177
180
  var headers = {};
178
- rawHeaders.split(/\r?\n/).forEach(function (line) {
181
+ // Unfold MIME part headers (RFC 5322 sec. 2.2.3 / RFC 2046): a CRLF immediately followed by whitespace is a
182
+ // FOLD, not a header break, so it is removed before the split -- a Content-Type whose smime-type parameter
183
+ // continues on the next line must be read whole, not truncated.
184
+ rawHeaders.replace(/\r?\n(?=[ \t])/g, "").split(/\r?\n/).forEach(function (line) {
179
185
  var col = line.indexOf(":");
180
186
  if (col > 0) headers[line.slice(0, col).trim().toLowerCase()] = line.slice(col + 1).trim();
181
187
  });
@@ -235,14 +241,24 @@ function findIssuedCert(certs, target) {
235
241
  // Collect every key-identifier (subjectKeyIdentifier / KEKIdentifier) a set of
236
242
  // RecipientInfos names, across the ktri / kari / kekri / kemri arms -- the
237
243
  // byte identifiers a client's advertised decryptKeyID would match.
238
- function _recipientKeyIds(recipientInfos) {
244
+ // The key-identifier bytes each RecipientInfo arm names, filtered by MECHANISM (RFC 7030 sec. 4.4.1): an
245
+ // ASYMMETRIC decryption key (AsymmetricDecryptKeyIdentifier) is named by a subjectKeyIdentifier on a
246
+ // KeyTrans / KeyAgree / KEM arm, a SYMMETRIC key (DecryptKeyIdentifier) by the keyIdentifier of a KEK arm.
247
+ // Filtering keeps a symmetric KEK arm from satisfying an advertised asymmetric key (and vice-versa) merely
248
+ // because the identifier bytes coincide -- the returned key would be unusable with the mechanism requested.
249
+ // kind: "asymmetric" | "symmetric" | undefined (any, for a direct caller that did not state the mechanism).
250
+ function _recipientKeyIds(recipientInfos, kind) {
239
251
  var ids = [];
240
252
  function push(v) { if (Buffer.isBuffer(v)) ids.push(v); }
241
253
  (recipientInfos || []).forEach(function (r) {
242
- if (r.rid) push(r.rid.subjectKeyIdentifier);
243
- if (r.kemri && r.kemri.rid) push(r.kemri.rid.subjectKeyIdentifier); // KEMRecipientInfo (RFC 9629, under the ori arm)
244
- if (r.kekid) push(r.kekid.keyIdentifier);
245
- (r.recipientEncryptedKeys || []).forEach(function (rek) { if (rek.rid) push(rek.rid.subjectKeyIdentifier); });
254
+ if (kind !== "symmetric") {
255
+ if (r.rid) push(r.rid.subjectKeyIdentifier);
256
+ if (r.kemri && r.kemri.rid) push(r.kemri.rid.subjectKeyIdentifier); // KEMRecipientInfo (RFC 9629, under the ori arm)
257
+ (r.recipientEncryptedKeys || []).forEach(function (rek) { if (rek.rid) push(rek.rid.subjectKeyIdentifier); });
258
+ }
259
+ if (kind !== "asymmetric") {
260
+ if (r.kekid) push(r.kekid.keyIdentifier);
261
+ }
246
262
  });
247
263
  return ids;
248
264
  }
@@ -266,11 +282,23 @@ function _recipientIssuerSerials(recipientInfos) {
266
282
  // form the server may have used to name the same requested key.
267
283
  function _recipientMatches(recipientInfos, opts) {
268
284
  if (Buffer.isBuffer(opts.expectedRecipientKeyId) &&
269
- _recipientKeyIds(recipientInfos).some(function (id) { return id.equals(opts.expectedRecipientKeyId); })) return true;
285
+ _recipientKeyIds(recipientInfos, opts.expectedRecipientKind).some(function (id) { return id.equals(opts.expectedRecipientKeyId); })) return true;
270
286
  var ias = opts.expectedRecipientIssuerSerial;
271
287
  if (ias && Buffer.isBuffer(ias.issuer) && ias.serialNumber != null) {
272
- var want = typeof ias.serialNumber === "bigint" ? ias.serialNumber : BigInt(ias.serialNumber);
273
- if (_recipientIssuerSerials(recipientInfos).some(function (r) { return r.issuer.equals(ias.issuer) && r.serialNumber === want; })) return true;
288
+ // Validate the caller-supplied serial BEFORE BigInt() so a bad value is a typed est/bad-input, never a
289
+ // raw SyntaxError / RangeError leaking from the codec (the single choke point for both the serverkeygen
290
+ // verb and the public parseServerKeygenResponse).
291
+ var sn = ias.serialNumber, want;
292
+ if (typeof sn === "bigint" && sn >= 0n) want = sn;
293
+ else if (typeof sn === "number" && Number.isSafeInteger(sn) && sn >= 0) want = BigInt(sn);
294
+ else if (typeof sn === "string" && /^[0-9]+$/.test(sn)) want = BigInt(sn);
295
+ else throw E("est/bad-input", "expectedRecipientIssuerSerial.serialNumber must be a NON-NEGATIVE bigint, a safe non-negative integer, or a decimal digit string (a certificate serial is non-negative, RFC 5280 sec. 4.1.2.2)");
296
+ // An issuerAndSerialNumber names an ASYMMETRIC recipient -- a certificate identity the server derived from
297
+ // an AsymmetricDecryptKeyIdentifier (RFC 7030 sec. 4.4.2); a symmetric KEK recipient has none. So a SYMMETRIC
298
+ // request must NOT be satisfied by an asymmetric arm's issuer+serial, or the client would accept ciphertext
299
+ // undecryptable with the symmetric key it requested. The key-id branch applies the same mechanism filter.
300
+ if (opts.expectedRecipientKind !== "symmetric" &&
301
+ _recipientIssuerSerials(recipientInfos).some(function (r) { return r.issuer.equals(ias.issuer) && r.serialNumber === want; })) return true;
274
302
  }
275
303
  return false;
276
304
  }
@@ -337,6 +365,11 @@ function parseServerKeygenResponse(body, contentType, opts) {
337
365
  }
338
366
  if (!keyPart || !certPart) throw E("est/bad-multipart", "a serverkeygen response needs one key part and one certificate part");
339
367
  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)");
368
+ // The reverse is also a mismatch: the CSR advertised NO key-encryption key (requestedEncryption explicitly
369
+ // false), yet the server returned an EnvelopedData. The client holds nothing to decrypt it, so an unsolicited
370
+ // encrypted key is an unusable credential, not a success (RFC 7030 sec. 4.4.2). (An undefined requestedEncryption
371
+ // -- a low-level caller not stating the mode -- stays permissive.)
372
+ if (opts.requestedEncryption === false && encrypted) throw E("est/unexpected-encrypted-key", "the server returned an encrypted key but the CSR advertised no DecryptKeyIdentifier / AsymmetricDecryptKeyIdentifier to decrypt it (RFC 7030 sec. 4.4.2)");
340
373
  var out = { certificates: parseCertsOnly(transferDecode(certPart.body)).certificates };
341
374
  if (encrypted) {
342
375
  // The server-generated key MUST be a CMS EnvelopedData (RFC 7030 sec. 4.4.2);
@@ -360,7 +393,13 @@ function parseServerKeygenResponse(body, contentType, opts) {
360
393
  }
361
394
  out.encryptedKey = parsedKey;
362
395
  } else {
363
- out.privateKey = pkcs8.parse(transferDecode(keyPart.body));
396
+ var keyDer = transferDecode(keyPart.body);
397
+ out.privateKey = pkcs8.parse(keyDer);
398
+ // The raw cleartext PrivateKeyInfo DER is surfaced so a semantic layer can bind the key to its
399
+ // certificate (the public-key coherence check the verb runs). Structural parsing itself never
400
+ // does the crypto derivation -- mirroring parseCertsOnly, which splits certs while the enroll
401
+ // verb picks the issued one by public-key match.
402
+ out.privateKeyDer = keyDer;
364
403
  }
365
404
  return out;
366
405
  }
@@ -677,15 +716,35 @@ function _redirectTarget(current, location, method, opts) {
677
716
  return resolved;
678
717
  }
679
718
 
680
- // Does WWW-Authenticate advertise a Basic challenge? The quoted-string contents are blanked first
681
- // (a quoted auth-param may contain a comma + "Basic", e.g. `Digest realm="x, Basic required"`, which
682
- // must NOT be read as a Basic challenge), then Basic is matched as an auth-scheme TOKEN (at the start
683
- // or after a comma-separated challenge, followed by whitespace / a comma / end) -- never a substring,
684
- // so `Digest realm="basic"` / `Bearer error="basic required"` are not taken as Basic either.
685
- function _hasBasicChallenge(www) {
719
+ // Does WWW-Authenticate advertise the given auth-scheme as a TOKEN? The quoted-string contents are blanked
720
+ // first (a quoted auth-param may contain a comma + the scheme name, e.g. `Digest realm="x, Basic required"`,
721
+ // which must NOT be read as a Basic challenge), then the scheme is matched at the start or after a
722
+ // comma-separated challenge, followed by whitespace / a comma / end -- never a substring, so `Digest
723
+ // realm="basic"` is not taken as Basic. `scheme` is always our own literal ("Basic" / "Digest").
724
+ function _offersScheme(www, scheme) {
686
725
  var s = String(www || "").replace(/"(?:[^"\\]|\\.)*"/g, "\"\"");
687
- return /(?:^|,)\s*Basic(?=[\s,]|$)/i.test(s);
726
+ return new RegExp("(?:^|,)\\s*" + scheme + "(?=[\\s,]|$)", "i").test(s);
727
+ }
728
+ // The chosen HTTP auth scheme: an explicit opts.auth.scheme wins; a legacy opts.username / opts.password
729
+ // WITHOUT opts.auth means Basic (backwards compatible); otherwise none. There is NO scheme:"auto" -- a
730
+ // Basic<->Digest downgrade is never silently chosen (a MITM stripping Digest to Basic must not succeed).
731
+ function _authScheme(opts) {
732
+ if (opts.auth && opts.auth.scheme != null) {
733
+ var sc = String(opts.auth.scheme).toLowerCase();
734
+ if (sc !== "basic" && sc !== "digest") throw E("est/bad-input", "opts.auth.scheme must be \"basic\" or \"digest\"");
735
+ return sc;
736
+ }
737
+ if (opts.username !== undefined || opts.password !== undefined) return "basic";
738
+ return null;
688
739
  }
740
+ function _authUser(opts) { return opts.auth && opts.auth.username !== undefined ? opts.auth.username : opts.username; }
741
+ function _authPass(opts) { return opts.auth && opts.auth.password !== undefined ? opts.auth.password : opts.password; }
742
+ // Credentials present -- an EMPTY username is allowed (RFC 7030 sec. 3.2.3), so "defined" (not "truthy").
743
+ function _hasCreds(opts) { return _authUser(opts) !== undefined || _authPass(opts) !== undefined; }
744
+ var DIGEST_CODES = { unsupportedAlgorithm: "est/digest-unsupported-algorithm", weakAlgorithm: "est/digest-weak-algorithm", noQop: "est/digest-no-qop", badChallenge: "est/digest-bad-challenge" };
745
+ // The Digest answer policy, from opts.auth. The SAME object drives challenge SELECTION (parseChallenge) and
746
+ // the ANSWER (answer), so the client never selects a challenge under one policy then answers under another.
747
+ function _digestPolicy(opts) { return { allowMD5: !!(opts.auth && opts.auth.allowMD5), allowLegacyQop: !!(opts.auth && opts.auth.allowLegacyQop), codes: DIGEST_CODES }; }
689
748
 
690
749
  // The redirect-follow + HTTP-auth-retry loop. Returns the terminal transport response
691
750
  // ({status, headers, body}) -- a 2xx, a 202, or a non-401 4xx/5xx. A 3xx is followed
@@ -696,6 +755,9 @@ function _hasBasicChallenge(www) {
696
755
  function _drive(method, url, body, headers, opts, transport, budgets) {
697
756
  var redirects = 0;
698
757
  var authTried = false;
758
+ var staleRetries = 0; // bounded re-answers to a Digest stale=true re-challenge (a fresh nonce), never an open loop
759
+ var authSpaces = 0; // distinct Digest protection spaces (realms) entered beyond the first -- bounded so a server cannot loop with an endless stream of new realms
760
+ var lastDigestNonce = null; // the nonce just answered -- a stale=true retry MUST carry a DIFFERENT (fresh) nonce
699
761
  var initialOrigin = url.origin; // the origin the caller intended to authenticate to
700
762
  // The per-hop TLS: the origin-specific identity (mTLS cert/key + pinned SNI) is sent ONLY to the caller's
701
763
  // configured origin; a cross-origin hop gets a narrowed copy with those stripped (checkServerIdentity kept).
@@ -709,14 +771,30 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
709
771
  }
710
772
  return t;
711
773
  }
712
- // The per-hop Authorization: the HTTP Basic credential (established after a 401 on the caller's origin) is
713
- // sent ONLY to that origin. A cross-origin hop travels unauthenticated, but a redirect back to the origin
714
- // restores it -- so an authenticated flow that bounces off-origin and returns still completes (mirrors _tlsFor).
774
+ // The per-hop Authorization, sent ONLY to the caller's origin (a cross-origin hop travels unauthenticated; a
775
+ // redirect back to the origin restores it -- mirrors _tlsFor). A Basic credential is target-INDEPENDENT, so it
776
+ // is cached whole in authValue. A Digest response is bound to the request method + target, so the CHALLENGE is
777
+ // cached and the answer is (re)computed for the CURRENT method / uri on every hop -- a same-origin redirect (or
778
+ // a 303 POST->GET) then gets a target-correct answer instead of a stale one the server would reject.
715
779
  var authValue = null;
780
+ var digestChallenge = null;
781
+ var digestNcByNonce = Object.create(null); // nonce string -> count of requests answered under THAT nonce, tracked PER NONCE so a nonce reissued after others were used never repeats an nc (RFC 7616 sec. 3.4); bounded by the total answered 401s; a null-proto map so an attacker-chosen nonce (e.g. "__proto__") is an ordinary key
782
+ var digestSent = false; // whether the LAST request actually carried a Digest Authorization -- the DIRECT signal that a following 401 rejects a credential, vs. is a fresh challenge for a request sent unauthenticated
716
783
  function _headersFor(u) {
717
- if (!authValue || u.origin !== initialOrigin) return headers;
784
+ digestSent = false;
785
+ if (u.origin !== initialOrigin || (!authValue && !digestChallenge)) return headers;
718
786
  var hh = Object.assign({}, headers);
719
- hh.authorization = authValue;
787
+ if (digestChallenge) {
788
+ // Preemptively reuse the cached answer only WITHIN the challenge's protection space (RFC 7616 sec. 3.5):
789
+ // a same-origin redirect to a URI outside the challenge's `domain` is sent unauthenticated, so the target
790
+ // resource's own challenge drives a fresh authentication instead of receiving an Authorization computed
791
+ // for the wrong protection space.
792
+ if (!httpDigest.inProtectionSpace(digestChallenge, u.origin, u.pathname + u.search)) return headers;
793
+ var nkey = digestChallenge.nonce;
794
+ digestNcByNonce[nkey] = (digestNcByNonce[nkey] || 0) + 1;
795
+ hh.authorization = httpDigest.answer(digestChallenge, { method: method, uri: u.pathname + u.search, username: _authUser(opts), password: _authPass(opts), body: body, nc: digestNcByNonce[nkey], policy: _digestPolicy(opts) }, E);
796
+ digestSent = true;
797
+ } else { hh.authorization = authValue; }
720
798
  return hh;
721
799
  }
722
800
  function step() {
@@ -751,17 +829,65 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
751
829
  return step();
752
830
  }
753
831
  if (status === 401) {
754
- if (authTried) throw E("est/auth-required", "the server rejected the credentialed request (RFC 7030 sec. 3.2.3)");
755
- // Answer a challenge ONLY on the origin the caller targeted: a 401 arriving after a
756
- // cross-origin redirect is a different server, and the client MUST NOT send its credentials
757
- // there (RFC 7030 sec. 3.6 -- credentials go only to an authorized server the caller chose).
832
+ // Answer a challenge ONLY on the origin the caller targeted: a 401 arriving after a cross-origin
833
+ // redirect is a different server, and the client MUST NOT send its credentials there (RFC 7030 sec. 3.6).
758
834
  if (url.origin !== initialOrigin) throw E("est/auth-required", "refusing to send HTTP credentials to a redirected origin (RFC 7030 sec. 3.6)");
759
835
  var www = String(h["www-authenticate"] || "");
760
- if (!_hasBasicChallenge(www)) throw E("est/auth-required", "the server requires an unsupported HTTP authentication scheme (only Basic is supported): " + www);
761
- if (opts.username === undefined && opts.password === undefined) throw E("est/auth-required", "the server requires HTTP authentication but no credentials were supplied (RFC 7030 sec. 3.2.3)");
836
+ // Bound the attacker-controlled challenge header BEFORE any scheme scan / copy (_offersScheme, the parser):
837
+ // an injected transport without its own header limit must not cause unbounded allocation or work despite
838
+ // the parser's own cap being applied later (CWE-400). The cap matches the Digest parser's.
839
+ if (www.length > constants.LIMITS.HTTP_AUTH_HEADER_MAX_BYTES) throw E("est/auth-required", "the WWW-Authenticate header exceeds the " + constants.LIMITS.HTTP_AUTH_HEADER_MAX_BYTES + "-byte cap (RFC 7030 sec. 3.2.3)");
840
+ var scheme = _authScheme(opts);
841
+ if (scheme === null) throw E("est/auth-required", "the server requires HTTP authentication but no credentials were supplied (RFC 7030 sec. 3.2.3)");
842
+ if (scheme === "digest") {
843
+ if (!_offersScheme(www, "Digest")) throw E("est/auth-required", "opts.auth.scheme is \"digest\" but the server offered no Digest challenge: " + www);
844
+ if (!_hasCreds(opts)) throw E("est/auth-required", "Digest authentication requires opts.auth.username / password (RFC 7030 sec. 3.2.3)");
845
+ // In a credential-rejection context (this request carried a Digest answer), bias selection toward a
846
+ // RETRYABLE stale=true offer so a stronger stale=false offer does not shadow one that could re-answer.
847
+ var selPol = _digestPolicy(opts);
848
+ selPol.preferStale = digestSent;
849
+ selPol.priorNonce = lastDigestNonce; // a retryable stale offer must carry a nonce DIFFERENT from the one just rejected
850
+ selPol.priorRealm = digestChallenge && digestChallenge.realm; // ...and be for the SAME realm that was rejected (a different realm is a new space, not a retry)
851
+ selPol.requestOrigin = url.origin; // prefer an offer whose protection space actually covers THIS request --
852
+ selPol.requestTarget = url.pathname + url.search; // one scoped elsewhere cannot authenticate it and must not shadow one that can
853
+ var ch = httpDigest.parseChallenge(www, E, "est/digest-bad-challenge", selPol);
854
+ if (!ch) throw E("est/auth-required", "the server offered no usable Digest challenge: " + www);
855
+ // Whether this 401 REJECTS a credential turns on the DIRECT signal (RFC 7616 sec. 3.3 / 3.5): did the
856
+ // request that received it actually CARRY a Digest answer for THIS protection space? A request sent
857
+ // unauthenticated -- the first attempt, or one whose target lay outside the cached challenge's domain --
858
+ // has not authenticated, so its 401 (even in the same realm) is a FRESH challenge to answer, never a
859
+ // rejection. A credentialed request refused in the SAME realm IS a rejection of that credential: only a
860
+ // stale=true re-challenge carrying a FRESH nonce may retry (a repeated nonce with stale=true is a
861
+ // self-contradictory / hostile server -> terminate). Any fresh authentication -- the first, a newly
862
+ // entered protection space, or an unauthenticated resource -- is bounded (maxRedirects) against a loop.
863
+ if (digestSent && digestChallenge && ch.realm === digestChallenge.realm) {
864
+ if (!(ch.stale && ch.nonce !== lastDigestNonce && staleRetries < budgets.maxStaleRetries)) throw E("est/auth-required", "the server rejected the credentialed Digest request (RFC 7030 sec. 3.2.3)");
865
+ staleRetries += 1;
866
+ } else {
867
+ if (authTried && authSpaces >= budgets.maxRedirects) throw E("est/auth-required", "the server demanded Digest authentication for too many distinct protection spaces (RFC 7616 sec. 3.3)");
868
+ if (authTried) authSpaces += 1;
869
+ authTried = true;
870
+ staleRetries = 0; // a fresh authentication (new space / unauthenticated resource) restarts the stale budget
871
+ }
872
+ lastDigestNonce = ch.nonce;
873
+ // Cache the CHALLENGE, not a fixed answer: _headersFor computes the response for the CURRENT method /
874
+ // request-target on each hop, so a same-origin redirect (or a 303 POST->GET) gets a target-correct answer
875
+ // rather than a stale one. A policy fault (MD5 / no-qop / unsupported) surfaces from _headersFor on the
876
+ // immediate re-send (the transport is not reached, so a rejected leg still proves calls did not advance).
877
+ // The nonce-count is tracked PER NONCE (digestNcByNonce), so a nonce reissued after other nonces were
878
+ // used in between resumes its own count -- a (nonce, nc) pair is never replayed, without a reset here.
879
+ digestChallenge = ch;
880
+ return step();
881
+ }
882
+ // Basic (default / explicit): the one-shot semantics unchanged.
883
+ if (authTried) throw E("est/auth-required", "the server rejected the credentialed request (RFC 7030 sec. 3.2.3)");
884
+ if (!_offersScheme(www, "Basic")) throw E("est/auth-required", "the server requires an unsupported HTTP authentication scheme (only Basic and Digest are supported): " + www);
885
+ // An explicit auth.scheme:"basic" with no credentials must fail closed too (an empty username stays legal),
886
+ // never transmit a "Basic Og==" (base64 of ":") the operator never supplied.
887
+ if (!_hasCreds(opts)) throw E("est/auth-required", "the server requires HTTP authentication but no credentials were supplied (RFC 7030 sec. 3.2.3)");
762
888
  // Establish the credential as origin-scoped state (_headersFor attaches it only on the initial origin),
763
889
  // never a mutation of the shared headers that would leak across a cross-origin redirect.
764
- authValue = "Basic " + Buffer.from((opts.username || "") + ":" + (opts.password || ""), "utf8").toString("base64");
890
+ authValue = "Basic " + Buffer.from((_authUser(opts) || "") + ":" + (_authPass(opts) || ""), "utf8").toString("base64");
765
891
  authTried = true;
766
892
  return step();
767
893
  }
@@ -794,7 +920,9 @@ function _client(op, method, baseUrl, body, headers, opts) {
794
920
  timeout: guard.limits.cap(opts.timeout, "timeout", DEFAULT_TIMEOUT, { E: E, code: "est/bad-input", min: 1, max: MAX_TIMEOUT }),
795
921
  maxResponseBytes: guard.limits.cap(opts.maxResponseBytes, "maxResponseBytes", constants.LIMITS.HTTP_MAX_RESPONSE_BYTES, { E: E, code: "est/bad-input", min: 1, max: constants.LIMITS.HTTP_MAX_RESPONSE_BYTES }),
796
922
  maxRedirects: guard.limits.cap(opts.maxRedirects, "maxRedirects", 5, { E: E, code: "est/bad-input", min: 0, max: 32 }),
923
+ maxStaleRetries: guard.limits.cap(opts.auth && opts.auth.maxStaleRetries, "maxStaleRetries", 1, { E: E, code: "est/bad-input", min: 0, max: 8 }),
797
924
  };
925
+ _authScheme(opts); // config-time: a bad opts.auth.scheme throws est/bad-input here, never deferred to a 401
798
926
  return _drive(method, url, body, Object.assign({}, headers), opts, transport, budgets);
799
927
  }
800
928
 
@@ -944,10 +1072,205 @@ function simplereenroll(baseUrl, csrInput, opts) {
944
1072
  });
945
1073
  }
946
1074
 
1075
+ // ---- /serverkeygen (RFC 7030 sec. 4.4) ----------------------------------
1076
+
1077
+ // Derive the server-generated-key ENCRYPTION coherence from the CSR the caller actually POSTs (RFC 7030
1078
+ // sec. 4.4.1): a DecryptKeyIdentifier / AsymmetricDecryptKeyIdentifier attribute names the key-encryption
1079
+ // key the server must encrypt the generated private key to. Binding the expected recipient to the CSR (not a
1080
+ // free-floating opts boolean) closes the drift where a cleartext key rides past a caller who forgot the flag.
1081
+ function _serverkeygenEncryptionFromCsr(csrDer) {
1082
+ var attrs = csr.parse(csrDer).attributes || [];
1083
+ var keyId = null, kind = null;
1084
+ for (var i = 0; i < attrs.length; i++) {
1085
+ if (attrs[i].type !== OID_DECRYPT_KEY_ID && attrs[i].type !== OID_ASYMM_DECRYPT_KEY_ID) continue;
1086
+ // DecryptKeyIdentifier names a SYMMETRIC key-encryption key; AsymmetricDecryptKeyIdentifier an ASYMMETRIC
1087
+ // one (RFC 7030 sec. 4.4.1). The mechanism is preserved so the returned key part is matched to a compatible
1088
+ // RecipientInfo arm, not merely one whose identifier bytes coincide.
1089
+ var thisKind = attrs[i].type === OID_ASYMM_DECRYPT_KEY_ID ? "asymmetric" : "symmetric";
1090
+ var vals = attrs[i].values || [];
1091
+ if (vals.length !== 1) throw E("est/bad-input", "a serverkeygen key-identifier attribute must carry exactly one value (RFC 7030 sec. 4.4.1)");
1092
+ var id;
1093
+ try { id = asn1.read.octetString(asn1.decode(vals[0])); }
1094
+ catch (e) { throw E("est/bad-input", "a serverkeygen key-identifier attribute value is not a valid OCTET STRING", e); }
1095
+ if (keyId !== null && !keyId.equals(id)) throw E("est/bad-input", "the CSR advertised two different serverkeygen key identifiers (RFC 7030 sec. 4.4.1)");
1096
+ if (kind !== null && kind !== thisKind) throw E("est/bad-input", "the CSR advertised both a symmetric (DecryptKeyIdentifier) and an asymmetric (AsymmetricDecryptKeyIdentifier) serverkeygen key -- the recipient mechanism is ambiguous (RFC 7030 sec. 4.4.1)");
1097
+ keyId = id; kind = thisKind;
1098
+ }
1099
+ return { requestedEncryption: keyId !== null, expectedRecipientKeyId: keyId, expectedRecipientKind: kind };
1100
+ }
1101
+
1102
+ // The delivered private key rides TLS confidentiality alone: a NULL / anonymous / EXPORT suite MUST NOT be
1103
+ // used to carry it (RFC 7030 sec. 4.4, sec. 6). Assert (never reconfigure) the negotiated cipher when the
1104
+ // transport surfaces it; an injected transport that reports no cipher is trusted (the test/loopback channel).
1105
+ function _assertConfidentialCipher(res) {
1106
+ if (!res || !res.tls || !res.tls.cipher) return;
1107
+ var c = res.tls.cipher;
1108
+ var name = (String(c.name || "") + " " + String(c.standardName || "")).toUpperCase();
1109
+ // EXPORT covers the RFC/IANA name; the OpenSSL short names use an EXP prefix followed by a separator OR a
1110
+ // digit run (EXP-RC4-MD5, EXP1024-RC4-SHA), so match EXP before either -- a bare `\bEXP\b` misses EXP1024.
1111
+ if (/NULL|ANON|EXPORT|\bEXP[-_0-9]|\bA(EC)?DH\b/.test(name)) throw E("est/weak-cipher", "the serverkeygen channel negotiated a NULL / anonymous / EXPORT cipher (" + (c.standardName || c.name) + "), which cannot protect the delivered private key (RFC 7030 sec. 4.4)");
1112
+ }
1113
+
1114
+ // A case-insensitive header lookup: the injectable transport seam only promises { status, headers, body }, so a
1115
+ // hostile / non-Node transport may deliver "Content-Type" in any casing -- read it case-insensitively (classifyResponse
1116
+ // normalizes internally, but the multipart boundary is read from the header directly here).
1117
+ function _ciHeader(headers, name) {
1118
+ headers = headers || {};
1119
+ if (headers[name] !== undefined) return headers[name];
1120
+ var lname = name.toLowerCase(), keys = Object.keys(headers);
1121
+ for (var i = 0; i < keys.length; i++) { if (keys[i].toLowerCase() === lname) return headers[keys[i]]; }
1122
+ return null;
1123
+ }
1124
+
1125
+ async function _serverkeygenResult(res, opts, derived) {
1126
+ var verdict = classifyResponse(res.status, res.headers, res.body, { op: "serverkeygen", now: opts.now });
1127
+ // A 202 is an enrollment posture (RFC 7030 sec. 4.4 treats /serverkeygen as an enroll): surfaced, never slept.
1128
+ if (verdict.status === "retry") return { retry: true, retryAfterSeconds: verdict.retryAfterSeconds, retryAfterDate: verdict.retryAfterDate };
1129
+ if (verdict.status !== "ok") throw E("est/http-error", "an EST serverkeygen response must be HTTP 200 or 202 (RFC 7030 sec. 4.4.2), got " + res.status);
1130
+ var bodyLen = Buffer.isBuffer(res.body) ? res.body.length : Buffer.byteLength(String(res.body == null ? "" : res.body), "utf8");
1131
+ if (bodyLen === 0) throw E("est/empty-body", "a 200 serverkeygen response carried an empty body (RFC 7030 sec. 4.4.2)");
1132
+ _assertConfidentialCipher(res);
1133
+ // parseServerKeygenResponse does the per-part transfer-decode; the recipient coherence is bound to the CSR.
1134
+ var out = parseServerKeygenResponse(res.body, _ciHeader(res.headers, "content-type"), {
1135
+ requestedEncryption: derived.requestedEncryption,
1136
+ expectedRecipientKeyId: derived.expectedRecipientKeyId,
1137
+ expectedRecipientKind: derived.expectedRecipientKind,
1138
+ expectedRecipientIssuerSerial: opts.expectedRecipientIssuerSerial,
1139
+ });
1140
+ // Bind a CLEARTEXT server-generated key to its certificate (RFC 7030 sec. 4.4.2): the CA generated
1141
+ // this pair and issued a certificate over its PUBLIC half, so the delivered PrivateKeyInfo's public key
1142
+ // MUST equal EXACTLY ONE returned certificate's SubjectPublicKeyInfo -- a key unrelated to every
1143
+ // certificate is an unusable / mis-associated credential, and a key matching more than one leaves the
1144
+ // issued certificate ambiguous. Mirrors simpleenroll's findIssuedCert public-key match; the encrypted
1145
+ // key stays opaque, so its recipient coherence is bound in the parser instead. The public half is
1146
+ // derived through the key engine (never re-serialized here); a key whose public half cannot be derived
1147
+ // is not bindable and fails closed.
1148
+ if (out.privateKeyDer) {
1149
+ var spki;
1150
+ try { spki = await key.publicFromPrivate(out.privateKeyDer); }
1151
+ catch (e) { throw E("est/key-cert-mismatch", "the cleartext server-generated private key's public half could not be derived to bind it to a returned certificate (RFC 7030 sec. 4.4.2)", e); }
1152
+ var bound = findIssuedCert(out.certificates, spki);
1153
+ if (!bound) throw E("est/key-cert-mismatch", "the cleartext server-generated private key matches no returned certificate's public key (RFC 7030 sec. 4.4.2)");
1154
+ if (findIssuedCert(out.certificates.filter(function (c) { return c !== bound; }), spki)) throw E("est/ambiguous-issued-cert", "more than one returned certificate carries the server-generated key; the issued certificate is ambiguous (RFC 7030 sec. 4.4.2)");
1155
+ delete out.privateKeyDer;
1156
+ }
1157
+ return out;
1158
+ }
1159
+
1160
+ /**
1161
+ * @primitive pki.est.serverkeygen
1162
+ * @signature pki.est.serverkeygen(baseUrl, csr, opts?) -> Promise<{ certificates, privateKey } | { certificates, encryptedKey } | { retry, retryAfterSeconds, retryAfterDate }>
1163
+ * @since 0.3.28
1164
+ * @status experimental
1165
+ * @spec RFC 7030, RFC 8951
1166
+ * @related pki.est.simpleenroll, pki.est.parseServerKeygenResponse
1167
+ *
1168
+ * Request a SERVER-GENERATED key pair + certificate: POST the CSR (base64 DER, `Content-Type:
1169
+ * application/pkcs10`, identical request encoding to `simpleenroll`) to
1170
+ * `<baseUrl>/.well-known/est/serverkeygen`. The two-part `multipart/mixed` response is surfaced as
1171
+ * `{ certificates, privateKey }` (a cleartext PKCS#8 `PrivateKeyInfo`) or `{ certificates,
1172
+ * encryptedKey }` (the CMS `EnvelopedData` the caller decrypts out-of-band with its key-encryption key --
1173
+ * the verb NEVER decrypts, so it is not a decryption oracle), or `{ retry, retryAfterSeconds }` on a 202.
1174
+ * The certificates are RAW/unordered -- unlike `simpleenroll` no leaf is picked, because the CA generated
1175
+ * the key so the issued certificate's public key is the generated one, not the throwaway CSR key. A CLEARTEXT
1176
+ * key is bound to its certificate before it resolves: the delivered private key's public half MUST match
1177
+ * EXACTLY ONE returned certificate (`est/key-cert-mismatch` on none, `est/ambiguous-issued-cert` on more than
1178
+ * one), so a mis-associated key is refused rather than handed back unusable.
1179
+ * The encryption requirement + expected recipient are DERIVED from the CSR's own DecryptKeyIdentifier /
1180
+ * AsymmetricDecryptKeyIdentifier attribute; an `opts` value that contradicts the CSR is `est/bad-input`
1181
+ * (a cleartext-key downgrade cannot slip past). The delivered key's channel is asserted confidentiality-
1182
+ * bearing (a NULL / anonymous / EXPORT cipher is `est/weak-cipher`). https-only, explicit-anchor, and the
1183
+ * whole redirect / auth / budget machinery of `simpleenroll` apply.
1184
+ *
1185
+ * @opts
1186
+ * - `requestedEncryption` / `expectedRecipientKeyId` / `expectedRecipientIssuerSerial` -- OPTIONAL
1187
+ * overrides of the CSR-derived recipient coherence; a value that contradicts the CSR is `est/bad-input`.
1188
+ * - every option of pki.est.simpleenroll (transport, tls, label, budgets, credentials incl. `auth`).
1189
+ * @example
1190
+ * var r = await pki.est.serverkeygen("https://ca.example", csrDer,
1191
+ * { transport: function () { return Promise.resolve({ status: 202, headers: { "retry-after": "60" }, body: "" }); } });
1192
+ * r.retry; // true -- a 202 is surfaced, never slept
1193
+ */
1194
+ function serverkeygen(baseUrl, csrInput, opts) {
1195
+ opts = opts || {};
1196
+ return Promise.resolve().then(function () {
1197
+ var csrDer = _csrDer(csrInput);
1198
+ var derived = _serverkeygenEncryptionFromCsr(csrDer);
1199
+ if (opts.requestedEncryption !== undefined && !!opts.requestedEncryption !== derived.requestedEncryption) throw E("est/bad-input", "opts.requestedEncryption (" + !!opts.requestedEncryption + ") contradicts the CSR's advertised key-encryption attribute (" + derived.requestedEncryption + ") (RFC 7030 sec. 4.4.1)");
1200
+ if (opts.expectedRecipientKeyId !== undefined) {
1201
+ if (!Buffer.isBuffer(opts.expectedRecipientKeyId)) throw E("est/bad-input", "opts.expectedRecipientKeyId must be a Buffer");
1202
+ if (derived.expectedRecipientKeyId && !opts.expectedRecipientKeyId.equals(derived.expectedRecipientKeyId)) throw E("est/bad-input", "opts.expectedRecipientKeyId contradicts the key identifier the CSR advertised (RFC 7030 sec. 4.4.1)");
1203
+ }
1204
+ if (opts.expectedRecipientIssuerSerial != null) {
1205
+ var eis = opts.expectedRecipientIssuerSerial;
1206
+ if (typeof eis !== "object" || Buffer.isBuffer(eis) || !Buffer.isBuffer(eis.issuer)) throw E("est/bad-input", "opts.expectedRecipientIssuerSerial must be { issuer: Buffer, serialNumber }");
1207
+ var s = eis.serialNumber;
1208
+ if (!((typeof s === "bigint" && s >= 0n) || (typeof s === "number" && Number.isSafeInteger(s) && s >= 0) || (typeof s === "string" && /^[0-9]+$/.test(s)))) throw E("est/bad-input", "opts.expectedRecipientIssuerSerial.serialNumber must be a NON-NEGATIVE bigint, a safe non-negative integer, or a decimal digit string (a certificate serial is non-negative, RFC 5280 sec. 4.1.2.2)");
1209
+ }
1210
+ // A recipient expectation implies the key part MUST be encrypted to that recipient; if the CSR advertised no
1211
+ // key-encryption attribute the coherence check below never runs, so a compromised CA could deliver the key
1212
+ // to a recipient IT controls (or cleartext) while the caller believes its pin was enforced. Refuse the
1213
+ // contradiction at config time (RFC 7030 sec. 4.4.1) -- the caller must advertise the KEK in the CSR.
1214
+ if ((opts.expectedRecipientKeyId !== undefined || opts.expectedRecipientIssuerSerial != null) && !derived.requestedEncryption) {
1215
+ throw E("est/bad-input", "a recipient expectation (expectedRecipientKeyId / expectedRecipientIssuerSerial) implies an encrypted key, but the CSR advertised no DecryptKeyIdentifier / AsymmetricDecryptKeyIdentifier attribute (RFC 7030 sec. 4.4.1)");
1216
+ }
1217
+ return _client("serverkeygen", "POST", baseUrl, transferEncode(csrDer), { accept: "multipart/mixed", "content-type": "application/pkcs10" }, opts)
1218
+ .then(function (res) { return _serverkeygenResult(res, opts, derived); });
1219
+ });
1220
+ }
1221
+
1222
+ // ---- /csrattrs GET (RFC 7030 sec. 4.5, RFC 9908) ------------------------
1223
+
1224
+ function _csrattrsResult(res, opts) {
1225
+ var verdict = classifyResponse(res.status, res.headers, res.body, { op: "csrattrs", now: opts.now });
1226
+ // 204 / 404 = "CSR Attributes Response not available" -- a valid NONE outcome, not an error (RFC 7030 sec. 4.5.2).
1227
+ if (verdict.status === "none-available") return { available: false, attrs: null };
1228
+ // A 202 is nonconforming for a policy GET (sec. 4.5.2 lists only 200 / 204 / 404).
1229
+ if (verdict.status === "retry") throw E("est/http-error", "a /csrattrs response must be HTTP 200, 204, or 404, not 202 (RFC 7030 sec. 4.5.2)");
1230
+ if (verdict.status !== "ok") throw E("est/http-error", "an EST csrattrs response must be HTTP 200 / 204 / 404 (RFC 7030 sec. 4.5.2), got " + res.status);
1231
+ var bodyLen = Buffer.isBuffer(res.body) ? res.body.length : Buffer.byteLength(String(res.body == null ? "" : res.body), "utf8");
1232
+ // An empty HTTP body is distinct from a valid EMPTY CsrAttrs (`30 00` / base64 `MAA=`), which parses as {items:[]}.
1233
+ if (bodyLen === 0) throw E("est/empty-body", "a 200 csrattrs response carried an empty body (RFC 7030 sec. 4.5.2)");
1234
+ var attrs = csrattrsFmt.parse(transferDecode(res.body));
1235
+ return { available: true, attrs: attrs, plan: buildEnrollAttributes(attrs) };
1236
+ }
1237
+
1238
+ /**
1239
+ * @primitive pki.est.csrattrs
1240
+ * @signature pki.est.csrattrs(baseUrl, opts?) -> Promise<{ available: true, attrs, plan } | { available: false, attrs: null }>
1241
+ * @since 0.3.28
1242
+ * @status experimental
1243
+ * @spec RFC 7030, RFC 8951, RFC 9908
1244
+ * @related pki.est.simpleenroll, pki.est.buildEnrollAttributes
1245
+ *
1246
+ * Fetch the CA's CSR-attributes policy: GET `<baseUrl>/.well-known/est/csrattrs` (`Accept:
1247
+ * application/csrattrs`). A 200 body is base64-decoded, parsed as an RFC 9908 `CsrAttrs`, and returned
1248
+ * with a `plan` (`buildEnrollAttributes`) the caller applies to its NEXT CSR -- the verb NEVER auto-applies
1249
+ * attributes to a CSR (single responsibility). A 204 or 404 is `{ available: false }` (a valid "no specific
1250
+ * attributes"), NOT an error; an empty SEQUENCE (`30 00`) is a COMPLETE empty policy (`attrs.items` empty),
1251
+ * distinct from an empty HTTP body (`est/empty-body`). Server auth is NOT required for this policy GET but a
1252
+ * 401 is tolerated (the shared auth path stays live). https-only + explicit-anchor as elsewhere.
1253
+ *
1254
+ * @opts
1255
+ * - every transport / tls / label / budget / credential option of the other verbs.
1256
+ * @example
1257
+ * var r = await pki.est.csrattrs("https://ca.example",
1258
+ * { transport: function () { return Promise.resolve({ status: 404, headers: {}, body: "" }); } });
1259
+ * r.available; // false -- a 404 is "no CSR-attributes policy available"
1260
+ */
1261
+ function csrattrs(baseUrl, opts) {
1262
+ opts = opts || {};
1263
+ return Promise.resolve().then(function () {
1264
+ return _client("csrattrs", "GET", baseUrl, null, { accept: "application/csrattrs" }, opts);
1265
+ }).then(function (res) { return _csrattrsResult(res, opts); });
1266
+ }
1267
+
947
1268
  module.exports = {
948
1269
  cacerts: cacerts,
949
1270
  simpleenroll: simpleenroll,
950
1271
  simplereenroll: simplereenroll,
1272
+ serverkeygen: serverkeygen,
1273
+ csrattrs: csrattrs,
951
1274
  transferDecode: transferDecode,
952
1275
  transferEncode: transferEncode,
953
1276
  splitMultipartMixed: splitMultipartMixed,