@blamejs/pki 0.6.14 → 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 +27 -1
- package/lib/acme.js +59 -815
- package/lib/asn1-der.js +7 -245
- package/lib/attrcert-sign.js +12 -260
- package/lib/attrcert-verify.js +1 -7
- package/lib/byte-reader.js +0 -8
- package/lib/byte-writer.js +0 -11
- package/lib/cbor-det.js +0 -54
- package/lib/cmc-build.js +15 -357
- package/lib/cmc-verify.js +6 -239
- package/lib/cmp-build.js +41 -317
- package/lib/cmp-session.js +73 -760
- package/lib/cmp-verify.js +32 -488
- package/lib/cms-compress.js +3 -32
- package/lib/cms-decrypt.js +29 -294
- package/lib/cms-encrypt.js +14 -234
- package/lib/cms-sign.js +29 -324
- package/lib/cms-verify.js +9 -500
- package/lib/composite-kem.js +2 -102
- package/lib/composite-sig.js +1 -77
- package/lib/constants.js +0 -249
- package/lib/crl-sign.js +9 -237
- package/lib/crl-verify.js +1 -10
- package/lib/crmf-sign.js +19 -198
- package/lib/crmf-verify.js +1 -12
- package/lib/csr-sign.js +3 -34
- package/lib/csr-verify.js +1 -12
- package/lib/ct.js +28 -253
- package/lib/edwards-point.js +10 -44
- package/lib/est.js +28 -764
- package/lib/framework-error.js +4 -182
- package/lib/guard-all.js +1 -58
- package/lib/guard-async.js +1 -36
- package/lib/guard-bytes.js +6 -650
- package/lib/guard-compress.js +5 -102
- package/lib/guard-crypto.js +1 -40
- package/lib/guard-der.js +2 -93
- package/lib/guard-encoding.js +2 -60
- package/lib/guard-header.js +1 -36
- package/lib/guard-identifier.js +7 -531
- package/lib/guard-intrinsic.js +1 -150
- package/lib/guard-json.js +10 -73
- package/lib/guard-limits.js +1 -77
- package/lib/guard-list.js +1 -49
- package/lib/guard-name.js +3 -209
- package/lib/guard-parsed.js +7 -335
- package/lib/guard-range.js +1 -71
- package/lib/guard-secret.js +1 -102
- package/lib/guard-text.js +1 -64
- package/lib/guard-time.js +1 -97
- package/lib/hpke.js +3 -105
- package/lib/http-digest.js +14 -142
- package/lib/http-retry-after.js +1 -47
- package/lib/http-transport.js +21 -131
- package/lib/inspect.js +30 -284
- package/lib/ip-utils.js +6 -47
- package/lib/jose.js +7 -142
- package/lib/key.js +2 -67
- package/lib/lint.js +9 -194
- package/lib/merkle.js +1 -27
- package/lib/mime.js +9 -82
- package/lib/ocsp-request-verify.js +1 -14
- package/lib/ocsp-verify.js +1 -82
- package/lib/ocsp.js +11 -169
- package/lib/oid.js +5 -337
- package/lib/path-validate.js +103 -1492
- package/lib/pbes2.js +2 -89
- package/lib/pkcs12-build.js +31 -379
- package/lib/pki-build.js +64 -290
- package/lib/rc2.js +2 -18
- package/lib/rfc3339.js +4 -27
- package/lib/scep.js +140 -286
- package/lib/schema-all.js +0 -122
- package/lib/schema-attrcert.js +4 -146
- package/lib/schema-c509.js +153 -802
- package/lib/schema-cmc.js +5 -220
- package/lib/schema-cmp.js +2 -223
- package/lib/schema-cms.js +13 -427
- package/lib/schema-crl.js +5 -68
- package/lib/schema-crmf.js +6 -167
- package/lib/schema-csr.js +1 -54
- package/lib/schema-csrattrs.js +2 -87
- package/lib/schema-engine.js +0 -152
- package/lib/schema-ocsp.js +0 -117
- package/lib/schema-pkcs12.js +2 -93
- package/lib/schema-pkcs8.js +3 -35
- package/lib/schema-pkix.js +24 -655
- package/lib/schema-smime.js +0 -54
- package/lib/schema-tsp.js +3 -99
- package/lib/schema-x509.js +4 -63
- package/lib/shbs.js +11 -95
- package/lib/sign-scheme.js +2 -97
- package/lib/sigstore.js +9 -169
- package/lib/sleep.js +2 -8
- package/lib/smime.js +15 -408
- package/lib/tls-cert-compress.js +0 -62
- package/lib/trust.js +19 -185
- package/lib/tsp-sign.js +16 -270
- package/lib/validator-all.js +1 -30
- package/lib/validator-attcert.js +2 -46
- package/lib/validator-cose.js +2 -153
- package/lib/validator-keydesc.js +1 -37
- package/lib/validator-sig.js +1 -46
- package/lib/validator-tls.js +1 -88
- package/lib/validator-tpm.js +19 -122
- package/lib/verify-seam.js +1 -24
- package/lib/webauthn-mds.js +3 -400
- package/lib/webauthn.js +12 -833
- package/lib/webcrypto.js +5 -485
- package/lib/x509-sign.js +4 -108
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/scep.js
CHANGED
|
@@ -43,8 +43,8 @@ var cmsSign = require("./cms-sign");
|
|
|
43
43
|
var cmsVerify = require("./cms-verify");
|
|
44
44
|
var cmsEncrypt = require("./cms-encrypt");
|
|
45
45
|
var cmsDecrypt = require("./cms-decrypt");
|
|
46
|
-
require("./path-validate");
|
|
47
|
-
var csrVerify = require("./csr-verify");
|
|
46
|
+
require("./path-validate");
|
|
47
|
+
var csrVerify = require("./csr-verify");
|
|
48
48
|
var schemaCms = require("./schema-cms");
|
|
49
49
|
var schemaCsr = require("./schema-csr");
|
|
50
50
|
var schemaX509 = require("./schema-x509");
|
|
@@ -52,42 +52,28 @@ var schemaCrl = require("./schema-crl");
|
|
|
52
52
|
var pkix = require("./schema-pkix");
|
|
53
53
|
var guard = require("./guard-all");
|
|
54
54
|
var C = require("./constants");
|
|
55
|
-
var httpTransport = require("./http-transport");
|
|
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); }
|
|
59
61
|
|
|
60
|
-
// The namespace pkix.keyUsageOf reads a certificate's keyUsage extension through.
|
|
61
62
|
var _KU_NS = pkix.makeNS("scep", ScepError, oid);
|
|
62
63
|
|
|
63
|
-
// The messageType values are a DATA table (RFC 8894 sec. 3.2.1.2, Table 3), never a switch: name <-> the
|
|
64
|
-
// decimal ASCII a PrintableString carries. RFC 8894 renumbered the legacy draft (RenewalReq is 17, and
|
|
65
|
-
// there is no GetCertInitial -- it is CertPoll 20).
|
|
66
64
|
var MESSAGE_TYPE = { CertRep: "3", RenewalReq: "17", PKCSReq: "19", CertPoll: "20", GetCert: "21", GetCRL: "22" };
|
|
67
|
-
// pkiStatus (Table 4): SUCCESS 0, FAILURE 2, PENDING 3 -- note there is NO value 1.
|
|
68
65
|
var PKI_STATUS = { SUCCESS: "0", FAILURE: "2", PENDING: "3" };
|
|
69
|
-
// failInfo (Table 5).
|
|
70
66
|
var FAIL_INFO = { badAlg: "0", badMessageCheck: "1", badRequest: "2", badTime: "3", badCertId: "4" };
|
|
71
67
|
|
|
72
|
-
// name -> code and code -> name, built once from each table so a lookup is a data read, not a switch.
|
|
73
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; }
|
|
74
69
|
var MESSAGE_TYPE_BY_CODE = _invert(MESSAGE_TYPE), PKI_STATUS_BY_CODE = _invert(PKI_STATUS), FAIL_INFO_BY_CODE = _invert(FAIL_INFO);
|
|
75
70
|
|
|
76
|
-
|
|
77
|
-
// response it receives. The CertPoll / GetCert / GetCRL client queries are refused (see parse).
|
|
78
|
-
var _PARSE_SUPPORTED = { PKCSReq: 1, RenewalReq: 1, CertRep: 1 };
|
|
71
|
+
var _PARSE_SUPPORTED = { PKCSReq: 1, RenewalReq: 1, CertRep: 1, CertPoll: 1 };
|
|
79
72
|
|
|
80
|
-
// The RFC 8894 sec. 3 EnvelopedData content-encryption profile. AES-128-CBC is the mandatory strong
|
|
81
|
-
// cipher; the toolkit's own cms.encrypt refuses the weak legacy ciphers structurally, so the builder
|
|
82
|
-
// only names the one it emits.
|
|
83
73
|
var ENVELOPE_CIPHER = "aes-128-cbc";
|
|
84
74
|
|
|
85
|
-
// The empty external content a conforming FAILURE / PENDING CertRep is verified against: its
|
|
86
|
-
// pkcsPKIEnvelope is omitted, so the SignedData carries no encapsulated content (RFC 8894 sec. 3.3.2).
|
|
87
75
|
var EMPTY_CONTENT = Buffer.alloc(0);
|
|
88
76
|
|
|
89
|
-
// A 16-byte nonce (RFC 8894 sec. 3.2.1.5): a caller value copied at the door and validated to width, or
|
|
90
|
-
// a fresh random one. The copy makes a caller value read after an await immune to concurrent mutation.
|
|
91
77
|
function _nonce(v, label) {
|
|
92
78
|
if (v == null) return nodeCrypto.randomBytes(16);
|
|
93
79
|
var buf = guard.bytes.snapshotSource(v, ScepError, "scep/bad-nonce", label);
|
|
@@ -95,19 +81,12 @@ function _nonce(v, label) {
|
|
|
95
81
|
return buf;
|
|
96
82
|
}
|
|
97
83
|
|
|
98
|
-
// A caller PrintableString for a transaction id: bounded (the RFC sets no length, so cap it against a DoS)
|
|
99
|
-
// and restricted to the PrintableString character set (asn1.build.printable enforces the set).
|
|
100
84
|
function _transactionId(v) {
|
|
101
85
|
if (typeof v !== "string" || v.length === 0) throw _err("scep/bad-input", "transactionId must be a non-empty string (RFC 8894 sec. 3.2.1.1)");
|
|
102
86
|
if (v.length > C.LIMITS.SCEP_TRANSACTION_ID_MAX) throw _err("scep/bad-input", "transactionId exceeds the " + C.LIMITS.SCEP_TRANSACTION_ID_MAX + "-character cap");
|
|
103
87
|
return v;
|
|
104
88
|
}
|
|
105
89
|
|
|
106
|
-
// Assert some bytes are a CMS ContentInfo wrapping a well-formed EnvelopedData whose encrypted content
|
|
107
|
-
// type is id-data -- the pkcsPKIEnvelope profile (RFC 8894 sec. 3.2, sec. 3.2.2). Delegated to the
|
|
108
|
-
// strict CMS parser, and checked structurally without decryption, so an absent, malformed, or
|
|
109
|
-
// off-profile envelope is refused for every envelope-carrying message whether or not a recipient key
|
|
110
|
-
// was supplied.
|
|
111
90
|
function _assertValidEnvelope(bytes, messageType) {
|
|
112
91
|
if (bytes == null) throw _err("scep/missing-envelope", "the pkcsPKIEnvelope is mandatory for a " + messageType + " message but the signed content is absent (RFC 8894 sec. 3.2)");
|
|
113
92
|
var parsed;
|
|
@@ -115,17 +94,10 @@ function _assertValidEnvelope(bytes, messageType) {
|
|
|
115
94
|
catch (e) { throw _err("scep/missing-envelope", "the pkcsPKIEnvelope of a " + messageType + " message is not a decodable CMS EnvelopedData (RFC 8894 sec. 3.2)", e); }
|
|
116
95
|
if (parsed.contentTypeName !== "envelopedData") throw _err("scep/missing-envelope", "the pkcsPKIEnvelope of a " + messageType + " message must be an EnvelopedData (RFC 8894 sec. 3.2), got " + parsed.contentTypeName);
|
|
117
96
|
if (parsed.encryptedContentInfo.contentType !== oid.byName("data")) throw _err("scep/bad-envelope-content-type", "the pkcsPKIEnvelope's encrypted content type must be id-data (RFC 8894 sec. 3.2.2), got " + parsed.encryptedContentInfo.contentType);
|
|
118
|
-
// The EnvelopedData must actually carry ciphertext: an inner envelope whose encryptedContent is absent
|
|
119
|
-
// or a zero-length OCTET STRING has no messageData to recover (SCEP provides none out of band), so it
|
|
120
|
-
// is refused (RFC 8894 sec. 3.2).
|
|
121
97
|
var ct = parsed.encryptedContentInfo.encryptedContent;
|
|
122
98
|
if (ct == null || ct.length === 0) throw _err("scep/missing-envelope", "the pkcsPKIEnvelope of a " + messageType + " message carries no encrypted content (RFC 8894 sec. 3.2)");
|
|
123
99
|
}
|
|
124
100
|
|
|
125
|
-
// Validate a SUCCESS CertRep's messageData as a degenerate certificates-only CMS SignedData (RFC 8894
|
|
126
|
-
// sec. 3.4) and surface its certificates and CRLs raw, for the caller to path-validate. The issued
|
|
127
|
-
// certificate(s) answer an enrollment; a CRL answers a GetCRL (sec. 3.3.4), so certificates and CRLs are
|
|
128
|
-
// accepted independently and a payload carrying neither is refused.
|
|
129
101
|
function _readIssuancePayload(der) {
|
|
130
102
|
var r;
|
|
131
103
|
try { r = schemaCms.parse(der); }
|
|
@@ -134,9 +106,6 @@ function _readIssuancePayload(der) {
|
|
|
134
106
|
r.encapContentInfo.eContent !== null || r.signerInfos.length !== 0) {
|
|
135
107
|
throw _err("scep/not-certs-only", "a SUCCESS CertRep messageData must be a degenerate certificates-only CMS SignedData (RFC 8894 sec. 3.4)");
|
|
136
108
|
}
|
|
137
|
-
// Validate each entry as a plain X.509 certificate / CRL: a universal-SEQUENCE CertificateChoice or
|
|
138
|
-
// RevocationInfoChoice must be a well-formed certificate / CRL, and a tagged alternative (attribute
|
|
139
|
-
// cert, other-message, otherRevInfo) is not a plain issuance object and is refused.
|
|
140
109
|
var certs = [], crls = [], i;
|
|
141
110
|
var rc = r.certificates || [], rl = r.crls || [];
|
|
142
111
|
for (i = 0; i < rc.length; i++) {
|
|
@@ -157,14 +126,8 @@ function _readIssuancePayload(der) {
|
|
|
157
126
|
return { certificates: certs, crls: crls };
|
|
158
127
|
}
|
|
159
128
|
|
|
160
|
-
// Build one SCEP transaction attribute { type, values:[valueDER] } for cms.sign's additionalSignedAttributes.
|
|
161
129
|
function _attr(name, valueDer) { return { type: oid.byName(name), values: [valueDer] }; }
|
|
162
130
|
|
|
163
|
-
// Find the single surfaced signed attribute of a given OID name (the values are raw AttributeValue
|
|
164
|
-
// DER). The CMS parser rejects a repeated attribute type (cms/duplicate-signed-attr) before these
|
|
165
|
-
// attributes are surfaced, so a SCEP transaction attribute appears at most once. It is also
|
|
166
|
-
// single-valued: a value SET that does not hold exactly one value is an ambiguous transaction state
|
|
167
|
-
// and is refused rather than resolved to the first value (RFC 8894 sec. 3.2.1).
|
|
168
131
|
function _find(attrs, name) {
|
|
169
132
|
var want = oid.byName(name);
|
|
170
133
|
for (var i = 0; i < attrs.length; i++) {
|
|
@@ -176,9 +139,6 @@ function _find(attrs, name) {
|
|
|
176
139
|
}
|
|
177
140
|
return null;
|
|
178
141
|
}
|
|
179
|
-
// Read a string transaction attribute to its JS string, requiring the exact ASN.1 string type RFC 8894
|
|
180
|
-
// sec. 3.2 pins for it (a generic string reader would accept an off-profile UTF8String or IA5String
|
|
181
|
-
// where a PrintableString is required). Returns null when the attribute is absent.
|
|
182
142
|
function _readStr(attrs, name, expectedTag) {
|
|
183
143
|
var a = _find(attrs, name);
|
|
184
144
|
if (a == null) return null;
|
|
@@ -192,7 +152,6 @@ function _readStr(attrs, name, expectedTag) {
|
|
|
192
152
|
catch (e) { throw _err("scep/bad-attribute", "the " + name + " transaction attribute is not a readable string", e); }
|
|
193
153
|
}
|
|
194
154
|
var _T_PRINTABLE = asn1.TAGS.PRINTABLE_STRING, _T_UTF8 = asn1.TAGS.UTF8_STRING;
|
|
195
|
-
// Read a 16-byte OCTET STRING nonce attribute, or null when absent.
|
|
196
155
|
function _readNonce(attrs, name) {
|
|
197
156
|
var a = _find(attrs, name);
|
|
198
157
|
if (a == null) return null;
|
|
@@ -203,9 +162,6 @@ function _readNonce(attrs, name) {
|
|
|
203
162
|
return n;
|
|
204
163
|
}
|
|
205
164
|
|
|
206
|
-
// Whether two certificates carry the same subject public key -- the SCEP signer-authentication test
|
|
207
|
-
// (a CertRep must be signed by the CA the client already holds, RFC 8894 sec. 3.1). Compared by the
|
|
208
|
-
// SubjectPublicKeyInfo so a re-encoded certificate of the same key still matches.
|
|
209
165
|
function _sameKey(certA, certB) {
|
|
210
166
|
var a, bb;
|
|
211
167
|
try { a = schemaX509.parse(certA).subjectPublicKeyInfo.bytes; bb = schemaX509.parse(certB).subjectPublicKeyInfo.bytes; }
|
|
@@ -213,9 +169,6 @@ function _sameKey(certA, certB) {
|
|
|
213
169
|
return guard.crypto.constantTimeEqual(a, bb);
|
|
214
170
|
}
|
|
215
171
|
|
|
216
|
-
// The pkcsPKIEnvelope uses RSAES-OAEP key transport, so the recipient MUST be an RSA key-transport
|
|
217
|
-
// certificate (RFC 8894 sec. 3). An EC / X25519 / ML-KEM recipient would make cms.encrypt emit a
|
|
218
|
-
// key-agreement or KEM recipient off the SCEP profile, so it is refused before the CMS dispatch.
|
|
219
172
|
function _assertRsaRecipient(recipientDer) {
|
|
220
173
|
var spki;
|
|
221
174
|
try { spki = schemaX509.parse(recipientDer).subjectPublicKeyInfo; }
|
|
@@ -225,10 +178,6 @@ function _assertRsaRecipient(recipientDer) {
|
|
|
225
178
|
}
|
|
226
179
|
}
|
|
227
180
|
|
|
228
|
-
// A SCEP signer's certificate MUST be authorized to sign: its keyUsage extension, when present, must
|
|
229
|
-
// assert digitalSignature (RFC 8894 sec. 2.3 requires that bit specifically). A certificate whose
|
|
230
|
-
// keyUsage omits it, permitting only encryption or non-repudiation, is refused, so a signature it makes
|
|
231
|
-
// is not read as authenticated. A certificate with no keyUsage extension is unrestricted and passes.
|
|
232
181
|
function _assertSigningKeyUsage(certDer, label) {
|
|
233
182
|
var cert;
|
|
234
183
|
try { cert = schemaX509.parse(certDer); }
|
|
@@ -239,9 +188,6 @@ function _assertSigningKeyUsage(certDer, label) {
|
|
|
239
188
|
}
|
|
240
189
|
}
|
|
241
190
|
|
|
242
|
-
// Snapshot the recipient key material at the parse door: the certificate byte source is copied so a
|
|
243
|
-
// concurrent mutation cannot redirect the decryption, and the private / content key is held by
|
|
244
|
-
// reference (copying a secret is the worse defect). null passes through unchanged.
|
|
245
191
|
function _captureRecipientKey(rk) {
|
|
246
192
|
if (rk == null) return null;
|
|
247
193
|
rk = guard.identifier.optionsObject(rk, _err, "scep/bad-input", "opts.recipientKey");
|
|
@@ -295,33 +241,29 @@ async function build(spec) {
|
|
|
295
241
|
guard.identifier.assertKnownKeys(spec, _BUILD_KEYS, _err, "scep/bad-input", "pki.scep.build has an unknown option: ");
|
|
296
242
|
var mtCode = MESSAGE_TYPE[spec.messageType];
|
|
297
243
|
if (mtCode == null) throw _err("scep/bad-message-type", "unknown messageType " + JSON.stringify(spec.messageType) + " (RFC 8894 sec. 3.2.1.2)");
|
|
298
|
-
if (spec.messageType !== "PKCSReq" && spec.messageType !== "RenewalReq") {
|
|
299
|
-
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");
|
|
300
246
|
}
|
|
301
247
|
if (spec.signer == null || typeof spec.signer !== "object") throw _err("scep/bad-input", "a signer { cert, key } is required");
|
|
302
248
|
var senderNonce = _nonce(spec.senderNonce, "senderNonce");
|
|
303
249
|
var txnId = _transactionId(spec.transactionId);
|
|
304
|
-
// The messageData is a PKCS#10 CertificationRequest whose self-signature is its proof-of-possession
|
|
305
|
-
// (RFC 8894 sec. 3.3.1, over RFC 2986). Verify the structure and that proof before enveloping it, so
|
|
306
|
-
// arbitrary bytes or a request whose signature does not verify under its own subject public key are
|
|
307
|
-
// refused here rather than encrypted into a message a CA would reject.
|
|
308
|
-
// Capture every input read after an await at this synchronous door, so a caller that mutates or reuses
|
|
309
|
-
// the spec while verification or encryption is pending cannot desync what was verified from what is
|
|
310
|
-
// emitted, or substitute the recipient a request is encrypted to. The certificate byte sources are
|
|
311
|
-
// copied; the signer private key is held by reference (copying a secret is the worse defect).
|
|
312
|
-
var messageData = guard.bytes.snapshotSource(spec.messageData, ScepError, "scep/bad-input", "messageData");
|
|
313
250
|
var recipient = guard.bytes.snapshotSource(spec.recipient, ScepError, "scep/bad-input", "recipient");
|
|
314
251
|
_assertRsaRecipient(recipient);
|
|
315
252
|
var signerCert = guard.bytes.snapshotSource(spec.signer.cert, ScepError, "scep/bad-input", "signer.cert");
|
|
316
253
|
_assertSigningKeyUsage(signerCert, "signer certificate");
|
|
317
254
|
var signerKey = spec.signer.key;
|
|
318
|
-
var
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
+
}
|
|
323
266
|
}
|
|
324
|
-
// Inner EnvelopedData: encrypt the PKCS#10 to the recipient CA, AES-128-CBC (RFC 8894 sec. 3).
|
|
325
267
|
var pkcsPKIEnvelope;
|
|
326
268
|
try {
|
|
327
269
|
pkcsPKIEnvelope = await cmsEncrypt.encrypt(messageData, [{ cert: recipient }], { contentEncryptionAlgorithm: ENVELOPE_CIPHER });
|
|
@@ -329,7 +271,6 @@ async function build(spec) {
|
|
|
329
271
|
if (e && e.isPkiError) throw _err("scep/bad-input", "the pkcsPKIEnvelope could not be built (recipient must be a keyEncipherment CA certificate and messageData a PKCS#10): " + e.message, e);
|
|
330
272
|
throw e;
|
|
331
273
|
}
|
|
332
|
-
// Outer SignedData over the envelope, carrying the transaction attributes as authenticated attributes.
|
|
333
274
|
var attrs = [
|
|
334
275
|
_attr("scepMessageType", b.printable(mtCode)),
|
|
335
276
|
_attr("scepTransactionId", b.printable(txnId)),
|
|
@@ -342,7 +283,26 @@ async function build(spec) {
|
|
|
342
283
|
throw e;
|
|
343
284
|
}
|
|
344
285
|
}
|
|
345
|
-
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
|
+
}
|
|
346
306
|
|
|
347
307
|
/**
|
|
348
308
|
* @primitive pki.scep.parse
|
|
@@ -355,7 +315,7 @@ var _BUILD_KEYS = { messageType: 1, messageData: 1, recipient: 1, signer: 1, tra
|
|
|
355
315
|
* Disassemble a SCEP pkiMessage (RFC 8894 sec. 3): verify the outer SignedData signature, read the
|
|
356
316
|
* transaction attributes BOUND to the verified signer (never from a separate untrusted parse), and,
|
|
357
317
|
* given the recipient key, decrypt the pkcsPKIEnvelope to recover the messageData. It reads the
|
|
358
|
-
* enrollment message types PKCSReq, RenewalReq, and
|
|
318
|
+
* enrollment message types PKCSReq, RenewalReq, CertRep, and CertPoll; the GetCert and GetCRL client
|
|
359
319
|
* queries a CA processes are refused (`scep/unsupported-message-type`). The verdict is
|
|
360
320
|
* `{ signatureValid, signerAuthenticated, signerCert, messageType, transactionId, senderNonce,
|
|
361
321
|
* recipientNonce, pkiStatus, failInfo, failInfoText, messageData, certificates, crls }` (fields null
|
|
@@ -386,24 +346,13 @@ var _BUILD_KEYS = { messageType: 1, messageData: 1, recipient: 1, signer: 1, tra
|
|
|
386
346
|
async function parse(bytes, opts) {
|
|
387
347
|
opts = guard.identifier.optionsObject(opts, _err, "scep/bad-input", "pki.scep.parse options");
|
|
388
348
|
guard.identifier.assertKnownKeys(opts, _PARSE_KEYS, _err, "scep/bad-input", "pki.scep.parse has an unknown option: ");
|
|
389
|
-
// Snapshot the message bytes once at this synchronous door: parse verifies them twice (a detached
|
|
390
|
-
// FAILURE / PENDING is re-verified against an empty payload), so a caller that swaps a mutable Buffer
|
|
391
|
-
// between the two attempts must not be able to return one message's attributes for another.
|
|
392
349
|
var msgBytes = guard.bytes.snapshotSource(bytes, ScepError, "scep/bad-der", "pkiMessage");
|
|
393
|
-
// Capture every option read after the awaited verification at this synchronous door, so a caller that
|
|
394
|
-
// mutates or reuses opts while verification is pending cannot swap the authenticated signer, the echo
|
|
395
|
-
// nonce, or the decryption recipient after the checks that depend on them have started. Certificate
|
|
396
|
-
// byte sources are copied; the recipient private key is held by reference.
|
|
397
350
|
var expectedSignerCert = opts.signerCert != null ? guard.bytes.snapshotSource(opts.signerCert, ScepError, "scep/bad-input", "opts.signerCert") : null;
|
|
398
351
|
var expectedSenderNonce = opts.expectedSenderNonce != null ? _nonce(opts.expectedSenderNonce, "expectedSenderNonce") : null;
|
|
399
352
|
var recipientKey = _captureRecipientKey(opts.recipientKey);
|
|
400
353
|
var v;
|
|
401
354
|
try { v = await cmsVerify.verify(msgBytes); }
|
|
402
355
|
catch (e) {
|
|
403
|
-
// A conforming FAILURE or PENDING CertRep omits the pkcsPKIEnvelope, so the SignedData has no
|
|
404
|
-
// encapsulated content and cms.verify reports it as detached. Re-verify against an empty payload,
|
|
405
|
-
// which the signature accepts only when the message-digest was taken over no content; a message
|
|
406
|
-
// signed over any other bytes still fails (RFC 8894 sec. 3.3.2).
|
|
407
356
|
if (e && e.code === "cms/detached-content-required") {
|
|
408
357
|
try { v = await cmsVerify.verify(msgBytes, { content: EMPTY_CONTENT }); }
|
|
409
358
|
catch (e2) {
|
|
@@ -415,29 +364,12 @@ async function parse(bytes, opts) {
|
|
|
415
364
|
} else { throw e; }
|
|
416
365
|
}
|
|
417
366
|
if (v.signers.length !== 1) throw _err("scep/bad-signer", "a SCEP pkiMessage MUST carry exactly one SignerInfo (RFC 8894 sec. 3.1), got " + v.signers.length);
|
|
418
|
-
// Fail closed: the transaction attributes are only trustworthy once the outer signature verifies.
|
|
419
|
-
// cms.verify returns a per-signer verdict rather than throwing, so a bad signature MUST be refused
|
|
420
|
-
// here before any attribute is read -- never surfaced as a false verdict alongside the attributes.
|
|
421
367
|
if (v.valid !== true) throw _err("scep/bad-signature", "the pkiMessage outer signature did not verify (RFC 8894 sec. 3.1)");
|
|
422
|
-
// The SignedData eContentType MUST be id-data (RFC 8894 sec. 3.2): a message that verifies as CMS but
|
|
423
|
-
// names another encapsulated content type is not a valid SCEP pkiMessage.
|
|
424
368
|
if (v.eContentType !== oid.byName("data")) throw _err("scep/bad-content-type", "the pkiMessage eContentType must be id-data (RFC 8894 sec. 3.2)");
|
|
425
369
|
var signer = v.signers[0];
|
|
426
|
-
// A SCEP signer's certificate must be authorized to sign (RFC 8894 sec. 2.3): a signature from a
|
|
427
|
-
// certificate whose keyUsage permits only encryption is not read as an authenticated pkiMessage.
|
|
428
370
|
_assertSigningKeyUsage(signer.cert, "pkiMessage signer certificate");
|
|
429
|
-
// A valid signature proves only that the message is self-consistent with the certificate it embeds,
|
|
430
|
-
// NOT that the signer is the expected party: anyone can mint a certificate and sign a forged CertRep
|
|
431
|
-
// that echoes an observed nonce. A SCEP client MUST authenticate a CA response against the CA
|
|
432
|
-
// certificate it already holds (RFC 8894 sec. 3.1). When opts.signerCert names the expected signer,
|
|
433
|
-
// require the message signer's public key to match it; a caller that omits it gets a crypto-only
|
|
434
|
-
// verdict (signerAuthenticated: false) and MUST authenticate the surfaced signerCert itself before
|
|
435
|
-
// acting on the transaction state.
|
|
436
371
|
var signerAuthenticated = false;
|
|
437
372
|
if (expectedSignerCert != null) {
|
|
438
|
-
// The certificate the caller pins is authenticated only if it is itself authorized to sign: a
|
|
439
|
-
// same-key certificate embedded in the message cannot lend its digitalSignature bit to a pinned
|
|
440
|
-
// certificate that permits only encryption (RFC 8894 sec. 2.3).
|
|
441
373
|
_assertSigningKeyUsage(expectedSignerCert, "opts.signerCert");
|
|
442
374
|
if (!_sameKey(signer.cert, expectedSignerCert)) {
|
|
443
375
|
throw _err("scep/untrusted-signer", "the pkiMessage signer's public key does not match opts.signerCert (RFC 8894 sec. 3.1)");
|
|
@@ -445,7 +377,6 @@ async function parse(bytes, opts) {
|
|
|
445
377
|
signerAuthenticated = true;
|
|
446
378
|
}
|
|
447
379
|
var attrs = signer.signedAttributes;
|
|
448
|
-
// The mandatory attributes (RFC 8894 sec. 3.2.1): transactionID, messageType, senderNonce.
|
|
449
380
|
var messageTypeCode = _readStr(attrs, "scepMessageType", _T_PRINTABLE);
|
|
450
381
|
var transactionId = _readStr(attrs, "scepTransactionId", _T_PRINTABLE);
|
|
451
382
|
var senderNonce = _readNonce(attrs, "scepSenderNonce");
|
|
@@ -454,10 +385,6 @@ async function parse(bytes, opts) {
|
|
|
454
385
|
if (senderNonce == null) throw _err("scep/missing-attribute", "the pkiMessage is missing its senderNonce attribute (RFC 8894 sec. 3.2.1)");
|
|
455
386
|
var messageType = MESSAGE_TYPE_BY_CODE[messageTypeCode];
|
|
456
387
|
if (messageType == null) throw _err("scep/bad-message-type", "unknown messageType " + JSON.stringify(messageTypeCode) + " (RFC 8894 sec. 3.2.1.2)");
|
|
457
|
-
// This release reads the enrollment messages a SCEP client builds and receives: PKCSReq, RenewalReq,
|
|
458
|
-
// and the CertRep response. The CertPoll / GetCert / GetCRL client queries a CA processes carry
|
|
459
|
-
// IssuerAndSubject / IssuerAndSerialNumber messageData this codec does not model, so they are refused
|
|
460
|
-
// rather than accepted with an unvalidated payload (RFC 8894 sec. 3.3.3, sec. 3.3.4).
|
|
461
388
|
if (_PARSE_SUPPORTED[messageType] !== 1) {
|
|
462
389
|
throw _err("scep/unsupported-message-type", "pki.scep.parse reads PKCSReq, RenewalReq, and CertRep; " + messageType + " is not read in this release");
|
|
463
390
|
}
|
|
@@ -467,16 +394,12 @@ async function parse(bytes, opts) {
|
|
|
467
394
|
recipientNonce: null, pkiStatus: null, failInfo: null, failInfoText: null, messageData: null,
|
|
468
395
|
certificates: null, crls: null,
|
|
469
396
|
};
|
|
470
|
-
// A CertRep carries pkiStatus (+ failInfo/failInfoText on FAILURE) and echoes the request's senderNonce.
|
|
471
397
|
if (messageType === "CertRep") {
|
|
472
398
|
var statusCode = _readStr(attrs, "scepPkiStatus", _T_PRINTABLE);
|
|
473
399
|
if (statusCode == null) throw _err("scep/missing-attribute", "a CertRep is missing its pkiStatus attribute (RFC 8894 sec. 3.2.1.3)");
|
|
474
400
|
out.pkiStatus = PKI_STATUS_BY_CODE[statusCode];
|
|
475
401
|
if (out.pkiStatus == null) throw _err("scep/bad-pki-status", "unknown pkiStatus " + JSON.stringify(statusCode) + " (RFC 8894 sec. 3.2.1.3)");
|
|
476
402
|
out.recipientNonce = _readNonce(attrs, "scepRecipientNonce");
|
|
477
|
-
// A CertRep MUST echo the request's senderNonce in a recipientNonce (RFC 8894 sec. 3.1), so a
|
|
478
|
-
// response missing it cannot be correlated to its request and is refused regardless of whether the
|
|
479
|
-
// caller supplied expectedSenderNonce.
|
|
480
403
|
if (out.recipientNonce == null) throw _err("scep/missing-attribute", "a CertRep is missing its recipientNonce attribute (RFC 8894 sec. 3.2.1.5)");
|
|
481
404
|
if (out.pkiStatus === "FAILURE") {
|
|
482
405
|
var failCode = _readStr(attrs, "scepFailInfo", _T_PRINTABLE);
|
|
@@ -485,43 +408,23 @@ async function parse(bytes, opts) {
|
|
|
485
408
|
if (out.failInfo == null) throw _err("scep/bad-fail-info", "unknown failInfo " + JSON.stringify(failCode) + " (RFC 8894 sec. 3.2.1.4)");
|
|
486
409
|
out.failInfoText = _readStr(attrs, "scepFailInfoText", _T_UTF8);
|
|
487
410
|
} else if (_find(attrs, "scepFailInfo") != null || _find(attrs, "scepFailInfoText") != null) {
|
|
488
|
-
// failInfo and failInfoText are defined for a FAILURE response (RFC 8894 sec. 3.2.1.4): a SUCCESS
|
|
489
|
-
// or PENDING CertRep carrying them asserts a contradictory authenticated status and is refused.
|
|
490
411
|
throw _err("scep/unexpected-attribute", "a " + out.pkiStatus + " CertRep must not carry a failInfo or failInfoText attribute (RFC 8894 sec. 3.2.1.4)");
|
|
491
412
|
}
|
|
492
413
|
} else {
|
|
493
|
-
// A request (PKCSReq / RenewalReq) is an initial message: the response-only attributes pkiStatus,
|
|
494
|
-
// failInfo, failInfoText, and recipientNonce are defined for a CertRep and MUST NOT appear here
|
|
495
|
-
// (RFC 8894 sec. 3.2.1). A request carrying any of them is off-profile and refused.
|
|
496
414
|
if (_find(attrs, "scepPkiStatus") != null || _find(attrs, "scepFailInfo") != null ||
|
|
497
415
|
_find(attrs, "scepFailInfoText") != null || _find(attrs, "scepRecipientNonce") != null) {
|
|
498
416
|
throw _err("scep/unexpected-attribute", "a " + messageType + " request must not carry a CertRep response attribute, pkiStatus / failInfo / failInfoText / recipientNonce (RFC 8894 sec. 3.2.1)");
|
|
499
417
|
}
|
|
500
418
|
}
|
|
501
|
-
// A caller-stated expected senderNonce echo (RFC 8894 sec. 3.2.1.5): a message whose recipientNonce
|
|
502
|
-
// does not match the nonce we sent MUST be rejected.
|
|
503
419
|
if (expectedSenderNonce != null) {
|
|
504
420
|
if (out.recipientNonce == null || !guard.crypto.constantTimeEqual(out.recipientNonce, expectedSenderNonce)) {
|
|
505
421
|
throw _err("scep/nonce-mismatch", "the pkiMessage recipientNonce does not echo the sent senderNonce (RFC 8894 sec. 3.2.1.5)");
|
|
506
422
|
}
|
|
507
423
|
}
|
|
508
|
-
// The pkcsPKIEnvelope is carried by a request and a SUCCESS CertRep, and is OMITTED by a FAILURE or
|
|
509
|
-
// PENDING CertRep, whose payload is the status itself (RFC 8894 sec. 3.3.2.1). Decrypt only when the
|
|
510
|
-
// message is one that carries the envelope, so a non-SUCCESS CertRep's authenticated status is
|
|
511
|
-
// returned rather than turned into a decrypt failure over content that is not an EnvelopedData. The
|
|
512
|
-
// eContent is bound to the verified signature (cms.verify).
|
|
513
424
|
var carriesEnvelope = !(messageType === "CertRep" && (out.pkiStatus === "FAILURE" || out.pkiStatus === "PENDING"));
|
|
514
425
|
if (carriesEnvelope) {
|
|
515
|
-
// For a message that carries messageData, the pkcsPKIEnvelope is mandatory, MUST be an EnvelopedData,
|
|
516
|
-
// and MUST encrypt id-data (RFC 8894 sec. 3.2, sec. 3.2.2). This is checked structurally, before any
|
|
517
|
-
// recipient-key decryption, so an absent or off-profile envelope is never accepted as a valid
|
|
518
|
-
// message with a null messageData, whether or not a recipient key was supplied.
|
|
519
426
|
_assertValidEnvelope(v.eContent, messageType);
|
|
520
427
|
} else {
|
|
521
|
-
// A FAILURE or PENDING CertRep MUST omit the pkcsPKIEnvelope (RFC 8894 sec. 3.3.2): its payload is
|
|
522
|
-
// the status itself, so the SignedData carries no encapsulated content (cms.verify surfaces it as a
|
|
523
|
-
// null eContent). Any present eContent -- an attached OCTET STRING even a zero-length one -- is
|
|
524
|
-
// off-profile and refused, so attacker-supplied bytes are never accepted under a non-SUCCESS verdict.
|
|
525
428
|
if (v.eContent != null) {
|
|
526
429
|
throw _err("scep/unexpected-envelope", "a " + out.pkiStatus + " CertRep must omit the pkcsPKIEnvelope (RFC 8894 sec. 3.3.2), but it carries encapsulated content");
|
|
527
430
|
}
|
|
@@ -534,55 +437,41 @@ async function parse(bytes, opts) {
|
|
|
534
437
|
throw e;
|
|
535
438
|
}
|
|
536
439
|
out.messageData = dec.content;
|
|
537
|
-
// A SUCCESS CertRep's messageData MUST be a degenerate certs-only CMS SignedData carrying the
|
|
538
|
-
// issued certificate(s) (RFC 8894 sec. 3.3.2, sec. 3.4). Validate that structure through the strict
|
|
539
|
-
// CMS parser and surface the certificates, so a decrypted payload that is not a valid issuance
|
|
540
|
-
// cannot read as a successful enrollment. The certificates are raw, for the caller to path-validate.
|
|
541
440
|
if (messageType === "CertRep" && out.pkiStatus === "SUCCESS") {
|
|
542
441
|
var payload = _readIssuancePayload(dec.content);
|
|
543
442
|
out.certificates = payload.certificates;
|
|
544
443
|
out.crls = payload.crls;
|
|
545
444
|
} else if (messageType === "PKCSReq" || messageType === "RenewalReq") {
|
|
546
|
-
// A request's messageData MUST be a PKCS#10 CertificationRequest (RFC 8894 sec. 3.3.1). Validate
|
|
547
|
-
// the structure so a decrypted payload that is not one cannot read as a valid request; the raw
|
|
548
|
-
// bytes stay in messageData for the caller to verify the proof-of-possession.
|
|
549
445
|
try { schemaCsr.parse(dec.content); }
|
|
550
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
|
+
}
|
|
551
455
|
}
|
|
552
456
|
}
|
|
553
457
|
return out;
|
|
554
458
|
}
|
|
555
459
|
var _PARSE_KEYS = { recipientKey: 1, expectedSenderNonce: 1, signerCert: 1 };
|
|
556
460
|
|
|
557
|
-
// ---- HTTP client (RFC 8894 sec. 4) -------------------------------------------
|
|
558
|
-
//
|
|
559
|
-
// The client verbs compose the shipped pkiMessage codec (build / parse) over an injectable transport
|
|
560
|
-
// (pki.transport.https by default; tests inject a fake). SCEP's message security is the CMS layer, not
|
|
561
|
-
// the transport (RFC 8894 sec. 2.8), so https is not forced at the URL level; a caller with a plain-HTTP
|
|
562
|
-
// CA injects its own transport. This release ships the mandatory-to-implement set (GetCACaps, GetCACert,
|
|
563
|
-
// PKCSReq via HTTP POST, AES-128-CBC + SHA-256, sec. 2.9) plus RenewalReq. Deferred, each re-opening
|
|
564
|
-
// under its named condition: GetNextCACert (the rollover-certificate authentication model, sec. 4.7);
|
|
565
|
-
// auto-poll / GetCert / GetCRL (their CertPoll / IssuerAndSerialNumber messageData builders are not in
|
|
566
|
-
// this codec); GET-carried PKIOperation (POST is mandatory, sec. 4.3); and HTTP auth (a CA behind an
|
|
567
|
-
// authenticating proxy).
|
|
568
461
|
var DEFAULT_TIMEOUT = C.TIME.seconds(30);
|
|
569
462
|
var MAX_TIMEOUT = C.TIME.seconds(600);
|
|
570
463
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
var
|
|
574
|
-
var
|
|
575
|
-
var
|
|
576
|
-
var CT_NEXT_CA_CERT = "application/x-x509-next-ca-cert"; // GetNextCACert, the rollover certificate(s), sec. 4.7.1
|
|
577
|
-
var CT_PLAIN = "text/plain"; // GetCACaps, a newline-separated capability list
|
|
464
|
+
var CT_CA_CERT = "application/x-x509-ca-cert";
|
|
465
|
+
var CT_CA_RA_CERT = "application/x-x509-ca-ra-cert";
|
|
466
|
+
var CT_PKI_MESSAGE = "application/x-pki-message";
|
|
467
|
+
var CT_NEXT_CA_CERT = "application/x-x509-next-ca-cert";
|
|
468
|
+
var CT_PLAIN = "text/plain";
|
|
578
469
|
|
|
579
470
|
function _isWs(c) { return c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d || c === 0x0b || c === 0x0c; }
|
|
580
471
|
function _foldLower(s) { var o = "", i, n = s.length; for (i = 0; i < n; i++) { var c = s.charCodeAt(i); o += String.fromCharCode(c >= 65 && c <= 90 ? c + 32 : c); } return o; }
|
|
581
472
|
function _foldUpper(s) { var o = "", i, n = s.length; for (i = 0; i < n; i++) { var c = s.charCodeAt(i); o += String.fromCharCode(c >= 97 && c <= 122 ? c - 32 : c); } return o; }
|
|
582
473
|
function _trim(s) { var a = 0, e = s.length; while (a < e && _isWs(s.charCodeAt(a))) a += 1; while (e > a && _isWs(s.charCodeAt(e - 1))) e -= 1; return s.slice(a, e); }
|
|
583
474
|
|
|
584
|
-
// A case-insensitive single-header read. A duplicated header is refused: an injected transport promises
|
|
585
|
-
// only { status, headers, body }, and a smuggled duplicate must never be silently coalesced.
|
|
586
475
|
function _header(headers, name) {
|
|
587
476
|
if (!headers || typeof headers !== "object") return null;
|
|
588
477
|
var found = null, seen = 0, k;
|
|
@@ -593,7 +482,6 @@ function _header(headers, name) {
|
|
|
593
482
|
if (seen > 1) throw _err("scep/bad-response", "the response carried more than one " + name + " header");
|
|
594
483
|
return found == null ? null : String(found);
|
|
595
484
|
}
|
|
596
|
-
// The bare media type of a Content-Type (strip parameters, lower-case), by character scan (no regex).
|
|
597
485
|
function _mediaType(ct) {
|
|
598
486
|
if (ct == null) return "";
|
|
599
487
|
var s = String(ct), semi = s.indexOf(";");
|
|
@@ -603,73 +491,45 @@ function _bodyBuf(body) {
|
|
|
603
491
|
if (Buffer.isBuffer(body)) return body;
|
|
604
492
|
if (body == null) return Buffer.alloc(0);
|
|
605
493
|
if (typeof body === "string") return Buffer.from(body, "utf8");
|
|
606
|
-
// A BufferSource (Uint8Array, ArrayBuffer, DataView) from an injected transport: normalize its raw bytes
|
|
607
|
-
// rather than String()-coercing it, which would turn a Uint8Array into its "48,1,..." toString.
|
|
608
494
|
return guard.bytes.snapshotSource(body, ScepError, "scep/bad-response", "the response body");
|
|
609
495
|
}
|
|
610
|
-
// The byte length of a transport response body, read without a full copy so the size cap is enforced before
|
|
611
|
-
// the body is materialized (cap-before-copy): a string is measured as UTF-8, a Buffer or other BufferSource
|
|
612
|
-
// through the byte guard, which reads the length via a captured intrinsic rather than a shadowable accessor.
|
|
613
496
|
function _responseLen(body) {
|
|
614
497
|
if (body == null) return 0;
|
|
615
498
|
if (typeof body === "string") return Buffer.byteLength(body, "utf8");
|
|
616
499
|
if (Buffer.isBuffer(body)) return guard.bytes.lengthOf(body);
|
|
617
500
|
return guard.bytes.lengthOf(guard.bytes.source(body, ScepError, "scep/bad-response", "the response body"));
|
|
618
501
|
}
|
|
619
|
-
// Map opts.tls to the transport request.tls shape. rejectUnauthorized is NOT set -- the transport forces
|
|
620
|
-
// it on for an https connection.
|
|
621
502
|
function _tlsForRequest(opts) {
|
|
622
503
|
var t = opts.tls || {};
|
|
623
504
|
return { anchors: t.anchors, useSystemStore: t.useSystemStore, cert: t.cert, key: t.key, minVersion: t.minVersion, servername: t.servername, checkServerIdentity: t.checkServerIdentity };
|
|
624
505
|
}
|
|
625
|
-
// Resolve a redirect target: refuse an https->http downgrade (a stripping MITM) and a cross-origin
|
|
626
|
-
// redirect on a non-idempotent method without opt-in; an unparseable Location is scep/bad-url.
|
|
627
506
|
function _redirectTarget(current, location, method, allowCrossOrigin) {
|
|
628
507
|
if (location == null || _trim(String(location)) === "") throw _err("scep/bad-response", "a redirect response carried no Location header");
|
|
629
508
|
var resolved;
|
|
630
509
|
try { resolved = new URL(String(location), current.href); }
|
|
631
510
|
catch (e) { throw _err("scep/bad-url", "a redirect Location did not parse: " + location, e); }
|
|
632
|
-
// SCEP is an HTTP operation (RFC 8894 sec. 4): a redirect target on any other scheme (ftp:, file:, ...)
|
|
633
|
-
// is refused before it reaches the transport, so a CA cannot steer an injected transport off HTTP.
|
|
634
511
|
if (resolved.protocol !== "http:" && resolved.protocol !== "https:") throw _err("scep/bad-url", "a redirect to a non-HTTP scheme is refused: " + resolved.protocol);
|
|
635
512
|
if (current.protocol === "https:" && resolved.protocol !== "https:") throw _err("scep/insecure-redirect", "a redirect from https to " + resolved.protocol + " is refused");
|
|
636
513
|
var safe = method === "GET" || method === "HEAD";
|
|
637
514
|
if (resolved.origin !== current.origin && !safe && !allowCrossOrigin) throw _err("scep/cross-origin-redirect", "a cross-origin redirect on a " + method + " needs opts.allowCrossOriginRedirect");
|
|
638
515
|
return resolved;
|
|
639
516
|
}
|
|
640
|
-
// Build a SCEP operation URL: the caller's base URL with ?operation=<op> appended (RFC 8894 sec. 4.1;
|
|
641
|
-
// SCEPPATH is whatever the base carries, the CA typically ignores it).
|
|
642
517
|
function _scepUrl(baseUrl, operation) {
|
|
643
518
|
var url;
|
|
644
519
|
try { url = new URL(String(baseUrl)); }
|
|
645
520
|
catch (e) { throw _err("scep/bad-url", "the SCEP base URL did not parse: " + String(baseUrl), e); }
|
|
646
|
-
// SCEP is an HTTP operation (RFC 8894 sec. 4): any other scheme (ftp:, file:, ...) is refused before the
|
|
647
|
-
// URL reaches the transport, so an injected transport cannot be steered off HTTP.
|
|
648
521
|
if (url.protocol !== "http:" && url.protocol !== "https:") throw _err("scep/bad-url", "the SCEP base URL must be http or https, got " + url.protocol);
|
|
649
522
|
if (url.hash) throw _err("scep/bad-url", "the SCEP base URL must not carry a fragment");
|
|
650
523
|
url.searchParams.set("operation", operation);
|
|
651
524
|
return url;
|
|
652
525
|
}
|
|
653
|
-
// The transport request / redirect loop. Returns the terminal transport response ({ status, headers,
|
|
654
|
-
// body }). Digest / Basic auth is not answered (deferred); a 401 is returned to the verb, which throws.
|
|
655
526
|
function _drive(method, url, body, headers, opts, transport, budgets) {
|
|
656
527
|
var redirects = 0, curMethod = method, curBody = body, initialOrigin = url.origin;
|
|
657
|
-
// Snapshot the caller's TLS fields and redirect policy once, at the start, before the first transport call:
|
|
658
|
-
// the redirect loop reruns per hop after each transport promise settles, so a hop must use the identity and
|
|
659
|
-
// policy the initial hop did even if opts (or opts.tls) is replaced while a request is in flight.
|
|
660
528
|
var tlsBase = _tlsForRequest(opts);
|
|
661
529
|
var allowCrossOrigin = !!opts.allowCrossOriginRedirect;
|
|
662
|
-
// The mTLS client identity (cert / key / pinned SNI) is sent ONLY to the origin the caller intended; a
|
|
663
|
-
// cross-origin redirect gets a copy with those stripped, so a CA that redirects to another origin cannot
|
|
664
|
-
// make the client authenticate to it with the caller's client credential. Derived fresh each hop, so a
|
|
665
|
-
// redirect back to the original origin restores it.
|
|
666
530
|
function _hopTls(current) {
|
|
667
531
|
var t = tlsBase;
|
|
668
532
|
if (current.origin !== initialOrigin) {
|
|
669
|
-
// Every cross-origin hop overrides the client identity with an explicit null, unconditionally: an
|
|
670
|
-
// omitted field falls back to the transport's own configured defaults (http-transport resolves
|
|
671
|
-
// reqTls.cert !== undefined ? reqTls.cert : tlsDefaults.cert), so the override must fire even when the
|
|
672
|
-
// request-level tls carries no credential, or a transport default would still reach the new origin.
|
|
673
533
|
t = Object.assign({}, t);
|
|
674
534
|
t.cert = null; t.key = null; t.servername = null;
|
|
675
535
|
}
|
|
@@ -704,7 +564,6 @@ function _client(method, url, body, headers, opts) {
|
|
|
704
564
|
};
|
|
705
565
|
return _drive(method, url, body, Object.assign({}, headers), opts, transport, budgets);
|
|
706
566
|
}
|
|
707
|
-
// Require a 200 with one of the expected media types; otherwise a typed error. Returns the body Buffer.
|
|
708
567
|
function _requireOk(res, expected, what) {
|
|
709
568
|
var status = res.status;
|
|
710
569
|
if (status !== 200) {
|
|
@@ -718,8 +577,6 @@ function _requireOk(res, expected, what) {
|
|
|
718
577
|
return _bodyBuf(res.body);
|
|
719
578
|
}
|
|
720
579
|
|
|
721
|
-
// The GetCACaps keyword set (RFC 8894 sec. 3.5.2, Table 7). Comparison is case-folded; the canonical
|
|
722
|
-
// spelling is stored. An unknown keyword is ignored (sec. 3.5.2). SCEPStandard implies the sec. 2.9 set.
|
|
723
580
|
var CAP_CANON = { AES: "AES", DES3: "DES3", GETNEXTCACERT: "GetNextCACert", POSTPKIOPERATION: "POSTPKIOperation",
|
|
724
581
|
RENEWAL: "Renewal", "SHA-1": "SHA-1", "SHA-256": "SHA-256", "SHA-512": "SHA-512", SCEPSTANDARD: "SCEPStandard" };
|
|
725
582
|
function _splitLines(s) {
|
|
@@ -790,29 +647,17 @@ function _assertStrongProfile(caps, profile) {
|
|
|
790
647
|
async function getCACaps(baseUrl, opts) {
|
|
791
648
|
opts = guard.identifier.optionsObject(opts, _err, "scep/bad-input", "pki.scep.getCACaps options");
|
|
792
649
|
guard.identifier.assertKnownKeys(opts, _CACAPS_KEYS, _err, "scep/bad-input", "pki.scep.getCACaps has an unknown option: ");
|
|
793
|
-
// Capture the strong-profile policy at the door: the downgrade check runs only after the GET await, so a
|
|
794
|
-
// caller must not be able to relax its own guard by mutating opts while the request is in flight.
|
|
795
650
|
var profile = { expectSCEPStandard: !!opts.expectSCEPStandard, requireStrongProfile: !!opts.requireStrongProfile };
|
|
796
651
|
var res = await _client("GET", _scepUrl(baseUrl, "GetCACaps"), null, {}, opts);
|
|
797
|
-
// An HTTP error or empty body means "none supported" (RFC 8894 sec. 3.5.1); a non-200 that is not a
|
|
798
|
-
// clean error is still surfaced so a caller is not silently told "no capabilities" on a broken CA.
|
|
799
652
|
if (res.status === 200) {
|
|
800
|
-
// A conforming GetCACaps response carries content-type text/plain (RFC 8894 sec. 3.5.1). Enforcing it
|
|
801
|
-
// stops a proxy's 200 HTML error page from being read as an empty (no capabilities) capability set.
|
|
802
653
|
var caps = parseCapabilities(_requireOk(res, [CT_PLAIN], "GetCACaps"));
|
|
803
654
|
_assertStrongProfile(caps, profile);
|
|
804
655
|
return caps;
|
|
805
656
|
}
|
|
806
|
-
// A CA that does not implement GetCACaps signals it with an HTTP error, which RFC 8894 sec. 3.5.1 reads
|
|
807
|
-
// as "no capabilities advertised": the codes that mean the operation is unsupported or unrecognized are
|
|
808
|
-
// 400 (an old CA that does not parse the query), 404 (no such operation), 405 (GET not allowed on it), and
|
|
809
|
-
// 501 Not Implemented. A transient server error (500, 502, 503) is surfaced instead of masked.
|
|
810
657
|
if (res.status === 204 || res.status === 400 || res.status === 404 || res.status === 405 || res.status === 501) { _assertStrongProfile({}, profile); return {}; }
|
|
811
658
|
throw _err("scep/http-error", "the SCEP GetCACaps returned HTTP " + res.status);
|
|
812
659
|
}
|
|
813
660
|
|
|
814
|
-
// The exact SPKI byte-match that identifies an issued certificate (RFC 5272 sec. 4.1); positional
|
|
815
|
-
// guessing is forbidden. `certs` is an array of raw cert Buffers, `wantSpki` the SPKI Buffer to match.
|
|
816
661
|
function findIssuedCert(certs, wantSpki) {
|
|
817
662
|
if (!Buffer.isBuffer(wantSpki)) return null;
|
|
818
663
|
for (var i = 0; i < certs.length; i++) {
|
|
@@ -823,12 +668,8 @@ function findIssuedCert(certs, wantSpki) {
|
|
|
823
668
|
}
|
|
824
669
|
return null;
|
|
825
670
|
}
|
|
826
|
-
// Normalize an expected fingerprint (hex string, optionally colon/space separated, or a Buffer) to
|
|
827
|
-
// { algorithm, value }. Character-scan the hex (Hard rule #11).
|
|
828
671
|
function _normalizeFingerprint(fp, algName) {
|
|
829
672
|
var algorithm = _foldLower(String(algName == null ? "sha256" : algName));
|
|
830
|
-
// Validate the digest name at the entry point so a misspelled or build-unavailable fingerprintAlgorithm
|
|
831
|
-
// is the typed scep/bad-input the client API documents, not a native createHash exception downstream.
|
|
832
673
|
try { nodeCrypto.createHash(algorithm); }
|
|
833
674
|
catch (_e) { throw _err("scep/bad-input", "fingerprintAlgorithm " + JSON.stringify(algorithm) + " is not an available hash"); }
|
|
834
675
|
var value;
|
|
@@ -878,9 +719,6 @@ function _normalizeFingerprint(fp, algName) {
|
|
|
878
719
|
async function getCACert(baseUrl, opts) {
|
|
879
720
|
opts = guard.identifier.optionsObject(opts, _err, "scep/bad-input", "pki.scep.getCACert options");
|
|
880
721
|
guard.identifier.assertKnownKeys(opts, _CACERT_KEYS, _err, "scep/bad-input", "pki.scep.getCACert has an unknown option: ");
|
|
881
|
-
// Normalize the expected fingerprint at the synchronous entry point: fail fast on a bad hash before the
|
|
882
|
-
// network call, and pin against the value the caller supplied at the call rather than one mutated while
|
|
883
|
-
// the GET is in flight.
|
|
884
722
|
var fp = opts.expectedFingerprint != null ? _normalizeFingerprint(opts.expectedFingerprint, opts.fingerprintAlgorithm) : null;
|
|
885
723
|
var res = await _client("GET", _scepUrl(baseUrl, "GetCACert"), null, {}, opts);
|
|
886
724
|
var body = _requireOk(res, [CT_CA_CERT, CT_CA_RA_CERT], "GetCACert");
|
|
@@ -890,7 +728,6 @@ async function getCACert(baseUrl, opts) {
|
|
|
890
728
|
certs = [Buffer.from(body)];
|
|
891
729
|
single = true;
|
|
892
730
|
} else {
|
|
893
|
-
// parseCertsOnly fails closed with scep/no-certificates on an empty CertificateSet, so certs is non-empty here.
|
|
894
731
|
certs = schemaCms.parseCertsOnly(body, _err, "scep").certificates;
|
|
895
732
|
}
|
|
896
733
|
var caCertificate = null;
|
|
@@ -903,9 +740,6 @@ async function getCACert(baseUrl, opts) {
|
|
|
903
740
|
if (!matched) throw _err("scep/fingerprint-mismatch", "no GetCACert certificate matched the expected " + fp.algorithm + " fingerprint (RFC 8894 sec. 2.2)");
|
|
904
741
|
caCertificate = matched;
|
|
905
742
|
} else if (single) {
|
|
906
|
-
// A single-certificate response (application/x-x509-ca-cert) is unambiguously the CA certificate. A
|
|
907
|
-
// ca-ra chain carries the CA, any intermediates, and RA certificate(s) in a CMS CertificateSet whose
|
|
908
|
-
// DER order does not identify the CA role, so caCertificate stays null there unless a fingerprint pins it.
|
|
909
743
|
caCertificate = certs[0];
|
|
910
744
|
}
|
|
911
745
|
return { caCertificate: caCertificate, certificates: certs };
|
|
@@ -940,22 +774,12 @@ async function getCACert(baseUrl, opts) {
|
|
|
940
774
|
async function getNextCACert(baseUrl, opts) {
|
|
941
775
|
opts = guard.identifier.optionsObject(opts, _err, "scep/bad-input", "pki.scep.getNextCACert options");
|
|
942
776
|
guard.identifier.assertKnownKeys(opts, _NEXTCACERT_KEYS, _err, "scep/bad-input", "pki.scep.getNextCACert has an unknown option: ");
|
|
943
|
-
// The current CA certificate authenticates the response (RFC 8894 sec. 4.7.1). Require it and snapshot
|
|
944
|
-
// its bytes at this synchronous door, before the GET await, so a caller cannot switch the authenticating
|
|
945
|
-
// certificate out from under the exchange, and so an unauthenticated rollover certificate -- which would
|
|
946
|
-
// become a future trust anchor -- is never returned.
|
|
947
777
|
if (opts.caCertificate == null) throw _err("scep/bad-input", "a caCertificate (the current SCEP CA certificate DER) is required to authenticate the GetNextCACert response (RFC 8894 sec. 4.7.1)");
|
|
948
778
|
var currentCa = guard.bytes.snapshotSource(opts.caCertificate, ScepError, "scep/bad-input", "the caCertificate");
|
|
949
|
-
// The current CA must itself be authorized to sign, or its signature over the rollover certificate
|
|
950
|
-
// authenticates nothing (RFC 8894 sec. 2.3); fail fast at the door before the network call.
|
|
951
779
|
_assertSigningKeyUsage(currentCa, "the caCertificate");
|
|
952
780
|
var url = _scepUrl(baseUrl, "GetNextCACert");
|
|
953
781
|
var res = await _client("GET", url, null, {}, opts);
|
|
954
782
|
var body = _requireOk(res, [CT_NEXT_CA_CERT], "GetNextCACert");
|
|
955
|
-
// Verify the outer SignedData, passing the current CA as a candidate so the response need not embed the
|
|
956
|
-
// signer. cms.verify reports a per-signer verdict rather than throwing on a bad signature, so an invalid
|
|
957
|
-
// signature is refused here before any content is read. A detached outer (no content) has no rollover
|
|
958
|
-
// certificate to carry and is refused.
|
|
959
783
|
var v;
|
|
960
784
|
try { v = await cmsVerify.verify(body, { certs: [currentCa] }); }
|
|
961
785
|
catch (e) {
|
|
@@ -963,28 +787,72 @@ async function getNextCACert(baseUrl, opts) {
|
|
|
963
787
|
if (e && e.isPkiError) throw _err("scep/bad-response", "the GetNextCACert response is not a verifiable CMS SignedData (RFC 8894 sec. 4.7.1): " + e.message, e);
|
|
964
788
|
throw e;
|
|
965
789
|
}
|
|
966
|
-
// A GetNextCACert response is signed by the current CA and only the current CA (RFC 8894 sec. 4.7.1);
|
|
967
|
-
// exactly one SignerInfo is expected, as with any authenticated SCEP message (sec. 3.1). Requiring one
|
|
968
|
-
// signer binds the signature-verified check and the current-CA key pin to the SAME signer, so a second
|
|
969
|
-
// signer's valid signature cannot stand in for a current-CA identity whose own signature does not verify.
|
|
970
790
|
if (v.signers.length !== 1) throw _err("scep/bad-signer", "a GetNextCACert response must carry exactly one SignerInfo (RFC 8894 sec. 4.7.1), got " + v.signers.length);
|
|
971
791
|
if (v.valid !== true) throw _err("scep/bad-signature", "the GetNextCACert outer signature did not verify (RFC 8894 sec. 4.7.1)");
|
|
972
|
-
// Pin the one verified signer to the current CA key: only the current CA holds the current CA key, so the
|
|
973
|
-
// verified signer carrying that key IS the current CA (RFC 8894 sec. 4.7.1).
|
|
974
792
|
if (!_sameKey(v.signers[0].cert, currentCa)) throw _err("scep/untrusted-signer", "the GetNextCACert response was not signed by the current CA key (RFC 8894 sec. 4.7.1)");
|
|
975
|
-
// The verified content is a degenerate certs-only SignedData carrying the next CA certificate(s);
|
|
976
|
-
// parseCertsOnly fails closed on an empty CertificateSet (scep/no-certificates).
|
|
977
793
|
var certs = schemaCms.parseCertsOnly(v.eContent, _err, "scep").certificates;
|
|
978
794
|
return { certificates: certs };
|
|
979
795
|
}
|
|
980
796
|
|
|
981
|
-
// The default transaction ID: 16 random bytes, hex-encoded. RFC 8894 sec. 3.2.1.1 requires the ID to be
|
|
982
|
-
// unique per operation, so a per-key value would collide across separate enrollments of the same key (a
|
|
983
|
-
// renewal could not be told from the original request). A caller wanting a stable per-device ID passes an
|
|
984
|
-
// explicit transactionId.
|
|
985
797
|
function _defaultTransactionId() {
|
|
986
798
|
return nodeCrypto.randomBytes(16).toString("hex");
|
|
987
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
|
+
}
|
|
988
856
|
async function _pkiOperation(messageType, baseUrl, opts, verbName) {
|
|
989
857
|
opts = guard.identifier.optionsObject(opts, _err, "scep/bad-input", "pki.scep." + verbName + " options");
|
|
990
858
|
guard.identifier.assertKnownKeys(opts, _ENROLL_KEYS, _err, "scep/bad-input", "pki.scep." + verbName + " has an unknown option: ");
|
|
@@ -992,22 +860,11 @@ async function _pkiOperation(messageType, baseUrl, opts, verbName) {
|
|
|
992
860
|
if (opts.caCert == null) throw _err("scep/bad-input", "a caCert (the SCEP CA certificate DER) is required");
|
|
993
861
|
if (opts.signer == null || typeof opts.signer !== "object") throw _err("scep/bad-input", "a signer { cert, key } is required");
|
|
994
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");
|
|
995
|
-
// Snapshot the response-authentication inputs at the synchronous entry point: the response is
|
|
996
|
-
// authenticated (signerCert) and decrypted (recipientKey) only after the build and POST awaits, so a
|
|
997
|
-
// caller mutating opts mid-flight cannot switch the trusted signer, or the CA the request was encrypted
|
|
998
|
-
// to, out from under the exchange. Certificate bytes are copied; the recipient private key and the
|
|
999
|
-
// request signer are held by reference.
|
|
1000
863
|
var caCert = guard.bytes.snapshotSource(opts.caCert, ScepError, "scep/bad-input", "the caCert");
|
|
1001
864
|
var signerCert = opts.responderCert != null ? guard.bytes.snapshotSource(opts.responderCert, ScepError, "scep/bad-input", "the responderCert") : caCert;
|
|
1002
|
-
|
|
1003
|
-
// held by reference), not the opts.recipientKey object: a caller replacing opts.recipientKey.cert or .key
|
|
1004
|
-
// while the POST is in flight must not switch the credential the response is decrypted with.
|
|
865
|
+
var issuerCert = opts.issuerCert != null ? guard.bytes.snapshotSource(opts.issuerCert, ScepError, "scep/bad-input", "the issuerCert") : caCert;
|
|
1005
866
|
var recipientKey = _captureRecipientKey(opts.recipientKey);
|
|
1006
|
-
var signer = opts.signer;
|
|
1007
|
-
// The transport options are read by _client only after the build await, so snapshot them at the door too:
|
|
1008
|
-
// a caller replacing opts.transport (rerouting the completed request) or opts.tls.cert/.key (presenting the
|
|
1009
|
-
// next operation's mTLS identity) while the POST is in flight must not take effect. tls is shallow-copied
|
|
1010
|
-
// so a replaced field does not reach the transport; the transport function and budgets are held by value.
|
|
867
|
+
var signer = { cert: guard.bytes.snapshotSource(opts.signer.cert, ScepError, "scep/bad-input", "signer.cert"), key: opts.signer.key };
|
|
1011
868
|
var clientOpts = {
|
|
1012
869
|
transport: opts.transport,
|
|
1013
870
|
tls: opts.tls != null ? Object.assign({}, opts.tls) : opts.tls,
|
|
@@ -1016,43 +873,30 @@ async function _pkiOperation(messageType, baseUrl, opts, verbName) {
|
|
|
1016
873
|
timeout: opts.timeout,
|
|
1017
874
|
maxResponseBytes: opts.maxResponseBytes,
|
|
1018
875
|
};
|
|
1019
|
-
// Resolve the request URL at the door: _scepUrl reads String(baseUrl) only when the POST is sent (after the
|
|
1020
|
-
// build await), so a baseUrl whose toString() the caller changes mid-flight could otherwise redirect it.
|
|
1021
876
|
var postUrl = _scepUrl(baseUrl, "PKIOperation");
|
|
1022
877
|
var csrView = guard.bytes.snapshotSource(opts.csr, ScepError, "scep/bad-input", "the csr");
|
|
1023
|
-
var spki;
|
|
1024
|
-
try {
|
|
878
|
+
var spki, csrSubject;
|
|
879
|
+
try { var _pcsr = schemaCsr.parse(csrView); spki = _pcsr.subjectPublicKeyInfo; csrSubject = _pcsr.subject.bytes; }
|
|
1025
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;
|
|
1026
885
|
var senderNonce = nodeCrypto.randomBytes(16);
|
|
1027
886
|
var transactionId = opts.transactionId != null ? opts.transactionId : _defaultTransactionId();
|
|
1028
887
|
var reqMsg = await build({ messageType: messageType, messageData: csrView, recipient: caCert, signer: signer, transactionId: transactionId, senderNonce: senderNonce });
|
|
1029
888
|
var res = await _client("POST", postUrl, reqMsg, { "content-type": CT_PKI_MESSAGE }, clientOpts);
|
|
1030
889
|
var body = _requireOk(res, [CT_PKI_MESSAGE], "PKIOperation");
|
|
1031
890
|
var rep = await parse(body, { signerCert: signerCert, expectedSenderNonce: senderNonce, recipientKey: recipientKey });
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
failErr.failInfo = rep.failInfo;
|
|
1042
|
-
failErr.failInfoText = rep.failInfoText;
|
|
1043
|
-
throw failErr;
|
|
1044
|
-
}
|
|
1045
|
-
if (rep.pkiStatus === "PENDING") return { status: "PENDING", transactionId: rep.transactionId, senderNonce: senderNonce };
|
|
1046
|
-
var repCerts = rep.certificates || [];
|
|
1047
|
-
var issued = findIssuedCert(repCerts, spki.bytes);
|
|
1048
|
-
if (issued == null) throw _err("scep/no-issued-cert", "the SUCCESS CertRep did not carry a certificate matching the request public key");
|
|
1049
|
-
// A CMS CertificateSet states no issuance order, so a second certificate carrying the request public key
|
|
1050
|
-
// (a same-key renewal returning old and new, or a duplicate) leaves the issued certificate ambiguous; it
|
|
1051
|
-
// is refused rather than resolved by position.
|
|
1052
|
-
if (findIssuedCert(repCerts.filter(function (c) { return c !== issued; }), spki.bytes) != null) {
|
|
1053
|
-
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)");
|
|
1054
|
-
}
|
|
1055
|
-
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
|
+
});
|
|
1056
900
|
}
|
|
1057
901
|
|
|
1058
902
|
/**
|
|
@@ -1068,8 +912,13 @@ async function _pkiOperation(messageType, baseUrl, opts, verbName) {
|
|
|
1068
912
|
* the CertRep. The CA response is authenticated against the CA certificate and its `recipientNonce` must
|
|
1069
913
|
* echo the request's fresh `senderNonce`. On SUCCESS the issued certificate is selected out of the
|
|
1070
914
|
* response by SubjectPublicKeyInfo byte-match against the request; a FAILURE throws
|
|
1071
|
-
* `scep/enrollment-failed` carrying the CA's failInfo
|
|
1072
|
-
*
|
|
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
|
|
1073
922
|
* certificate must be an RSA `keyEncipherment` certificate that accepts OAEP; the shared-secret
|
|
1074
923
|
* authenticator (RFC 8894 sec. 2.4) is the `challengePassword` the caller placed in the PKCS#10.
|
|
1075
924
|
*
|
|
@@ -1079,14 +928,19 @@ async function _pkiOperation(messageType, baseUrl, opts, verbName) {
|
|
|
1079
928
|
* - `signer` -- `{ cert, key }`, the outer SignedData signer (for a PKCSReq, a self-signed certificate over the request key).
|
|
1080
929
|
* - `recipientKey` -- `{ cert, key }`, the RSA key the CA encrypts the response to, to decrypt the CertRep.
|
|
1081
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).
|
|
1082
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.
|
|
1083
937
|
* - `transport` / `timeout` / `maxResponseBytes` / `maxRedirects` / `tls` / `allowCrossOriginRedirect` -- transport bounds.
|
|
1084
938
|
* @example
|
|
1085
939
|
* // requires: scepCsr, caCertDer, clientCertDer, clientKeyPkcs8, scepTransport
|
|
1086
940
|
* var out = await pki.scep.enroll("http://ca.example/scep", { csr: scepCsr, caCert: caCertDer,
|
|
1087
941
|
* signer: { cert: clientCertDer, key: clientKeyPkcs8 },
|
|
1088
942
|
* recipientKey: { cert: clientCertDer, key: clientKeyPkcs8 }, transport: scepTransport });
|
|
1089
|
-
* out.status; // "SUCCESS"
|
|
943
|
+
* out.status; // "SUCCESS" (a PENDING request is polled to a terminal status unless pollCount is 0)
|
|
1090
944
|
*/
|
|
1091
945
|
function enroll(baseUrl, opts) { return _pkiOperation("PKCSReq", baseUrl, opts, "enroll"); }
|
|
1092
946
|
|
|
@@ -1118,7 +972,7 @@ function _withKeys(extra) { var o = {}, k; for (k in _TRANSPORT_KEYS) o[k] = 1;
|
|
|
1118
972
|
var _CACAPS_KEYS = _withKeys({ expectSCEPStandard: 1, requireStrongProfile: 1 });
|
|
1119
973
|
var _CACERT_KEYS = _withKeys({ expectedFingerprint: 1, fingerprintAlgorithm: 1 });
|
|
1120
974
|
var _NEXTCACERT_KEYS = _withKeys({ caCertificate: 1 });
|
|
1121
|
-
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 });
|
|
1122
976
|
|
|
1123
977
|
module.exports = {
|
|
1124
978
|
build: build,
|