@blamejs/pki 0.6.15 → 0.6.16
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 +13 -0
- package/lib/scep.js +132 -34
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ All notable changes to `@blamejs/pki` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## v0.6.16 — 2026-09-01
|
|
8
|
+
|
|
9
|
+
pki.scep.enroll and pki.scep.renew poll a PENDING enrollment to its issued certificate, sending RFC 8894 CertPoll requests until the CA answers or a bounded poll-count and wait budget is exhausted.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.scep.build and pki.scep.parse now handle the CertPoll (GetCertInitial, messageType 20) request: a pkcsPKIEnvelope carrying an IssuerAndSubject (the CA's subject name and the request's subject name), signed under the transaction attributes with the original transactionID (RFC 8894 sec. 3.3.3).
|
|
14
|
+
- pki.scep.enroll and pki.scep.renew poll a PENDING response to a terminal status. New options bound the loop: pollCount (maximum CertPoll requests, default 5), maxTotalWait (total Retry-After sleep budget in seconds), sleep (an injectable (ms) -> Promise sleeper), and onRetryAfter (an observer of each delay). A budget exhausted with no terminal response throws scep/poll-exhausted carrying the transactionId to resume with. For a deployment whose request is encrypted to a separate RA certificate, issuerCert names the issuing CA the CertPoll must carry (default caCert).
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- pki.scep.enroll and pki.scep.renew poll by default when the CA answers PENDING: a PENDING enrollment now resolves to the issued certificate or throws, rather than returning { status: "PENDING" } for the caller to retry. Pass pollCount: 0 to restore the single-shot PENDING return and drive the retry yourself.
|
|
19
|
+
|
|
7
20
|
## v0.6.15 — 2026-09-01
|
|
8
21
|
|
|
9
22
|
Verifying a Sigstore bundle no longer matches a pinned identity against a non-text subjectAltName value; the value is surfaced as null so the policy fails closed instead of comparing raw bytes. The library source also ships without its explanatory comments, keeping the JSDoc that generates the API reference and the SPDX license headers.
|
package/lib/scep.js
CHANGED
|
@@ -53,6 +53,8 @@ var pkix = require("./schema-pkix");
|
|
|
53
53
|
var guard = require("./guard-all");
|
|
54
54
|
var C = require("./constants");
|
|
55
55
|
var httpTransport = require("./http-transport");
|
|
56
|
+
var retryAfter = require("./http-retry-after");
|
|
57
|
+
var _defaultSleep = require("./sleep").sleep;
|
|
56
58
|
var nodeCrypto = require("node:crypto");
|
|
57
59
|
|
|
58
60
|
function _err(code, msg, cause) { return new ScepError(code, msg, cause); }
|
|
@@ -66,7 +68,7 @@ var FAIL_INFO = { badAlg: "0", badMessageCheck: "1", badRequest: "2", badTime: "
|
|
|
66
68
|
function _invert(table) { var o = Object.create(null); for (var k in table) if (Object.prototype.hasOwnProperty.call(table, k)) o[table[k]] = k; return o; }
|
|
67
69
|
var MESSAGE_TYPE_BY_CODE = _invert(MESSAGE_TYPE), PKI_STATUS_BY_CODE = _invert(PKI_STATUS), FAIL_INFO_BY_CODE = _invert(FAIL_INFO);
|
|
68
70
|
|
|
69
|
-
var _PARSE_SUPPORTED = { PKCSReq: 1, RenewalReq: 1, CertRep: 1 };
|
|
71
|
+
var _PARSE_SUPPORTED = { PKCSReq: 1, RenewalReq: 1, CertRep: 1, CertPoll: 1 };
|
|
70
72
|
|
|
71
73
|
var ENVELOPE_CIPHER = "aes-128-cbc";
|
|
72
74
|
|
|
@@ -239,23 +241,28 @@ async function build(spec) {
|
|
|
239
241
|
guard.identifier.assertKnownKeys(spec, _BUILD_KEYS, _err, "scep/bad-input", "pki.scep.build has an unknown option: ");
|
|
240
242
|
var mtCode = MESSAGE_TYPE[spec.messageType];
|
|
241
243
|
if (mtCode == null) throw _err("scep/bad-message-type", "unknown messageType " + JSON.stringify(spec.messageType) + " (RFC 8894 sec. 3.2.1.2)");
|
|
242
|
-
if (spec.messageType !== "PKCSReq" && spec.messageType !== "RenewalReq") {
|
|
243
|
-
throw _err("scep/bad-message-type", "pki.scep.build issues a request (PKCSReq / RenewalReq); " + spec.messageType + " is not built in this release");
|
|
244
|
+
if (spec.messageType !== "PKCSReq" && spec.messageType !== "RenewalReq" && spec.messageType !== "CertPoll") {
|
|
245
|
+
throw _err("scep/bad-message-type", "pki.scep.build issues a request (PKCSReq / RenewalReq / CertPoll); " + spec.messageType + " is not built in this release");
|
|
244
246
|
}
|
|
245
247
|
if (spec.signer == null || typeof spec.signer !== "object") throw _err("scep/bad-input", "a signer { cert, key } is required");
|
|
246
248
|
var senderNonce = _nonce(spec.senderNonce, "senderNonce");
|
|
247
249
|
var txnId = _transactionId(spec.transactionId);
|
|
248
|
-
var messageData = guard.bytes.snapshotSource(spec.messageData, ScepError, "scep/bad-input", "messageData");
|
|
249
250
|
var recipient = guard.bytes.snapshotSource(spec.recipient, ScepError, "scep/bad-input", "recipient");
|
|
250
251
|
_assertRsaRecipient(recipient);
|
|
251
252
|
var signerCert = guard.bytes.snapshotSource(spec.signer.cert, ScepError, "scep/bad-input", "signer.cert");
|
|
252
253
|
_assertSigningKeyUsage(signerCert, "signer certificate");
|
|
253
254
|
var signerKey = spec.signer.key;
|
|
254
|
-
var
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
255
|
+
var messageData;
|
|
256
|
+
if (spec.messageType === "CertPoll") {
|
|
257
|
+
messageData = _buildIssuerAndSubject(recipient, spec.requestSubject, spec.issuer);
|
|
258
|
+
} else {
|
|
259
|
+
messageData = guard.bytes.snapshotSource(spec.messageData, ScepError, "scep/bad-input", "messageData");
|
|
260
|
+
var parsedCsr;
|
|
261
|
+
try { parsedCsr = schemaCsr.parse(messageData); }
|
|
262
|
+
catch (e) { throw _err("scep/bad-input", "messageData is not a valid PKCS#10 CertificationRequest (RFC 8894 sec. 3.3.1): " + e.message, e); }
|
|
263
|
+
if (!(await csrVerify.verifyCsrSignature(parsedCsr))) {
|
|
264
|
+
throw _err("scep/bad-popo", "the PKCS#10 messageData failed its proof-of-possession: its self-signature does not verify under its own subject public key, so a CA would reject it");
|
|
265
|
+
}
|
|
259
266
|
}
|
|
260
267
|
var pkcsPKIEnvelope;
|
|
261
268
|
try {
|
|
@@ -276,7 +283,26 @@ async function build(spec) {
|
|
|
276
283
|
throw e;
|
|
277
284
|
}
|
|
278
285
|
}
|
|
279
|
-
var _BUILD_KEYS = { messageType: 1, messageData: 1, recipient: 1, signer: 1, transactionId: 1, senderNonce: 1 };
|
|
286
|
+
var _BUILD_KEYS = { messageType: 1, messageData: 1, recipient: 1, requestSubject: 1, issuer: 1, signer: 1, transactionId: 1, senderNonce: 1 };
|
|
287
|
+
|
|
288
|
+
function _buildIssuerAndSubject(recipientCertDer, requestSubjectSource, issuerOverride) {
|
|
289
|
+
var subject = guard.bytes.snapshotSource(requestSubjectSource, ScepError, "scep/bad-input", "requestSubject");
|
|
290
|
+
var issuer, issuerNode, subjNode;
|
|
291
|
+
try {
|
|
292
|
+
issuer = issuerOverride != null
|
|
293
|
+
? guard.bytes.snapshotSource(issuerOverride, ScepError, "scep/bad-input", "issuer")
|
|
294
|
+
: schemaX509.parse(recipientCertDer).subject.bytes;
|
|
295
|
+
issuerNode = asn1.decode(issuer);
|
|
296
|
+
subjNode = asn1.decode(subject);
|
|
297
|
+
} catch (e) {
|
|
298
|
+
if (e instanceof ScepError) throw e;
|
|
299
|
+
throw _err("scep/bad-input", "the CertPoll IssuerAndSubject Names are not valid DER (RFC 8894 sec. 3.3.3): " + e.message, e);
|
|
300
|
+
}
|
|
301
|
+
if (!pkix.rootSequenceChildren(issuerNode, 0) || !pkix.rootSequenceChildren(subjNode, 0)) {
|
|
302
|
+
throw _err("scep/bad-input", "the CertPoll issuer and requestSubject must each be a Name (a SEQUENCE) DER (RFC 8894 sec. 3.3.3)");
|
|
303
|
+
}
|
|
304
|
+
return b.sequence([issuer, subject]);
|
|
305
|
+
}
|
|
280
306
|
|
|
281
307
|
/**
|
|
282
308
|
* @primitive pki.scep.parse
|
|
@@ -289,7 +315,7 @@ var _BUILD_KEYS = { messageType: 1, messageData: 1, recipient: 1, signer: 1, tra
|
|
|
289
315
|
* Disassemble a SCEP pkiMessage (RFC 8894 sec. 3): verify the outer SignedData signature, read the
|
|
290
316
|
* transaction attributes BOUND to the verified signer (never from a separate untrusted parse), and,
|
|
291
317
|
* given the recipient key, decrypt the pkcsPKIEnvelope to recover the messageData. It reads the
|
|
292
|
-
* enrollment message types PKCSReq, RenewalReq, and
|
|
318
|
+
* enrollment message types PKCSReq, RenewalReq, CertRep, and CertPoll; the GetCert and GetCRL client
|
|
293
319
|
* queries a CA processes are refused (`scep/unsupported-message-type`). The verdict is
|
|
294
320
|
* `{ signatureValid, signerAuthenticated, signerCert, messageType, transactionId, senderNonce,
|
|
295
321
|
* recipientNonce, pkiStatus, failInfo, failInfoText, messageData, certificates, crls }` (fields null
|
|
@@ -418,6 +444,14 @@ async function parse(bytes, opts) {
|
|
|
418
444
|
} else if (messageType === "PKCSReq" || messageType === "RenewalReq") {
|
|
419
445
|
try { schemaCsr.parse(dec.content); }
|
|
420
446
|
catch (e) { throw _err("scep/bad-request-payload", "a " + messageType + " messageData must be a PKCS#10 CertificationRequest (RFC 8894 sec. 3.3.1)", e); }
|
|
447
|
+
} else if (messageType === "CertPoll") {
|
|
448
|
+
var ias;
|
|
449
|
+
try { ias = asn1.decode(dec.content); }
|
|
450
|
+
catch (e) { throw _err("scep/bad-request-payload", "a CertPoll messageData must be an IssuerAndSubject (RFC 8894 sec. 3.3.3)", e); }
|
|
451
|
+
var iasCh = pkix.rootSequenceChildren(ias, 2, 2);
|
|
452
|
+
if (!iasCh || !pkix.rootSequenceChildren(iasCh[0], 0) || !pkix.rootSequenceChildren(iasCh[1], 0)) {
|
|
453
|
+
throw _err("scep/bad-request-payload", "a CertPoll messageData must be an IssuerAndSubject: a SEQUENCE of two Name values (RFC 8894 sec. 3.3.3)");
|
|
454
|
+
}
|
|
421
455
|
}
|
|
422
456
|
}
|
|
423
457
|
return out;
|
|
@@ -763,6 +797,62 @@ async function getNextCACert(baseUrl, opts) {
|
|
|
763
797
|
function _defaultTransactionId() {
|
|
764
798
|
return nodeCrypto.randomBytes(16).toString("hex");
|
|
765
799
|
}
|
|
800
|
+
var POLL_COUNT_MAX = 1000;
|
|
801
|
+
|
|
802
|
+
function _dispatchCertRep(rep, transactionId, spki) {
|
|
803
|
+
if (rep.messageType !== "CertRep") throw _err("scep/unexpected-message-type", "the SCEP response is a " + rep.messageType + ", not a CertRep");
|
|
804
|
+
if (rep.transactionId !== transactionId) throw _err("scep/transaction-id-mismatch", "the CertRep transactionID does not match the request (RFC 8894 sec. 3.2.1.1)");
|
|
805
|
+
if (rep.pkiStatus === "FAILURE") {
|
|
806
|
+
var failErr = _err("scep/enrollment-failed", "the CA refused the request with failInfo " + JSON.stringify(rep.failInfo) + (rep.failInfoText ? " (" + rep.failInfoText + ")" : ""));
|
|
807
|
+
failErr.failInfo = rep.failInfo;
|
|
808
|
+
failErr.failInfoText = rep.failInfoText;
|
|
809
|
+
throw failErr;
|
|
810
|
+
}
|
|
811
|
+
if (rep.pkiStatus === "PENDING") return { pending: true };
|
|
812
|
+
var repCerts = rep.certificates || [];
|
|
813
|
+
var issued = findIssuedCert(repCerts, spki.bytes);
|
|
814
|
+
if (issued == null) throw _err("scep/no-issued-cert", "the SUCCESS CertRep did not carry a certificate matching the request public key");
|
|
815
|
+
if (findIssuedCert(repCerts.filter(function (c) { return c !== issued; }), spki.bytes) != null) {
|
|
816
|
+
throw _err("scep/ambiguous-issued-cert", "the SUCCESS CertRep carried more than one certificate matching the request public key; the issued certificate is ambiguous (RFC 8894 sec. 3.3)");
|
|
817
|
+
}
|
|
818
|
+
return { pending: false, result: { status: "SUCCESS", certificate: issued, certificates: repCerts, transactionId: rep.transactionId } };
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function _pollExhausted(which, transactionId, senderNonce) {
|
|
822
|
+
var e = _err("scep/poll-exhausted", "the CA kept the request PENDING and " + which + " was exhausted before it resolved (RFC 8894 sec. 4.4); resume the transaction later with this transactionId");
|
|
823
|
+
e.transactionId = transactionId;
|
|
824
|
+
e.senderNonce = senderNonce;
|
|
825
|
+
return e;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
async function _pollForIssuance(ctx) {
|
|
829
|
+
var issuerName;
|
|
830
|
+
try { issuerName = schemaX509.parse(ctx.issuerCert).subject.bytes; }
|
|
831
|
+
catch (e) { throw _err("scep/bad-input", "the issuerCert whose subject names the CertPoll issuer is not a parseable certificate (RFC 8894 sec. 3.3.3)", e); }
|
|
832
|
+
var res = ctx.firstResponse, count = 0, waitedMs = 0, lastNonce = ctx.firstSenderNonce;
|
|
833
|
+
for (;;) {
|
|
834
|
+
var delaySec = 1;
|
|
835
|
+
var ra = _header(res.headers, "retry-after");
|
|
836
|
+
if (typeof ra === "string" && ra.trim() !== "") {
|
|
837
|
+
var parsed = retryAfter.parse(ra, { now: Date.now(), E: _err, code: "scep/bad-input", lenient: true });
|
|
838
|
+
if (parsed.retryAfterSeconds != null) delaySec = parsed.retryAfterSeconds;
|
|
839
|
+
}
|
|
840
|
+
count += 1;
|
|
841
|
+
if (count > ctx.pollCount) throw _pollExhausted("the poll count", ctx.transactionId, lastNonce);
|
|
842
|
+
waitedMs += delaySec * C.TIME.seconds(1);
|
|
843
|
+
if (waitedMs > ctx.maxTotalWait * C.TIME.seconds(1)) throw _pollExhausted("the total wait budget", ctx.transactionId, lastNonce);
|
|
844
|
+
if (ctx.onRetryAfter) ctx.onRetryAfter(delaySec);
|
|
845
|
+
await ctx.sleep(delaySec * C.TIME.seconds(1));
|
|
846
|
+
var sn = nodeCrypto.randomBytes(16);
|
|
847
|
+
lastNonce = sn;
|
|
848
|
+
var certPoll = await build({ messageType: "CertPoll", requestSubject: ctx.csrSubject, issuer: issuerName, recipient: ctx.caCert, signer: ctx.signer, transactionId: ctx.transactionId, senderNonce: sn });
|
|
849
|
+
res = await _client("POST", ctx.postUrl, certPoll, { "content-type": CT_PKI_MESSAGE }, ctx.clientOpts);
|
|
850
|
+
var body = _requireOk(res, [CT_PKI_MESSAGE], "PKIOperation");
|
|
851
|
+
var rep = await parse(body, { signerCert: ctx.signerCert, expectedSenderNonce: sn, recipientKey: ctx.recipientKey });
|
|
852
|
+
var d = _dispatchCertRep(rep, ctx.transactionId, ctx.spki);
|
|
853
|
+
if (!d.pending) return d.result;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
766
856
|
async function _pkiOperation(messageType, baseUrl, opts, verbName) {
|
|
767
857
|
opts = guard.identifier.optionsObject(opts, _err, "scep/bad-input", "pki.scep." + verbName + " options");
|
|
768
858
|
guard.identifier.assertKnownKeys(opts, _ENROLL_KEYS, _err, "scep/bad-input", "pki.scep." + verbName + " has an unknown option: ");
|
|
@@ -772,8 +862,9 @@ async function _pkiOperation(messageType, baseUrl, opts, verbName) {
|
|
|
772
862
|
if (opts.recipientKey == null || typeof opts.recipientKey !== "object") throw _err("scep/bad-input", "a recipientKey { cert, key } is required to decrypt the CA response");
|
|
773
863
|
var caCert = guard.bytes.snapshotSource(opts.caCert, ScepError, "scep/bad-input", "the caCert");
|
|
774
864
|
var signerCert = opts.responderCert != null ? guard.bytes.snapshotSource(opts.responderCert, ScepError, "scep/bad-input", "the responderCert") : caCert;
|
|
865
|
+
var issuerCert = opts.issuerCert != null ? guard.bytes.snapshotSource(opts.issuerCert, ScepError, "scep/bad-input", "the issuerCert") : caCert;
|
|
775
866
|
var recipientKey = _captureRecipientKey(opts.recipientKey);
|
|
776
|
-
var signer = opts.signer;
|
|
867
|
+
var signer = { cert: guard.bytes.snapshotSource(opts.signer.cert, ScepError, "scep/bad-input", "signer.cert"), key: opts.signer.key };
|
|
777
868
|
var clientOpts = {
|
|
778
869
|
transport: opts.transport,
|
|
779
870
|
tls: opts.tls != null ? Object.assign({}, opts.tls) : opts.tls,
|
|
@@ -784,31 +875,28 @@ async function _pkiOperation(messageType, baseUrl, opts, verbName) {
|
|
|
784
875
|
};
|
|
785
876
|
var postUrl = _scepUrl(baseUrl, "PKIOperation");
|
|
786
877
|
var csrView = guard.bytes.snapshotSource(opts.csr, ScepError, "scep/bad-input", "the csr");
|
|
787
|
-
var spki;
|
|
788
|
-
try {
|
|
878
|
+
var spki, csrSubject;
|
|
879
|
+
try { var _pcsr = schemaCsr.parse(csrView); spki = _pcsr.subjectPublicKeyInfo; csrSubject = _pcsr.subject.bytes; }
|
|
789
880
|
catch (e) { throw _err("scep/bad-input", "the csr is not a valid PKCS#10 CertificationRequest", e); }
|
|
881
|
+
var pollCount = guard.limits.cap(opts.pollCount, "pollCount", 5, { E: _err, code: "scep/bad-input", min: 0, max: POLL_COUNT_MAX });
|
|
882
|
+
var maxTotalWait = guard.limits.cap(opts.maxTotalWait, "maxTotalWait", retryAfter.MAX_RETRY_AFTER_SECONDS, { E: _err, code: "scep/bad-input", min: 0, max: retryAfter.MAX_RETRY_AFTER_SECONDS });
|
|
883
|
+
var sleep = typeof opts.sleep === "function" ? opts.sleep : _defaultSleep;
|
|
884
|
+
var onRetryAfter = typeof opts.onRetryAfter === "function" ? opts.onRetryAfter : null;
|
|
790
885
|
var senderNonce = nodeCrypto.randomBytes(16);
|
|
791
886
|
var transactionId = opts.transactionId != null ? opts.transactionId : _defaultTransactionId();
|
|
792
887
|
var reqMsg = await build({ messageType: messageType, messageData: csrView, recipient: caCert, signer: signer, transactionId: transactionId, senderNonce: senderNonce });
|
|
793
888
|
var res = await _client("POST", postUrl, reqMsg, { "content-type": CT_PKI_MESSAGE }, clientOpts);
|
|
794
889
|
var body = _requireOk(res, [CT_PKI_MESSAGE], "PKIOperation");
|
|
795
890
|
var rep = await parse(body, { signerCert: signerCert, expectedSenderNonce: senderNonce, recipientKey: recipientKey });
|
|
796
|
-
|
|
797
|
-
if (
|
|
798
|
-
if (
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
var repCerts = rep.certificates || [];
|
|
806
|
-
var issued = findIssuedCert(repCerts, spki.bytes);
|
|
807
|
-
if (issued == null) throw _err("scep/no-issued-cert", "the SUCCESS CertRep did not carry a certificate matching the request public key");
|
|
808
|
-
if (findIssuedCert(repCerts.filter(function (c) { return c !== issued; }), spki.bytes) != null) {
|
|
809
|
-
throw _err("scep/ambiguous-issued-cert", "the SUCCESS CertRep carried more than one certificate matching the request public key; the issued certificate is ambiguous (RFC 8894 sec. 3.3)");
|
|
810
|
-
}
|
|
811
|
-
return { status: "SUCCESS", certificate: issued, certificates: repCerts, transactionId: rep.transactionId };
|
|
891
|
+
var d = _dispatchCertRep(rep, transactionId, spki);
|
|
892
|
+
if (!d.pending) return d.result;
|
|
893
|
+
if (pollCount <= 0) return { status: "PENDING", transactionId: rep.transactionId, senderNonce: senderNonce };
|
|
894
|
+
return _pollForIssuance({
|
|
895
|
+
firstResponse: res, firstSenderNonce: senderNonce, postUrl: postUrl, clientOpts: clientOpts,
|
|
896
|
+
caCert: caCert, csrSubject: csrSubject, issuerCert: issuerCert, signer: signer, signerCert: signerCert, recipientKey: recipientKey,
|
|
897
|
+
spki: spki, transactionId: transactionId, pollCount: pollCount, maxTotalWait: maxTotalWait,
|
|
898
|
+
sleep: sleep, onRetryAfter: onRetryAfter,
|
|
899
|
+
});
|
|
812
900
|
}
|
|
813
901
|
|
|
814
902
|
/**
|
|
@@ -824,8 +912,13 @@ async function _pkiOperation(messageType, baseUrl, opts, verbName) {
|
|
|
824
912
|
* the CertRep. The CA response is authenticated against the CA certificate and its `recipientNonce` must
|
|
825
913
|
* echo the request's fresh `senderNonce`. On SUCCESS the issued certificate is selected out of the
|
|
826
914
|
* response by SubjectPublicKeyInfo byte-match against the request; a FAILURE throws
|
|
827
|
-
* `scep/enrollment-failed` carrying the CA's failInfo
|
|
828
|
-
*
|
|
915
|
+
* `scep/enrollment-failed` carrying the CA's failInfo. A CA that answers PENDING (manual enrollment, RFC
|
|
916
|
+
* 8894 sec. 2.5) is polled automatically: the client issues CertPoll (GetCertInitial) messages reusing
|
|
917
|
+
* the transactionID, each authenticated against the CA certificate with a fresh nonce it must echo,
|
|
918
|
+
* until the CA answers SUCCESS or FAILURE or a `pollCount` / `maxTotalWait` budget is exhausted
|
|
919
|
+
* (`scep/poll-exhausted`, carrying the `transactionId` to resume). Pass `pollCount: 0` to disable polling
|
|
920
|
+
* and get the single `{ status: "PENDING", transactionId }` return for a caller that drives the retry
|
|
921
|
+
* itself. The content key is transported under RSAES-OAEP, so the CA
|
|
829
922
|
* certificate must be an RSA `keyEncipherment` certificate that accepts OAEP; the shared-secret
|
|
830
923
|
* authenticator (RFC 8894 sec. 2.4) is the `challengePassword` the caller placed in the PKCS#10.
|
|
831
924
|
*
|
|
@@ -835,14 +928,19 @@ async function _pkiOperation(messageType, baseUrl, opts, verbName) {
|
|
|
835
928
|
* - `signer` -- `{ cert, key }`, the outer SignedData signer (for a PKCSReq, a self-signed certificate over the request key).
|
|
836
929
|
* - `recipientKey` -- `{ cert, key }`, the RSA key the CA encrypts the response to, to decrypt the CertRep.
|
|
837
930
|
* - `responderCert` -- the certificate to authenticate the CA's response signature against (defaults to `caCert`).
|
|
931
|
+
* - `issuerCert` -- the issuing CA certificate whose subject names the CertPoll issuer (defaults to `caCert`; set it when the request is encrypted to a separate RA certificate, RFC 8894 sec. 3.3.3).
|
|
838
932
|
* - `transactionId` -- a caller-unique PrintableString (defaults to 16 random bytes, hex-encoded, unique per call).
|
|
933
|
+
* - `pollCount` -- the maximum number of CertPoll requests on a PENDING response (default 5; `0` disables polling).
|
|
934
|
+
* - `maxTotalWait` -- the total Retry-After sleep budget in seconds across all polls (default the Retry-After horizon).
|
|
935
|
+
* - `sleep` -- an injectable sleeper `(ms) -> Promise` for the Retry-After wait (default a real timer).
|
|
936
|
+
* - `onRetryAfter` -- an optional observer `(delaySeconds) -> void` called before each poll sleep.
|
|
839
937
|
* - `transport` / `timeout` / `maxResponseBytes` / `maxRedirects` / `tls` / `allowCrossOriginRedirect` -- transport bounds.
|
|
840
938
|
* @example
|
|
841
939
|
* // requires: scepCsr, caCertDer, clientCertDer, clientKeyPkcs8, scepTransport
|
|
842
940
|
* var out = await pki.scep.enroll("http://ca.example/scep", { csr: scepCsr, caCert: caCertDer,
|
|
843
941
|
* signer: { cert: clientCertDer, key: clientKeyPkcs8 },
|
|
844
942
|
* recipientKey: { cert: clientCertDer, key: clientKeyPkcs8 }, transport: scepTransport });
|
|
845
|
-
* out.status; // "SUCCESS"
|
|
943
|
+
* out.status; // "SUCCESS" (a PENDING request is polled to a terminal status unless pollCount is 0)
|
|
846
944
|
*/
|
|
847
945
|
function enroll(baseUrl, opts) { return _pkiOperation("PKCSReq", baseUrl, opts, "enroll"); }
|
|
848
946
|
|
|
@@ -874,7 +972,7 @@ function _withKeys(extra) { var o = {}, k; for (k in _TRANSPORT_KEYS) o[k] = 1;
|
|
|
874
972
|
var _CACAPS_KEYS = _withKeys({ expectSCEPStandard: 1, requireStrongProfile: 1 });
|
|
875
973
|
var _CACERT_KEYS = _withKeys({ expectedFingerprint: 1, fingerprintAlgorithm: 1 });
|
|
876
974
|
var _NEXTCACERT_KEYS = _withKeys({ caCertificate: 1 });
|
|
877
|
-
var _ENROLL_KEYS = _withKeys({ csr: 1, caCert: 1, signer: 1, recipientKey: 1, responderCert: 1, transactionId: 1 });
|
|
975
|
+
var _ENROLL_KEYS = _withKeys({ csr: 1, caCert: 1, signer: 1, recipientKey: 1, responderCert: 1, issuerCert: 1, transactionId: 1, pollCount: 1, maxTotalWait: 1, sleep: 1, onRetryAfter: 1 });
|
|
878
976
|
|
|
879
977
|
module.exports = {
|
|
880
978
|
build: build,
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:e63bdf7d-807a-4e39-8cb7-7eb6a47b5808",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-09-
|
|
8
|
+
"timestamp": "2026-09-01T10:42:10.477Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/pki@0.6.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.6.16",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.6.
|
|
25
|
+
"version": "0.6.16",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/pki@0.6.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.6.16",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/pki@0.6.
|
|
57
|
+
"ref": "@blamejs/pki@0.6.16",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|