@blamejs/pki 0.6.13 → 0.6.15
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 +22 -1
- package/README.md +1 -1
- package/index.js +1 -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 +59 -233
- 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,26 @@ 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
56
|
var nodeCrypto = require("node:crypto");
|
|
57
57
|
|
|
58
58
|
function _err(code, msg, cause) { return new ScepError(code, msg, cause); }
|
|
59
59
|
|
|
60
|
-
// The namespace pkix.keyUsageOf reads a certificate's keyUsage extension through.
|
|
61
60
|
var _KU_NS = pkix.makeNS("scep", ScepError, oid);
|
|
62
61
|
|
|
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
62
|
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
63
|
var PKI_STATUS = { SUCCESS: "0", FAILURE: "2", PENDING: "3" };
|
|
69
|
-
// failInfo (Table 5).
|
|
70
64
|
var FAIL_INFO = { badAlg: "0", badMessageCheck: "1", badRequest: "2", badTime: "3", badCertId: "4" };
|
|
71
65
|
|
|
72
|
-
// name -> code and code -> name, built once from each table so a lookup is a data read, not a switch.
|
|
73
66
|
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
67
|
var MESSAGE_TYPE_BY_CODE = _invert(MESSAGE_TYPE), PKI_STATUS_BY_CODE = _invert(PKI_STATUS), FAIL_INFO_BY_CODE = _invert(FAIL_INFO);
|
|
75
68
|
|
|
76
|
-
// The message types pki.scep.parse reads: the enrollment request pair a client builds and the CertRep
|
|
77
|
-
// response it receives. The CertPoll / GetCert / GetCRL client queries are refused (see parse).
|
|
78
69
|
var _PARSE_SUPPORTED = { PKCSReq: 1, RenewalReq: 1, CertRep: 1 };
|
|
79
70
|
|
|
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
71
|
var ENVELOPE_CIPHER = "aes-128-cbc";
|
|
84
72
|
|
|
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
73
|
var EMPTY_CONTENT = Buffer.alloc(0);
|
|
88
74
|
|
|
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
75
|
function _nonce(v, label) {
|
|
92
76
|
if (v == null) return nodeCrypto.randomBytes(16);
|
|
93
77
|
var buf = guard.bytes.snapshotSource(v, ScepError, "scep/bad-nonce", label);
|
|
@@ -95,19 +79,12 @@ function _nonce(v, label) {
|
|
|
95
79
|
return buf;
|
|
96
80
|
}
|
|
97
81
|
|
|
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
82
|
function _transactionId(v) {
|
|
101
83
|
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
84
|
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
85
|
return v;
|
|
104
86
|
}
|
|
105
87
|
|
|
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
88
|
function _assertValidEnvelope(bytes, messageType) {
|
|
112
89
|
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
90
|
var parsed;
|
|
@@ -115,17 +92,10 @@ function _assertValidEnvelope(bytes, messageType) {
|
|
|
115
92
|
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
93
|
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
94
|
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
95
|
var ct = parsed.encryptedContentInfo.encryptedContent;
|
|
122
96
|
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
97
|
}
|
|
124
98
|
|
|
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
99
|
function _readIssuancePayload(der) {
|
|
130
100
|
var r;
|
|
131
101
|
try { r = schemaCms.parse(der); }
|
|
@@ -134,9 +104,6 @@ function _readIssuancePayload(der) {
|
|
|
134
104
|
r.encapContentInfo.eContent !== null || r.signerInfos.length !== 0) {
|
|
135
105
|
throw _err("scep/not-certs-only", "a SUCCESS CertRep messageData must be a degenerate certificates-only CMS SignedData (RFC 8894 sec. 3.4)");
|
|
136
106
|
}
|
|
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
107
|
var certs = [], crls = [], i;
|
|
141
108
|
var rc = r.certificates || [], rl = r.crls || [];
|
|
142
109
|
for (i = 0; i < rc.length; i++) {
|
|
@@ -157,14 +124,8 @@ function _readIssuancePayload(der) {
|
|
|
157
124
|
return { certificates: certs, crls: crls };
|
|
158
125
|
}
|
|
159
126
|
|
|
160
|
-
// Build one SCEP transaction attribute { type, values:[valueDER] } for cms.sign's additionalSignedAttributes.
|
|
161
127
|
function _attr(name, valueDer) { return { type: oid.byName(name), values: [valueDer] }; }
|
|
162
128
|
|
|
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
129
|
function _find(attrs, name) {
|
|
169
130
|
var want = oid.byName(name);
|
|
170
131
|
for (var i = 0; i < attrs.length; i++) {
|
|
@@ -176,9 +137,6 @@ function _find(attrs, name) {
|
|
|
176
137
|
}
|
|
177
138
|
return null;
|
|
178
139
|
}
|
|
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
140
|
function _readStr(attrs, name, expectedTag) {
|
|
183
141
|
var a = _find(attrs, name);
|
|
184
142
|
if (a == null) return null;
|
|
@@ -192,7 +150,6 @@ function _readStr(attrs, name, expectedTag) {
|
|
|
192
150
|
catch (e) { throw _err("scep/bad-attribute", "the " + name + " transaction attribute is not a readable string", e); }
|
|
193
151
|
}
|
|
194
152
|
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
153
|
function _readNonce(attrs, name) {
|
|
197
154
|
var a = _find(attrs, name);
|
|
198
155
|
if (a == null) return null;
|
|
@@ -203,9 +160,6 @@ function _readNonce(attrs, name) {
|
|
|
203
160
|
return n;
|
|
204
161
|
}
|
|
205
162
|
|
|
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
163
|
function _sameKey(certA, certB) {
|
|
210
164
|
var a, bb;
|
|
211
165
|
try { a = schemaX509.parse(certA).subjectPublicKeyInfo.bytes; bb = schemaX509.parse(certB).subjectPublicKeyInfo.bytes; }
|
|
@@ -213,9 +167,6 @@ function _sameKey(certA, certB) {
|
|
|
213
167
|
return guard.crypto.constantTimeEqual(a, bb);
|
|
214
168
|
}
|
|
215
169
|
|
|
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
170
|
function _assertRsaRecipient(recipientDer) {
|
|
220
171
|
var spki;
|
|
221
172
|
try { spki = schemaX509.parse(recipientDer).subjectPublicKeyInfo; }
|
|
@@ -225,10 +176,6 @@ function _assertRsaRecipient(recipientDer) {
|
|
|
225
176
|
}
|
|
226
177
|
}
|
|
227
178
|
|
|
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
179
|
function _assertSigningKeyUsage(certDer, label) {
|
|
233
180
|
var cert;
|
|
234
181
|
try { cert = schemaX509.parse(certDer); }
|
|
@@ -239,9 +186,6 @@ function _assertSigningKeyUsage(certDer, label) {
|
|
|
239
186
|
}
|
|
240
187
|
}
|
|
241
188
|
|
|
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
189
|
function _captureRecipientKey(rk) {
|
|
246
190
|
if (rk == null) return null;
|
|
247
191
|
rk = guard.identifier.optionsObject(rk, _err, "scep/bad-input", "opts.recipientKey");
|
|
@@ -301,14 +245,6 @@ async function build(spec) {
|
|
|
301
245
|
if (spec.signer == null || typeof spec.signer !== "object") throw _err("scep/bad-input", "a signer { cert, key } is required");
|
|
302
246
|
var senderNonce = _nonce(spec.senderNonce, "senderNonce");
|
|
303
247
|
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
248
|
var messageData = guard.bytes.snapshotSource(spec.messageData, ScepError, "scep/bad-input", "messageData");
|
|
313
249
|
var recipient = guard.bytes.snapshotSource(spec.recipient, ScepError, "scep/bad-input", "recipient");
|
|
314
250
|
_assertRsaRecipient(recipient);
|
|
@@ -321,7 +257,6 @@ async function build(spec) {
|
|
|
321
257
|
if (!(await csrVerify.verifyCsrSignature(parsedCsr))) {
|
|
322
258
|
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");
|
|
323
259
|
}
|
|
324
|
-
// Inner EnvelopedData: encrypt the PKCS#10 to the recipient CA, AES-128-CBC (RFC 8894 sec. 3).
|
|
325
260
|
var pkcsPKIEnvelope;
|
|
326
261
|
try {
|
|
327
262
|
pkcsPKIEnvelope = await cmsEncrypt.encrypt(messageData, [{ cert: recipient }], { contentEncryptionAlgorithm: ENVELOPE_CIPHER });
|
|
@@ -329,7 +264,6 @@ async function build(spec) {
|
|
|
329
264
|
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
265
|
throw e;
|
|
331
266
|
}
|
|
332
|
-
// Outer SignedData over the envelope, carrying the transaction attributes as authenticated attributes.
|
|
333
267
|
var attrs = [
|
|
334
268
|
_attr("scepMessageType", b.printable(mtCode)),
|
|
335
269
|
_attr("scepTransactionId", b.printable(txnId)),
|
|
@@ -386,24 +320,13 @@ var _BUILD_KEYS = { messageType: 1, messageData: 1, recipient: 1, signer: 1, tra
|
|
|
386
320
|
async function parse(bytes, opts) {
|
|
387
321
|
opts = guard.identifier.optionsObject(opts, _err, "scep/bad-input", "pki.scep.parse options");
|
|
388
322
|
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
323
|
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
324
|
var expectedSignerCert = opts.signerCert != null ? guard.bytes.snapshotSource(opts.signerCert, ScepError, "scep/bad-input", "opts.signerCert") : null;
|
|
398
325
|
var expectedSenderNonce = opts.expectedSenderNonce != null ? _nonce(opts.expectedSenderNonce, "expectedSenderNonce") : null;
|
|
399
326
|
var recipientKey = _captureRecipientKey(opts.recipientKey);
|
|
400
327
|
var v;
|
|
401
328
|
try { v = await cmsVerify.verify(msgBytes); }
|
|
402
329
|
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
330
|
if (e && e.code === "cms/detached-content-required") {
|
|
408
331
|
try { v = await cmsVerify.verify(msgBytes, { content: EMPTY_CONTENT }); }
|
|
409
332
|
catch (e2) {
|
|
@@ -415,29 +338,12 @@ async function parse(bytes, opts) {
|
|
|
415
338
|
} else { throw e; }
|
|
416
339
|
}
|
|
417
340
|
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
341
|
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
342
|
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
343
|
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
344
|
_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
345
|
var signerAuthenticated = false;
|
|
437
346
|
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
347
|
_assertSigningKeyUsage(expectedSignerCert, "opts.signerCert");
|
|
442
348
|
if (!_sameKey(signer.cert, expectedSignerCert)) {
|
|
443
349
|
throw _err("scep/untrusted-signer", "the pkiMessage signer's public key does not match opts.signerCert (RFC 8894 sec. 3.1)");
|
|
@@ -445,7 +351,6 @@ async function parse(bytes, opts) {
|
|
|
445
351
|
signerAuthenticated = true;
|
|
446
352
|
}
|
|
447
353
|
var attrs = signer.signedAttributes;
|
|
448
|
-
// The mandatory attributes (RFC 8894 sec. 3.2.1): transactionID, messageType, senderNonce.
|
|
449
354
|
var messageTypeCode = _readStr(attrs, "scepMessageType", _T_PRINTABLE);
|
|
450
355
|
var transactionId = _readStr(attrs, "scepTransactionId", _T_PRINTABLE);
|
|
451
356
|
var senderNonce = _readNonce(attrs, "scepSenderNonce");
|
|
@@ -454,10 +359,6 @@ async function parse(bytes, opts) {
|
|
|
454
359
|
if (senderNonce == null) throw _err("scep/missing-attribute", "the pkiMessage is missing its senderNonce attribute (RFC 8894 sec. 3.2.1)");
|
|
455
360
|
var messageType = MESSAGE_TYPE_BY_CODE[messageTypeCode];
|
|
456
361
|
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
362
|
if (_PARSE_SUPPORTED[messageType] !== 1) {
|
|
462
363
|
throw _err("scep/unsupported-message-type", "pki.scep.parse reads PKCSReq, RenewalReq, and CertRep; " + messageType + " is not read in this release");
|
|
463
364
|
}
|
|
@@ -467,16 +368,12 @@ async function parse(bytes, opts) {
|
|
|
467
368
|
recipientNonce: null, pkiStatus: null, failInfo: null, failInfoText: null, messageData: null,
|
|
468
369
|
certificates: null, crls: null,
|
|
469
370
|
};
|
|
470
|
-
// A CertRep carries pkiStatus (+ failInfo/failInfoText on FAILURE) and echoes the request's senderNonce.
|
|
471
371
|
if (messageType === "CertRep") {
|
|
472
372
|
var statusCode = _readStr(attrs, "scepPkiStatus", _T_PRINTABLE);
|
|
473
373
|
if (statusCode == null) throw _err("scep/missing-attribute", "a CertRep is missing its pkiStatus attribute (RFC 8894 sec. 3.2.1.3)");
|
|
474
374
|
out.pkiStatus = PKI_STATUS_BY_CODE[statusCode];
|
|
475
375
|
if (out.pkiStatus == null) throw _err("scep/bad-pki-status", "unknown pkiStatus " + JSON.stringify(statusCode) + " (RFC 8894 sec. 3.2.1.3)");
|
|
476
376
|
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
377
|
if (out.recipientNonce == null) throw _err("scep/missing-attribute", "a CertRep is missing its recipientNonce attribute (RFC 8894 sec. 3.2.1.5)");
|
|
481
378
|
if (out.pkiStatus === "FAILURE") {
|
|
482
379
|
var failCode = _readStr(attrs, "scepFailInfo", _T_PRINTABLE);
|
|
@@ -485,43 +382,23 @@ async function parse(bytes, opts) {
|
|
|
485
382
|
if (out.failInfo == null) throw _err("scep/bad-fail-info", "unknown failInfo " + JSON.stringify(failCode) + " (RFC 8894 sec. 3.2.1.4)");
|
|
486
383
|
out.failInfoText = _readStr(attrs, "scepFailInfoText", _T_UTF8);
|
|
487
384
|
} 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
385
|
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
386
|
}
|
|
492
387
|
} 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
388
|
if (_find(attrs, "scepPkiStatus") != null || _find(attrs, "scepFailInfo") != null ||
|
|
497
389
|
_find(attrs, "scepFailInfoText") != null || _find(attrs, "scepRecipientNonce") != null) {
|
|
498
390
|
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
391
|
}
|
|
500
392
|
}
|
|
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
393
|
if (expectedSenderNonce != null) {
|
|
504
394
|
if (out.recipientNonce == null || !guard.crypto.constantTimeEqual(out.recipientNonce, expectedSenderNonce)) {
|
|
505
395
|
throw _err("scep/nonce-mismatch", "the pkiMessage recipientNonce does not echo the sent senderNonce (RFC 8894 sec. 3.2.1.5)");
|
|
506
396
|
}
|
|
507
397
|
}
|
|
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
398
|
var carriesEnvelope = !(messageType === "CertRep" && (out.pkiStatus === "FAILURE" || out.pkiStatus === "PENDING"));
|
|
514
399
|
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
400
|
_assertValidEnvelope(v.eContent, messageType);
|
|
520
401
|
} 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
402
|
if (v.eContent != null) {
|
|
526
403
|
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
404
|
}
|
|
@@ -534,18 +411,11 @@ async function parse(bytes, opts) {
|
|
|
534
411
|
throw e;
|
|
535
412
|
}
|
|
536
413
|
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
414
|
if (messageType === "CertRep" && out.pkiStatus === "SUCCESS") {
|
|
542
415
|
var payload = _readIssuancePayload(dec.content);
|
|
543
416
|
out.certificates = payload.certificates;
|
|
544
417
|
out.crls = payload.crls;
|
|
545
418
|
} 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
419
|
try { schemaCsr.parse(dec.content); }
|
|
550
420
|
catch (e) { throw _err("scep/bad-request-payload", "a " + messageType + " messageData must be a PKCS#10 CertificationRequest (RFC 8894 sec. 3.3.1)", e); }
|
|
551
421
|
}
|
|
@@ -554,34 +424,20 @@ async function parse(bytes, opts) {
|
|
|
554
424
|
}
|
|
555
425
|
var _PARSE_KEYS = { recipientKey: 1, expectedSenderNonce: 1, signerCert: 1 };
|
|
556
426
|
|
|
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
427
|
var DEFAULT_TIMEOUT = C.TIME.seconds(30);
|
|
569
428
|
var MAX_TIMEOUT = C.TIME.seconds(600);
|
|
570
429
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
var
|
|
574
|
-
var
|
|
575
|
-
var
|
|
576
|
-
var CT_PLAIN = "text/plain"; // GetCACaps, a newline-separated capability list
|
|
430
|
+
var CT_CA_CERT = "application/x-x509-ca-cert";
|
|
431
|
+
var CT_CA_RA_CERT = "application/x-x509-ca-ra-cert";
|
|
432
|
+
var CT_PKI_MESSAGE = "application/x-pki-message";
|
|
433
|
+
var CT_NEXT_CA_CERT = "application/x-x509-next-ca-cert";
|
|
434
|
+
var CT_PLAIN = "text/plain";
|
|
577
435
|
|
|
578
436
|
function _isWs(c) { return c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d || c === 0x0b || c === 0x0c; }
|
|
579
437
|
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; }
|
|
580
438
|
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; }
|
|
581
439
|
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); }
|
|
582
440
|
|
|
583
|
-
// A case-insensitive single-header read. A duplicated header is refused: an injected transport promises
|
|
584
|
-
// only { status, headers, body }, and a smuggled duplicate must never be silently coalesced.
|
|
585
441
|
function _header(headers, name) {
|
|
586
442
|
if (!headers || typeof headers !== "object") return null;
|
|
587
443
|
var found = null, seen = 0, k;
|
|
@@ -592,7 +448,6 @@ function _header(headers, name) {
|
|
|
592
448
|
if (seen > 1) throw _err("scep/bad-response", "the response carried more than one " + name + " header");
|
|
593
449
|
return found == null ? null : String(found);
|
|
594
450
|
}
|
|
595
|
-
// The bare media type of a Content-Type (strip parameters, lower-case), by character scan (no regex).
|
|
596
451
|
function _mediaType(ct) {
|
|
597
452
|
if (ct == null) return "";
|
|
598
453
|
var s = String(ct), semi = s.indexOf(";");
|
|
@@ -602,73 +457,45 @@ function _bodyBuf(body) {
|
|
|
602
457
|
if (Buffer.isBuffer(body)) return body;
|
|
603
458
|
if (body == null) return Buffer.alloc(0);
|
|
604
459
|
if (typeof body === "string") return Buffer.from(body, "utf8");
|
|
605
|
-
// A BufferSource (Uint8Array, ArrayBuffer, DataView) from an injected transport: normalize its raw bytes
|
|
606
|
-
// rather than String()-coercing it, which would turn a Uint8Array into its "48,1,..." toString.
|
|
607
460
|
return guard.bytes.snapshotSource(body, ScepError, "scep/bad-response", "the response body");
|
|
608
461
|
}
|
|
609
|
-
// The byte length of a transport response body, read without a full copy so the size cap is enforced before
|
|
610
|
-
// the body is materialized (cap-before-copy): a string is measured as UTF-8, a Buffer or other BufferSource
|
|
611
|
-
// through the byte guard, which reads the length via a captured intrinsic rather than a shadowable accessor.
|
|
612
462
|
function _responseLen(body) {
|
|
613
463
|
if (body == null) return 0;
|
|
614
464
|
if (typeof body === "string") return Buffer.byteLength(body, "utf8");
|
|
615
465
|
if (Buffer.isBuffer(body)) return guard.bytes.lengthOf(body);
|
|
616
466
|
return guard.bytes.lengthOf(guard.bytes.source(body, ScepError, "scep/bad-response", "the response body"));
|
|
617
467
|
}
|
|
618
|
-
// Map opts.tls to the transport request.tls shape. rejectUnauthorized is NOT set -- the transport forces
|
|
619
|
-
// it on for an https connection.
|
|
620
468
|
function _tlsForRequest(opts) {
|
|
621
469
|
var t = opts.tls || {};
|
|
622
470
|
return { anchors: t.anchors, useSystemStore: t.useSystemStore, cert: t.cert, key: t.key, minVersion: t.minVersion, servername: t.servername, checkServerIdentity: t.checkServerIdentity };
|
|
623
471
|
}
|
|
624
|
-
// Resolve a redirect target: refuse an https->http downgrade (a stripping MITM) and a cross-origin
|
|
625
|
-
// redirect on a non-idempotent method without opt-in; an unparseable Location is scep/bad-url.
|
|
626
472
|
function _redirectTarget(current, location, method, allowCrossOrigin) {
|
|
627
473
|
if (location == null || _trim(String(location)) === "") throw _err("scep/bad-response", "a redirect response carried no Location header");
|
|
628
474
|
var resolved;
|
|
629
475
|
try { resolved = new URL(String(location), current.href); }
|
|
630
476
|
catch (e) { throw _err("scep/bad-url", "a redirect Location did not parse: " + location, e); }
|
|
631
|
-
// SCEP is an HTTP operation (RFC 8894 sec. 4): a redirect target on any other scheme (ftp:, file:, ...)
|
|
632
|
-
// is refused before it reaches the transport, so a CA cannot steer an injected transport off HTTP.
|
|
633
477
|
if (resolved.protocol !== "http:" && resolved.protocol !== "https:") throw _err("scep/bad-url", "a redirect to a non-HTTP scheme is refused: " + resolved.protocol);
|
|
634
478
|
if (current.protocol === "https:" && resolved.protocol !== "https:") throw _err("scep/insecure-redirect", "a redirect from https to " + resolved.protocol + " is refused");
|
|
635
479
|
var safe = method === "GET" || method === "HEAD";
|
|
636
480
|
if (resolved.origin !== current.origin && !safe && !allowCrossOrigin) throw _err("scep/cross-origin-redirect", "a cross-origin redirect on a " + method + " needs opts.allowCrossOriginRedirect");
|
|
637
481
|
return resolved;
|
|
638
482
|
}
|
|
639
|
-
// Build a SCEP operation URL: the caller's base URL with ?operation=<op> appended (RFC 8894 sec. 4.1;
|
|
640
|
-
// SCEPPATH is whatever the base carries, the CA typically ignores it).
|
|
641
483
|
function _scepUrl(baseUrl, operation) {
|
|
642
484
|
var url;
|
|
643
485
|
try { url = new URL(String(baseUrl)); }
|
|
644
486
|
catch (e) { throw _err("scep/bad-url", "the SCEP base URL did not parse: " + String(baseUrl), e); }
|
|
645
|
-
// SCEP is an HTTP operation (RFC 8894 sec. 4): any other scheme (ftp:, file:, ...) is refused before the
|
|
646
|
-
// URL reaches the transport, so an injected transport cannot be steered off HTTP.
|
|
647
487
|
if (url.protocol !== "http:" && url.protocol !== "https:") throw _err("scep/bad-url", "the SCEP base URL must be http or https, got " + url.protocol);
|
|
648
488
|
if (url.hash) throw _err("scep/bad-url", "the SCEP base URL must not carry a fragment");
|
|
649
489
|
url.searchParams.set("operation", operation);
|
|
650
490
|
return url;
|
|
651
491
|
}
|
|
652
|
-
// The transport request / redirect loop. Returns the terminal transport response ({ status, headers,
|
|
653
|
-
// body }). Digest / Basic auth is not answered (deferred); a 401 is returned to the verb, which throws.
|
|
654
492
|
function _drive(method, url, body, headers, opts, transport, budgets) {
|
|
655
493
|
var redirects = 0, curMethod = method, curBody = body, initialOrigin = url.origin;
|
|
656
|
-
// Snapshot the caller's TLS fields and redirect policy once, at the start, before the first transport call:
|
|
657
|
-
// the redirect loop reruns per hop after each transport promise settles, so a hop must use the identity and
|
|
658
|
-
// policy the initial hop did even if opts (or opts.tls) is replaced while a request is in flight.
|
|
659
494
|
var tlsBase = _tlsForRequest(opts);
|
|
660
495
|
var allowCrossOrigin = !!opts.allowCrossOriginRedirect;
|
|
661
|
-
// The mTLS client identity (cert / key / pinned SNI) is sent ONLY to the origin the caller intended; a
|
|
662
|
-
// cross-origin redirect gets a copy with those stripped, so a CA that redirects to another origin cannot
|
|
663
|
-
// make the client authenticate to it with the caller's client credential. Derived fresh each hop, so a
|
|
664
|
-
// redirect back to the original origin restores it.
|
|
665
496
|
function _hopTls(current) {
|
|
666
497
|
var t = tlsBase;
|
|
667
498
|
if (current.origin !== initialOrigin) {
|
|
668
|
-
// Every cross-origin hop overrides the client identity with an explicit null, unconditionally: an
|
|
669
|
-
// omitted field falls back to the transport's own configured defaults (http-transport resolves
|
|
670
|
-
// reqTls.cert !== undefined ? reqTls.cert : tlsDefaults.cert), so the override must fire even when the
|
|
671
|
-
// request-level tls carries no credential, or a transport default would still reach the new origin.
|
|
672
499
|
t = Object.assign({}, t);
|
|
673
500
|
t.cert = null; t.key = null; t.servername = null;
|
|
674
501
|
}
|
|
@@ -703,7 +530,6 @@ function _client(method, url, body, headers, opts) {
|
|
|
703
530
|
};
|
|
704
531
|
return _drive(method, url, body, Object.assign({}, headers), opts, transport, budgets);
|
|
705
532
|
}
|
|
706
|
-
// Require a 200 with one of the expected media types; otherwise a typed error. Returns the body Buffer.
|
|
707
533
|
function _requireOk(res, expected, what) {
|
|
708
534
|
var status = res.status;
|
|
709
535
|
if (status !== 200) {
|
|
@@ -717,8 +543,6 @@ function _requireOk(res, expected, what) {
|
|
|
717
543
|
return _bodyBuf(res.body);
|
|
718
544
|
}
|
|
719
545
|
|
|
720
|
-
// The GetCACaps keyword set (RFC 8894 sec. 3.5.2, Table 7). Comparison is case-folded; the canonical
|
|
721
|
-
// spelling is stored. An unknown keyword is ignored (sec. 3.5.2). SCEPStandard implies the sec. 2.9 set.
|
|
722
546
|
var CAP_CANON = { AES: "AES", DES3: "DES3", GETNEXTCACERT: "GetNextCACert", POSTPKIOPERATION: "POSTPKIOperation",
|
|
723
547
|
RENEWAL: "Renewal", "SHA-1": "SHA-1", "SHA-256": "SHA-256", "SHA-512": "SHA-512", SCEPSTANDARD: "SCEPStandard" };
|
|
724
548
|
function _splitLines(s) {
|
|
@@ -789,29 +613,17 @@ function _assertStrongProfile(caps, profile) {
|
|
|
789
613
|
async function getCACaps(baseUrl, opts) {
|
|
790
614
|
opts = guard.identifier.optionsObject(opts, _err, "scep/bad-input", "pki.scep.getCACaps options");
|
|
791
615
|
guard.identifier.assertKnownKeys(opts, _CACAPS_KEYS, _err, "scep/bad-input", "pki.scep.getCACaps has an unknown option: ");
|
|
792
|
-
// Capture the strong-profile policy at the door: the downgrade check runs only after the GET await, so a
|
|
793
|
-
// caller must not be able to relax its own guard by mutating opts while the request is in flight.
|
|
794
616
|
var profile = { expectSCEPStandard: !!opts.expectSCEPStandard, requireStrongProfile: !!opts.requireStrongProfile };
|
|
795
617
|
var res = await _client("GET", _scepUrl(baseUrl, "GetCACaps"), null, {}, opts);
|
|
796
|
-
// An HTTP error or empty body means "none supported" (RFC 8894 sec. 3.5.1); a non-200 that is not a
|
|
797
|
-
// clean error is still surfaced so a caller is not silently told "no capabilities" on a broken CA.
|
|
798
618
|
if (res.status === 200) {
|
|
799
|
-
// A conforming GetCACaps response carries content-type text/plain (RFC 8894 sec. 3.5.1). Enforcing it
|
|
800
|
-
// stops a proxy's 200 HTML error page from being read as an empty (no capabilities) capability set.
|
|
801
619
|
var caps = parseCapabilities(_requireOk(res, [CT_PLAIN], "GetCACaps"));
|
|
802
620
|
_assertStrongProfile(caps, profile);
|
|
803
621
|
return caps;
|
|
804
622
|
}
|
|
805
|
-
// A CA that does not implement GetCACaps signals it with an HTTP error, which RFC 8894 sec. 3.5.1 reads
|
|
806
|
-
// as "no capabilities advertised": the codes that mean the operation is unsupported or unrecognized are
|
|
807
|
-
// 400 (an old CA that does not parse the query), 404 (no such operation), 405 (GET not allowed on it), and
|
|
808
|
-
// 501 Not Implemented. A transient server error (500, 502, 503) is surfaced instead of masked.
|
|
809
623
|
if (res.status === 204 || res.status === 400 || res.status === 404 || res.status === 405 || res.status === 501) { _assertStrongProfile({}, profile); return {}; }
|
|
810
624
|
throw _err("scep/http-error", "the SCEP GetCACaps returned HTTP " + res.status);
|
|
811
625
|
}
|
|
812
626
|
|
|
813
|
-
// The exact SPKI byte-match that identifies an issued certificate (RFC 5272 sec. 4.1); positional
|
|
814
|
-
// guessing is forbidden. `certs` is an array of raw cert Buffers, `wantSpki` the SPKI Buffer to match.
|
|
815
627
|
function findIssuedCert(certs, wantSpki) {
|
|
816
628
|
if (!Buffer.isBuffer(wantSpki)) return null;
|
|
817
629
|
for (var i = 0; i < certs.length; i++) {
|
|
@@ -822,12 +634,8 @@ function findIssuedCert(certs, wantSpki) {
|
|
|
822
634
|
}
|
|
823
635
|
return null;
|
|
824
636
|
}
|
|
825
|
-
// Normalize an expected fingerprint (hex string, optionally colon/space separated, or a Buffer) to
|
|
826
|
-
// { algorithm, value }. Character-scan the hex (Hard rule #11).
|
|
827
637
|
function _normalizeFingerprint(fp, algName) {
|
|
828
638
|
var algorithm = _foldLower(String(algName == null ? "sha256" : algName));
|
|
829
|
-
// Validate the digest name at the entry point so a misspelled or build-unavailable fingerprintAlgorithm
|
|
830
|
-
// is the typed scep/bad-input the client API documents, not a native createHash exception downstream.
|
|
831
639
|
try { nodeCrypto.createHash(algorithm); }
|
|
832
640
|
catch (_e) { throw _err("scep/bad-input", "fingerprintAlgorithm " + JSON.stringify(algorithm) + " is not an available hash"); }
|
|
833
641
|
var value;
|
|
@@ -877,9 +685,6 @@ function _normalizeFingerprint(fp, algName) {
|
|
|
877
685
|
async function getCACert(baseUrl, opts) {
|
|
878
686
|
opts = guard.identifier.optionsObject(opts, _err, "scep/bad-input", "pki.scep.getCACert options");
|
|
879
687
|
guard.identifier.assertKnownKeys(opts, _CACERT_KEYS, _err, "scep/bad-input", "pki.scep.getCACert has an unknown option: ");
|
|
880
|
-
// Normalize the expected fingerprint at the synchronous entry point: fail fast on a bad hash before the
|
|
881
|
-
// network call, and pin against the value the caller supplied at the call rather than one mutated while
|
|
882
|
-
// the GET is in flight.
|
|
883
688
|
var fp = opts.expectedFingerprint != null ? _normalizeFingerprint(opts.expectedFingerprint, opts.fingerprintAlgorithm) : null;
|
|
884
689
|
var res = await _client("GET", _scepUrl(baseUrl, "GetCACert"), null, {}, opts);
|
|
885
690
|
var body = _requireOk(res, [CT_CA_CERT, CT_CA_RA_CERT], "GetCACert");
|
|
@@ -889,7 +694,6 @@ async function getCACert(baseUrl, opts) {
|
|
|
889
694
|
certs = [Buffer.from(body)];
|
|
890
695
|
single = true;
|
|
891
696
|
} else {
|
|
892
|
-
// parseCertsOnly fails closed with scep/no-certificates on an empty CertificateSet, so certs is non-empty here.
|
|
893
697
|
certs = schemaCms.parseCertsOnly(body, _err, "scep").certificates;
|
|
894
698
|
}
|
|
895
699
|
var caCertificate = null;
|
|
@@ -902,18 +706,60 @@ async function getCACert(baseUrl, opts) {
|
|
|
902
706
|
if (!matched) throw _err("scep/fingerprint-mismatch", "no GetCACert certificate matched the expected " + fp.algorithm + " fingerprint (RFC 8894 sec. 2.2)");
|
|
903
707
|
caCertificate = matched;
|
|
904
708
|
} else if (single) {
|
|
905
|
-
// A single-certificate response (application/x-x509-ca-cert) is unambiguously the CA certificate. A
|
|
906
|
-
// ca-ra chain carries the CA, any intermediates, and RA certificate(s) in a CMS CertificateSet whose
|
|
907
|
-
// DER order does not identify the CA role, so caCertificate stays null there unless a fingerprint pins it.
|
|
908
709
|
caCertificate = certs[0];
|
|
909
710
|
}
|
|
910
711
|
return { caCertificate: caCertificate, certificates: certs };
|
|
911
712
|
}
|
|
912
713
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
714
|
+
/**
|
|
715
|
+
* @primitive pki.scep.getNextCACert
|
|
716
|
+
* @signature pki.scep.getNextCACert(baseUrl, opts) -> Promise<{ certificates }>
|
|
717
|
+
* @since 0.6.14
|
|
718
|
+
* @status stable
|
|
719
|
+
* @spec RFC 8894
|
|
720
|
+
* @related pki.scep.getCACert, pki.cms.verify
|
|
721
|
+
*
|
|
722
|
+
* Retrieve a SCEP CA's next (rollover) certificate (RFC 8894 sec. 4.7): a GET
|
|
723
|
+
* `?operation=GetNextCACert` returning a SignedData signed by the CURRENT CA signing key
|
|
724
|
+
* (`application/x-x509-next-ca-cert`), whose content is a degenerate certs-only SignedData carrying the
|
|
725
|
+
* CA certificate(s) to install when the current one expires. The current CA certificate the caller
|
|
726
|
+
* already holds authenticates the response: the outer signature is verified AND pinned to that
|
|
727
|
+
* certificate's key, so a self-consistent CMS signed by any other (embedded) certificate is refused
|
|
728
|
+
* (`scep/untrusted-signer`) rather than trusted as the rollover certificate. `caCertificate` is
|
|
729
|
+
* required; an unauthenticated rollover certificate becomes a future trust anchor, so it is refused
|
|
730
|
+
* rather than returned. The verified next certificate(s) are returned in `certificates`.
|
|
731
|
+
*
|
|
732
|
+
* @opts
|
|
733
|
+
* - `caCertificate` -- the current SCEP CA certificate DER; the response is authenticated against its key (required).
|
|
734
|
+
* - `transport` / `timeout` / `maxResponseBytes` / `maxRedirects` / `tls` / `allowCrossOriginRedirect` -- transport bounds.
|
|
735
|
+
* @example
|
|
736
|
+
* // requires: caCertDer -- the current CA certificate (from getCACert), scepTransport
|
|
737
|
+
* var next = await pki.scep.getNextCACert("http://ca.example/scep", { caCertificate: caCertDer, transport: scepTransport });
|
|
738
|
+
* next.certificates[0]; // the next CA certificate DER, to install when the current one expires
|
|
739
|
+
*/
|
|
740
|
+
async function getNextCACert(baseUrl, opts) {
|
|
741
|
+
opts = guard.identifier.optionsObject(opts, _err, "scep/bad-input", "pki.scep.getNextCACert options");
|
|
742
|
+
guard.identifier.assertKnownKeys(opts, _NEXTCACERT_KEYS, _err, "scep/bad-input", "pki.scep.getNextCACert has an unknown option: ");
|
|
743
|
+
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)");
|
|
744
|
+
var currentCa = guard.bytes.snapshotSource(opts.caCertificate, ScepError, "scep/bad-input", "the caCertificate");
|
|
745
|
+
_assertSigningKeyUsage(currentCa, "the caCertificate");
|
|
746
|
+
var url = _scepUrl(baseUrl, "GetNextCACert");
|
|
747
|
+
var res = await _client("GET", url, null, {}, opts);
|
|
748
|
+
var body = _requireOk(res, [CT_NEXT_CA_CERT], "GetNextCACert");
|
|
749
|
+
var v;
|
|
750
|
+
try { v = await cmsVerify.verify(body, { certs: [currentCa] }); }
|
|
751
|
+
catch (e) {
|
|
752
|
+
if (e && e.code === "cms/detached-content-required") throw _err("scep/bad-response", "the GetNextCACert SignedData has no content; the rollover certificate(s) are the content (RFC 8894 sec. 4.7.1)", e);
|
|
753
|
+
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);
|
|
754
|
+
throw e;
|
|
755
|
+
}
|
|
756
|
+
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);
|
|
757
|
+
if (v.valid !== true) throw _err("scep/bad-signature", "the GetNextCACert outer signature did not verify (RFC 8894 sec. 4.7.1)");
|
|
758
|
+
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)");
|
|
759
|
+
var certs = schemaCms.parseCertsOnly(v.eContent, _err, "scep").certificates;
|
|
760
|
+
return { certificates: certs };
|
|
761
|
+
}
|
|
762
|
+
|
|
917
763
|
function _defaultTransactionId() {
|
|
918
764
|
return nodeCrypto.randomBytes(16).toString("hex");
|
|
919
765
|
}
|
|
@@ -924,22 +770,10 @@ async function _pkiOperation(messageType, baseUrl, opts, verbName) {
|
|
|
924
770
|
if (opts.caCert == null) throw _err("scep/bad-input", "a caCert (the SCEP CA certificate DER) is required");
|
|
925
771
|
if (opts.signer == null || typeof opts.signer !== "object") throw _err("scep/bad-input", "a signer { cert, key } is required");
|
|
926
772
|
if (opts.recipientKey == null || typeof opts.recipientKey !== "object") throw _err("scep/bad-input", "a recipientKey { cert, key } is required to decrypt the CA response");
|
|
927
|
-
// Snapshot the response-authentication inputs at the synchronous entry point: the response is
|
|
928
|
-
// authenticated (signerCert) and decrypted (recipientKey) only after the build and POST awaits, so a
|
|
929
|
-
// caller mutating opts mid-flight cannot switch the trusted signer, or the CA the request was encrypted
|
|
930
|
-
// to, out from under the exchange. Certificate bytes are copied; the recipient private key and the
|
|
931
|
-
// request signer are held by reference.
|
|
932
773
|
var caCert = guard.bytes.snapshotSource(opts.caCert, ScepError, "scep/bad-input", "the caCert");
|
|
933
774
|
var signerCert = opts.responderCert != null ? guard.bytes.snapshotSource(opts.responderCert, ScepError, "scep/bad-input", "the responderCert") : caCert;
|
|
934
|
-
// Capture the recipient-key FIELDS at the door (snapshotSource copies the cert bytes; the private key is
|
|
935
|
-
// held by reference), not the opts.recipientKey object: a caller replacing opts.recipientKey.cert or .key
|
|
936
|
-
// while the POST is in flight must not switch the credential the response is decrypted with.
|
|
937
775
|
var recipientKey = _captureRecipientKey(opts.recipientKey);
|
|
938
776
|
var signer = opts.signer;
|
|
939
|
-
// The transport options are read by _client only after the build await, so snapshot them at the door too:
|
|
940
|
-
// a caller replacing opts.transport (rerouting the completed request) or opts.tls.cert/.key (presenting the
|
|
941
|
-
// next operation's mTLS identity) while the POST is in flight must not take effect. tls is shallow-copied
|
|
942
|
-
// so a replaced field does not reach the transport; the transport function and budgets are held by value.
|
|
943
777
|
var clientOpts = {
|
|
944
778
|
transport: opts.transport,
|
|
945
779
|
tls: opts.tls != null ? Object.assign({}, opts.tls) : opts.tls,
|
|
@@ -948,8 +782,6 @@ async function _pkiOperation(messageType, baseUrl, opts, verbName) {
|
|
|
948
782
|
timeout: opts.timeout,
|
|
949
783
|
maxResponseBytes: opts.maxResponseBytes,
|
|
950
784
|
};
|
|
951
|
-
// Resolve the request URL at the door: _scepUrl reads String(baseUrl) only when the POST is sent (after the
|
|
952
|
-
// build await), so a baseUrl whose toString() the caller changes mid-flight could otherwise redirect it.
|
|
953
785
|
var postUrl = _scepUrl(baseUrl, "PKIOperation");
|
|
954
786
|
var csrView = guard.bytes.snapshotSource(opts.csr, ScepError, "scep/bad-input", "the csr");
|
|
955
787
|
var spki;
|
|
@@ -962,13 +794,8 @@ async function _pkiOperation(messageType, baseUrl, opts, verbName) {
|
|
|
962
794
|
var body = _requireOk(res, [CT_PKI_MESSAGE], "PKIOperation");
|
|
963
795
|
var rep = await parse(body, { signerCert: signerCert, expectedSenderNonce: senderNonce, recipientKey: recipientKey });
|
|
964
796
|
if (rep.messageType !== "CertRep") throw _err("scep/unexpected-message-type", "the SCEP response is a " + rep.messageType + ", not a CertRep");
|
|
965
|
-
// Every message in one SCEP transaction carries the same transactionID (RFC 8894 sec. 3.2.1.1); a
|
|
966
|
-
// response whose transactionID does not echo the request is for a different exchange and is refused,
|
|
967
|
-
// even if its nonce and signer check out.
|
|
968
797
|
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)");
|
|
969
798
|
if (rep.pkiStatus === "FAILURE") {
|
|
970
|
-
// Surface the CA's reason as structured fields, not only interpolated text, so a caller can branch on
|
|
971
|
-
// the failInfo enumerant (RFC 8894 sec. 3.3.2.1) without parsing the human-readable message.
|
|
972
799
|
var failErr = _err("scep/enrollment-failed", "the CA refused the request with failInfo " + JSON.stringify(rep.failInfo) + (rep.failInfoText ? " (" + rep.failInfoText + ")" : ""));
|
|
973
800
|
failErr.failInfo = rep.failInfo;
|
|
974
801
|
failErr.failInfoText = rep.failInfoText;
|
|
@@ -978,9 +805,6 @@ async function _pkiOperation(messageType, baseUrl, opts, verbName) {
|
|
|
978
805
|
var repCerts = rep.certificates || [];
|
|
979
806
|
var issued = findIssuedCert(repCerts, spki.bytes);
|
|
980
807
|
if (issued == null) throw _err("scep/no-issued-cert", "the SUCCESS CertRep did not carry a certificate matching the request public key");
|
|
981
|
-
// A CMS CertificateSet states no issuance order, so a second certificate carrying the request public key
|
|
982
|
-
// (a same-key renewal returning old and new, or a duplicate) leaves the issued certificate ambiguous; it
|
|
983
|
-
// is refused rather than resolved by position.
|
|
984
808
|
if (findIssuedCert(repCerts.filter(function (c) { return c !== issued; }), spki.bytes) != null) {
|
|
985
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)");
|
|
986
810
|
}
|
|
@@ -1049,6 +873,7 @@ var _TRANSPORT_KEYS = { transport: 1, timeout: 1, maxResponseBytes: 1, maxRedire
|
|
|
1049
873
|
function _withKeys(extra) { var o = {}, k; for (k in _TRANSPORT_KEYS) o[k] = 1; for (k in extra) o[k] = 1; return o; }
|
|
1050
874
|
var _CACAPS_KEYS = _withKeys({ expectSCEPStandard: 1, requireStrongProfile: 1 });
|
|
1051
875
|
var _CACERT_KEYS = _withKeys({ expectedFingerprint: 1, fingerprintAlgorithm: 1 });
|
|
876
|
+
var _NEXTCACERT_KEYS = _withKeys({ caCertificate: 1 });
|
|
1052
877
|
var _ENROLL_KEYS = _withKeys({ csr: 1, caCert: 1, signer: 1, recipientKey: 1, responderCert: 1, transactionId: 1 });
|
|
1053
878
|
|
|
1054
879
|
module.exports = {
|
|
@@ -1056,6 +881,7 @@ module.exports = {
|
|
|
1056
881
|
parse: parse,
|
|
1057
882
|
getCACaps: getCACaps,
|
|
1058
883
|
getCACert: getCACert,
|
|
884
|
+
getNextCACert: getNextCACert,
|
|
1059
885
|
enroll: enroll,
|
|
1060
886
|
renew: renew,
|
|
1061
887
|
parseCapabilities: parseCapabilities,
|