@blamejs/pki 0.6.0 → 0.6.2
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 +20 -0
- package/README.md +5 -3
- package/index.js +2 -0
- package/lib/cms-verify.js +17 -0
- package/lib/constants.js +5 -0
- package/lib/ct.js +270 -13
- package/lib/framework-error.js +7 -0
- package/lib/oid.js +10 -0
- package/lib/path-validate.js +70 -4
- package/lib/scep.js +562 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/scep.js
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @module pki.scep
|
|
6
|
+
* @nav Schema
|
|
7
|
+
* @title SCEP
|
|
8
|
+
* @fullname Simple Certificate Enrolment Protocol (RFC 8894)
|
|
9
|
+
* @order 193
|
|
10
|
+
* @slug scep
|
|
11
|
+
*
|
|
12
|
+
* @intro
|
|
13
|
+
* The SCEP pkiMessage codec. SCEP wraps a PKCS#10 certification request (or an
|
|
14
|
+
* issued certificate) in two nested CMS layers: an inner EnvelopedData
|
|
15
|
+
* (`pkcsPKIEnvelope`) that encrypts the messageData to the recipient, and an
|
|
16
|
+
* outer SignedData that signs it under a set of authenticated transaction
|
|
17
|
+
* attributes -- messageType, transactionID, pkiStatus, failInfo, and the
|
|
18
|
+
* sender/recipient nonces (RFC 8894 sec. 3). `pki.scep.build` assembles a
|
|
19
|
+
* request pkiMessage (PKCSReq / RenewalReq); `pki.scep.parse` verifies the
|
|
20
|
+
* outer signature, reads the transaction attributes BOUND to the verified
|
|
21
|
+
* signer, and (given the recipient key) decrypts the pkcsPKIEnvelope to the
|
|
22
|
+
* messageData.
|
|
23
|
+
*
|
|
24
|
+
* The transaction attributes are surfaced only from a signature that verified:
|
|
25
|
+
* a pkiMessage whose outer signature does not check is refused, never returned
|
|
26
|
+
* with a false verdict. The pkcsPKIEnvelope is built with AES-128-CBC. This is
|
|
27
|
+
* the message layer, transport-agnostic; a live SCEP CA is driven over
|
|
28
|
+
* pki.transport separately.
|
|
29
|
+
*
|
|
30
|
+
* @card
|
|
31
|
+
* Build and read SCEP (RFC 8894) enrollment messages: a PKCSReq / RenewalReq
|
|
32
|
+
* request wrapped in the signed-over-encrypted CMS layering, and a PKCSReq,
|
|
33
|
+
* RenewalReq, or CertRep parsed back to its verified transaction attributes and
|
|
34
|
+
* decrypted messageData.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
var frameworkError = require("./framework-error");
|
|
38
|
+
var ScepError = frameworkError.ScepError;
|
|
39
|
+
var oid = require("./oid");
|
|
40
|
+
var asn1 = require("./asn1-der");
|
|
41
|
+
var b = asn1.build;
|
|
42
|
+
var cmsSign = require("./cms-sign");
|
|
43
|
+
var cmsVerify = require("./cms-verify");
|
|
44
|
+
var cmsEncrypt = require("./cms-encrypt");
|
|
45
|
+
var cmsDecrypt = require("./cms-decrypt");
|
|
46
|
+
require("./path-validate"); // side-effect: injects the certification-path signature engine into csr-verify at load
|
|
47
|
+
var csrVerify = require("./csr-verify"); // verify an embedded PKCS#10's proof-of-possession before enveloping it
|
|
48
|
+
var schemaCms = require("./schema-cms");
|
|
49
|
+
var schemaCsr = require("./schema-csr");
|
|
50
|
+
var schemaX509 = require("./schema-x509");
|
|
51
|
+
var schemaCrl = require("./schema-crl");
|
|
52
|
+
var pkix = require("./schema-pkix");
|
|
53
|
+
var guard = require("./guard-all");
|
|
54
|
+
var C = require("./constants");
|
|
55
|
+
var nodeCrypto = require("node:crypto");
|
|
56
|
+
|
|
57
|
+
function _err(code, msg, cause) { return new ScepError(code, msg, cause); }
|
|
58
|
+
|
|
59
|
+
// The namespace pkix.keyUsageOf reads a certificate's keyUsage extension through.
|
|
60
|
+
var _KU_NS = pkix.makeNS("scep", ScepError, oid);
|
|
61
|
+
|
|
62
|
+
// The messageType values are a DATA table (RFC 8894 sec. 3.2.1.2, Table 3), never a switch: name <-> the
|
|
63
|
+
// decimal ASCII a PrintableString carries. RFC 8894 renumbered the legacy draft (RenewalReq is 17, and
|
|
64
|
+
// there is no GetCertInitial -- it is CertPoll 20).
|
|
65
|
+
var MESSAGE_TYPE = { CertRep: "3", RenewalReq: "17", PKCSReq: "19", CertPoll: "20", GetCert: "21", GetCRL: "22" };
|
|
66
|
+
// pkiStatus (Table 4): SUCCESS 0, FAILURE 2, PENDING 3 -- note there is NO value 1.
|
|
67
|
+
var PKI_STATUS = { SUCCESS: "0", FAILURE: "2", PENDING: "3" };
|
|
68
|
+
// failInfo (Table 5).
|
|
69
|
+
var FAIL_INFO = { badAlg: "0", badMessageCheck: "1", badRequest: "2", badTime: "3", badCertId: "4" };
|
|
70
|
+
|
|
71
|
+
// name -> code and code -> name, built once from each table so a lookup is a data read, not a switch.
|
|
72
|
+
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; }
|
|
73
|
+
var MESSAGE_TYPE_BY_CODE = _invert(MESSAGE_TYPE), PKI_STATUS_BY_CODE = _invert(PKI_STATUS), FAIL_INFO_BY_CODE = _invert(FAIL_INFO);
|
|
74
|
+
|
|
75
|
+
// The message types pki.scep.parse reads: the enrollment request pair a client builds and the CertRep
|
|
76
|
+
// response it receives. The CertPoll / GetCert / GetCRL client queries are refused (see parse).
|
|
77
|
+
var _PARSE_SUPPORTED = { PKCSReq: 1, RenewalReq: 1, CertRep: 1 };
|
|
78
|
+
|
|
79
|
+
// The RFC 8894 sec. 3 EnvelopedData content-encryption profile. AES-128-CBC is the mandatory strong
|
|
80
|
+
// cipher; the toolkit's own cms.encrypt refuses the weak legacy ciphers structurally, so the builder
|
|
81
|
+
// only names the one it emits.
|
|
82
|
+
var ENVELOPE_CIPHER = "aes-128-cbc";
|
|
83
|
+
|
|
84
|
+
// The empty external content a conforming FAILURE / PENDING CertRep is verified against: its
|
|
85
|
+
// pkcsPKIEnvelope is omitted, so the SignedData carries no encapsulated content (RFC 8894 sec. 3.3.2).
|
|
86
|
+
var EMPTY_CONTENT = Buffer.alloc(0);
|
|
87
|
+
|
|
88
|
+
// A 16-byte nonce (RFC 8894 sec. 3.2.1.5): a caller value copied at the door and validated to width, or
|
|
89
|
+
// a fresh random one. The copy makes a caller value read after an await immune to concurrent mutation.
|
|
90
|
+
function _nonce(v, label) {
|
|
91
|
+
if (v == null) return nodeCrypto.randomBytes(16);
|
|
92
|
+
var buf = guard.bytes.snapshotSource(v, ScepError, "scep/bad-nonce", label);
|
|
93
|
+
if (buf.length !== 16) throw _err("scep/bad-nonce", label + " must be exactly 16 bytes (RFC 8894 sec. 3.2.1.5), got " + buf.length);
|
|
94
|
+
return buf;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// A caller PrintableString for a transaction id: bounded (the RFC sets no length, so cap it against a DoS)
|
|
98
|
+
// and restricted to the PrintableString character set (asn1.build.printable enforces the set).
|
|
99
|
+
function _transactionId(v) {
|
|
100
|
+
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)");
|
|
101
|
+
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");
|
|
102
|
+
return v;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Assert some bytes are a CMS ContentInfo wrapping a well-formed EnvelopedData whose encrypted content
|
|
106
|
+
// type is id-data -- the pkcsPKIEnvelope profile (RFC 8894 sec. 3.2, sec. 3.2.2). Delegated to the
|
|
107
|
+
// strict CMS parser, and checked structurally without decryption, so an absent, malformed, or
|
|
108
|
+
// off-profile envelope is refused for every envelope-carrying message whether or not a recipient key
|
|
109
|
+
// was supplied.
|
|
110
|
+
function _assertValidEnvelope(bytes, messageType) {
|
|
111
|
+
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)");
|
|
112
|
+
var parsed;
|
|
113
|
+
try { parsed = schemaCms.parse(bytes); }
|
|
114
|
+
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); }
|
|
115
|
+
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);
|
|
116
|
+
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);
|
|
117
|
+
// The EnvelopedData must actually carry ciphertext: an inner envelope whose encryptedContent is absent
|
|
118
|
+
// or a zero-length OCTET STRING has no messageData to recover (SCEP provides none out of band), so it
|
|
119
|
+
// is refused (RFC 8894 sec. 3.2).
|
|
120
|
+
var ct = parsed.encryptedContentInfo.encryptedContent;
|
|
121
|
+
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)");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Validate a SUCCESS CertRep's messageData as a degenerate certificates-only CMS SignedData (RFC 8894
|
|
125
|
+
// sec. 3.4) and surface its certificates and CRLs raw, for the caller to path-validate. The issued
|
|
126
|
+
// certificate(s) answer an enrollment; a CRL answers a GetCRL (sec. 3.3.4), so certificates and CRLs are
|
|
127
|
+
// accepted independently and a payload carrying neither is refused.
|
|
128
|
+
function _readIssuancePayload(der) {
|
|
129
|
+
var r;
|
|
130
|
+
try { r = schemaCms.parse(der); }
|
|
131
|
+
catch (e) { throw _err("scep/bad-response", "a SUCCESS CertRep messageData did not decode as a CMS SignedData (RFC 8894 sec. 3.4)", e); }
|
|
132
|
+
if (r.contentTypeName !== "signedData" || r.encapContentInfo.eContentType !== oid.byName("data") ||
|
|
133
|
+
r.encapContentInfo.eContent !== null || r.signerInfos.length !== 0) {
|
|
134
|
+
throw _err("scep/not-certs-only", "a SUCCESS CertRep messageData must be a degenerate certificates-only CMS SignedData (RFC 8894 sec. 3.4)");
|
|
135
|
+
}
|
|
136
|
+
// Validate each entry as a plain X.509 certificate / CRL: a universal-SEQUENCE CertificateChoice or
|
|
137
|
+
// RevocationInfoChoice must be a well-formed certificate / CRL, and a tagged alternative (attribute
|
|
138
|
+
// cert, other-message, otherRevInfo) is not a plain issuance object and is refused.
|
|
139
|
+
var certs = [], crls = [], i;
|
|
140
|
+
var rc = r.certificates || [], rl = r.crls || [];
|
|
141
|
+
for (i = 0; i < rc.length; i++) {
|
|
142
|
+
if (rc[i].tagClass !== "universal") throw _err("scep/bad-certificate", "a SUCCESS CertRep certificates field carried a tagged CertificateChoices alternative, not a plain X.509 certificate (RFC 8894 sec. 3.4)");
|
|
143
|
+
try { schemaX509.parse(rc[i].bytes); }
|
|
144
|
+
catch (e) { throw _err("scep/bad-certificate", "a SUCCESS CertRep carried a non-certificate in its certificates field", e); }
|
|
145
|
+
certs[i] = rc[i].bytes;
|
|
146
|
+
}
|
|
147
|
+
for (i = 0; i < rl.length; i++) {
|
|
148
|
+
if (rl[i].tagClass !== "universal") throw _err("scep/bad-crl", "a SUCCESS CertRep crls field carried a tagged RevocationInfoChoice alternative, not a plain X.509 CRL (RFC 5652 sec. 10.2.1)");
|
|
149
|
+
try { schemaCrl.parse(rl[i].bytes); }
|
|
150
|
+
catch (e) { throw _err("scep/bad-crl", "a SUCCESS CertRep carried a non-CRL in its crls field", e); }
|
|
151
|
+
crls[i] = rl[i].bytes;
|
|
152
|
+
}
|
|
153
|
+
if (certs.length === 0 && crls.length === 0) {
|
|
154
|
+
throw _err("scep/empty-response", "a SUCCESS CertRep messageData carried neither a certificate nor a CRL (RFC 8894 sec. 3.3.2)");
|
|
155
|
+
}
|
|
156
|
+
return { certificates: certs, crls: crls };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Build one SCEP transaction attribute { type, values:[valueDER] } for cms.sign's additionalSignedAttributes.
|
|
160
|
+
function _attr(name, valueDer) { return { type: oid.byName(name), values: [valueDer] }; }
|
|
161
|
+
|
|
162
|
+
// Find the single surfaced signed attribute of a given OID name (the values are raw AttributeValue
|
|
163
|
+
// DER). The CMS parser rejects a repeated attribute type (cms/duplicate-signed-attr) before these
|
|
164
|
+
// attributes are surfaced, so a SCEP transaction attribute appears at most once. It is also
|
|
165
|
+
// single-valued: a value SET that does not hold exactly one value is an ambiguous transaction state
|
|
166
|
+
// and is refused rather than resolved to the first value (RFC 8894 sec. 3.2.1).
|
|
167
|
+
function _find(attrs, name) {
|
|
168
|
+
var want = oid.byName(name);
|
|
169
|
+
for (var i = 0; i < attrs.length; i++) {
|
|
170
|
+
if (attrs[i].type !== want) continue;
|
|
171
|
+
if (!attrs[i].values || attrs[i].values.length !== 1) {
|
|
172
|
+
throw _err("scep/bad-attribute", "the " + name + " transaction attribute must carry exactly one value (RFC 8894 sec. 3.2.1)");
|
|
173
|
+
}
|
|
174
|
+
return attrs[i];
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
// Read a string transaction attribute to its JS string, requiring the exact ASN.1 string type RFC 8894
|
|
179
|
+
// sec. 3.2 pins for it (a generic string reader would accept an off-profile UTF8String or IA5String
|
|
180
|
+
// where a PrintableString is required). Returns null when the attribute is absent.
|
|
181
|
+
function _readStr(attrs, name, expectedTag) {
|
|
182
|
+
var a = _find(attrs, name);
|
|
183
|
+
if (a == null) return null;
|
|
184
|
+
var node;
|
|
185
|
+
try { node = asn1.decode(a.values[0]); }
|
|
186
|
+
catch (e) { throw _err("scep/bad-attribute", "the " + name + " transaction attribute is not decodable", e); }
|
|
187
|
+
if (node.tagClass !== "universal" || node.tagNumber !== expectedTag) {
|
|
188
|
+
throw _err("scep/bad-attribute", "the " + name + " transaction attribute is not the ASN.1 string type RFC 8894 sec. 3.2 requires");
|
|
189
|
+
}
|
|
190
|
+
try { return asn1.read.string(node); }
|
|
191
|
+
catch (e) { throw _err("scep/bad-attribute", "the " + name + " transaction attribute is not a readable string", e); }
|
|
192
|
+
}
|
|
193
|
+
var _T_PRINTABLE = asn1.TAGS.PRINTABLE_STRING, _T_UTF8 = asn1.TAGS.UTF8_STRING;
|
|
194
|
+
// Read a 16-byte OCTET STRING nonce attribute, or null when absent.
|
|
195
|
+
function _readNonce(attrs, name) {
|
|
196
|
+
var a = _find(attrs, name);
|
|
197
|
+
if (a == null) return null;
|
|
198
|
+
var n;
|
|
199
|
+
try { n = asn1.read.octetString(asn1.decode(a.values[0])); }
|
|
200
|
+
catch (e) { throw _err("scep/bad-nonce", "the " + name + " transaction attribute is not a readable OCTET STRING", e); }
|
|
201
|
+
if (n.length !== 16) throw _err("scep/bad-nonce", "the " + name + " nonce must be exactly 16 bytes (RFC 8894 sec. 3.2.1.5), got " + n.length);
|
|
202
|
+
return n;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Whether two certificates carry the same subject public key -- the SCEP signer-authentication test
|
|
206
|
+
// (a CertRep must be signed by the CA the client already holds, RFC 8894 sec. 3.1). Compared by the
|
|
207
|
+
// SubjectPublicKeyInfo so a re-encoded certificate of the same key still matches.
|
|
208
|
+
function _sameKey(certA, certB) {
|
|
209
|
+
var a, bb;
|
|
210
|
+
try { a = schemaX509.parse(certA).subjectPublicKeyInfo.bytes; bb = schemaX509.parse(certB).subjectPublicKeyInfo.bytes; }
|
|
211
|
+
catch (e) { throw _err("scep/bad-input", "a certificate could not be parsed for signer authentication: " + e.message, e); }
|
|
212
|
+
return guard.crypto.constantTimeEqual(a, bb);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// The pkcsPKIEnvelope uses RSAES-OAEP key transport, so the recipient MUST be an RSA key-transport
|
|
216
|
+
// certificate (RFC 8894 sec. 3). An EC / X25519 / ML-KEM recipient would make cms.encrypt emit a
|
|
217
|
+
// key-agreement or KEM recipient off the SCEP profile, so it is refused before the CMS dispatch.
|
|
218
|
+
function _assertRsaRecipient(recipientDer) {
|
|
219
|
+
var spki;
|
|
220
|
+
try { spki = schemaX509.parse(recipientDer).subjectPublicKeyInfo; }
|
|
221
|
+
catch (e) { throw _err("scep/bad-input", "the recipient is not a parseable certificate: " + e.message, e); }
|
|
222
|
+
if (spki.algorithm.oid !== oid.byName("rsaEncryption")) {
|
|
223
|
+
throw _err("scep/bad-recipient", "the SCEP recipient must be an RSA key-transport certificate (RFC 8894 sec. 3); its public-key algorithm is " + spki.algorithm.oid);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// A SCEP signer's certificate MUST be authorized to sign: its keyUsage extension, when present, must
|
|
228
|
+
// assert digitalSignature (RFC 8894 sec. 2.3 requires that bit specifically). A certificate whose
|
|
229
|
+
// keyUsage omits it, permitting only encryption or non-repudiation, is refused, so a signature it makes
|
|
230
|
+
// is not read as authenticated. A certificate with no keyUsage extension is unrestricted and passes.
|
|
231
|
+
function _assertSigningKeyUsage(certDer, label) {
|
|
232
|
+
var cert;
|
|
233
|
+
try { cert = schemaX509.parse(certDer); }
|
|
234
|
+
catch (e) { throw _err("scep/bad-input", "the " + label + " could not be parsed for its key usage: " + e.message, e); }
|
|
235
|
+
var ku = pkix.keyUsageOf(_KU_NS, cert, _err, "scep/bad-input", "the " + label + "'s");
|
|
236
|
+
if (ku != null && ku.digitalSignature !== true) {
|
|
237
|
+
throw _err("scep/bad-signer-usage", "the " + label + " keyUsage does not assert digitalSignature, so it is not authorized to sign (RFC 8894 sec. 2.3)");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Snapshot the recipient key material at the parse door: the certificate byte source is copied so a
|
|
242
|
+
// concurrent mutation cannot redirect the decryption, and the private / content key is held by
|
|
243
|
+
// reference (copying a secret is the worse defect). null passes through unchanged.
|
|
244
|
+
function _captureRecipientKey(rk) {
|
|
245
|
+
if (rk == null) return null;
|
|
246
|
+
rk = guard.identifier.optionsObject(rk, _err, "scep/bad-input", "opts.recipientKey");
|
|
247
|
+
var out = {};
|
|
248
|
+
if (rk.cert != null) out.cert = guard.bytes.snapshotSource(rk.cert, ScepError, "scep/bad-input", "opts.recipientKey.cert");
|
|
249
|
+
if (rk.key != null) out.key = rk.key;
|
|
250
|
+
if (rk.cek != null) out.cek = rk.cek;
|
|
251
|
+
return out;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* @primitive pki.scep.build
|
|
256
|
+
* @signature pki.scep.build(spec) -> Promise<Buffer>
|
|
257
|
+
* @since 0.6.2
|
|
258
|
+
* @status stable
|
|
259
|
+
* @spec RFC 8894
|
|
260
|
+
* @related pki.scep.parse, pki.cms.sign, pki.cms.encrypt, pki.csr.sign
|
|
261
|
+
*
|
|
262
|
+
* Assemble a SCEP pkiMessage (RFC 8894 sec. 3). A request (`PKCSReq` / `RenewalReq`) encrypts the
|
|
263
|
+
* supplied PKCS#10 `messageData` to the recipient CA certificate as the inner EnvelopedData
|
|
264
|
+
* (`pkcsPKIEnvelope`, AES-128-CBC), then signs it under the transaction attributes with the caller's
|
|
265
|
+
* signer certificate and key. `transactionId` is a caller-unique PrintableString; `senderNonce` is a
|
|
266
|
+
* 16-byte value (a fresh random one is generated when omitted). The recipient CA certificate must
|
|
267
|
+
* assert the `keyEncipherment` key usage (the EnvelopedData uses RSA key transport). The content key
|
|
268
|
+
* is transported under RSAES-OAEP (SHA-256), the toolkit's key-transport algorithm; the legacy
|
|
269
|
+
* RSAES-PKCS1-v1_5 that older SCEP servers expect is never emitted, so the CA must support OAEP.
|
|
270
|
+
*
|
|
271
|
+
* For a `PKCSReq` the caller's signer SHOULD be a self-signed certificate using the same subject name
|
|
272
|
+
* and key as the PKCS#10 request (RFC 8894 sec. 2.3); a `RenewalReq` is signed with the existing
|
|
273
|
+
* CA-issued certificate instead, which is why this is the caller's choice rather than an enforced
|
|
274
|
+
* match. The proof-of-possession over the PKCS#10 itself is verified regardless.
|
|
275
|
+
*
|
|
276
|
+
* @opts
|
|
277
|
+
* - `messageType` -- "PKCSReq" | "RenewalReq" (this release's request set).
|
|
278
|
+
* - `messageData` -- the PKCS#10 CertificationRequest DER to enrol.
|
|
279
|
+
* - `recipient` -- the CA (or RA) certificate DER the messageData is encrypted to (an RSA key-transport certificate; a non-RSA recipient is refused, since the envelope uses RSAES-OAEP).
|
|
280
|
+
* - `signer` -- `{ cert, key }`, the outer SignedData signer (the client for a request).
|
|
281
|
+
* - `transactionId` -- a caller-unique PrintableString identifying the transaction.
|
|
282
|
+
* - `senderNonce` -- a 16-byte Buffer (generated when omitted).
|
|
283
|
+
* @example
|
|
284
|
+
* // requires: csrDer -- the PKCS#10 CertificationRequest DER to enrol
|
|
285
|
+
* // requires: caCertDer -- the SCEP CA (or RA) certificate the request is encrypted to (keyEncipherment)
|
|
286
|
+
* // requires: clientCertDer -- the client's own certificate, the outer signer
|
|
287
|
+
* // requires: clientKeyPkcs8 -- the client's private key matching clientCertDer
|
|
288
|
+
* var msg = await pki.scep.build({ messageType: "PKCSReq", messageData: csrDer,
|
|
289
|
+
* recipient: caCertDer, signer: { cert: clientCertDer, key: clientKeyPkcs8 },
|
|
290
|
+
* transactionId: "txn-0001" });
|
|
291
|
+
*/
|
|
292
|
+
async function build(spec) {
|
|
293
|
+
spec = guard.identifier.optionsObject(spec, _err, "scep/bad-input", "pki.scep.build spec");
|
|
294
|
+
guard.identifier.assertKnownKeys(spec, _BUILD_KEYS, _err, "scep/bad-input", "pki.scep.build has an unknown option: ");
|
|
295
|
+
var mtCode = MESSAGE_TYPE[spec.messageType];
|
|
296
|
+
if (mtCode == null) throw _err("scep/bad-message-type", "unknown messageType " + JSON.stringify(spec.messageType) + " (RFC 8894 sec. 3.2.1.2)");
|
|
297
|
+
if (spec.messageType !== "PKCSReq" && spec.messageType !== "RenewalReq") {
|
|
298
|
+
throw _err("scep/bad-message-type", "pki.scep.build issues a request (PKCSReq / RenewalReq); " + spec.messageType + " is not built in this release");
|
|
299
|
+
}
|
|
300
|
+
if (spec.signer == null || typeof spec.signer !== "object") throw _err("scep/bad-input", "a signer { cert, key } is required");
|
|
301
|
+
var senderNonce = _nonce(spec.senderNonce, "senderNonce");
|
|
302
|
+
var txnId = _transactionId(spec.transactionId);
|
|
303
|
+
// The messageData is a PKCS#10 CertificationRequest whose self-signature is its proof-of-possession
|
|
304
|
+
// (RFC 8894 sec. 3.3.1, over RFC 2986). Verify the structure and that proof before enveloping it, so
|
|
305
|
+
// arbitrary bytes or a request whose signature does not verify under its own subject public key are
|
|
306
|
+
// refused here rather than encrypted into a message a CA would reject.
|
|
307
|
+
// Capture every input read after an await at this synchronous door, so a caller that mutates or reuses
|
|
308
|
+
// the spec while verification or encryption is pending cannot desync what was verified from what is
|
|
309
|
+
// emitted, or substitute the recipient a request is encrypted to. The certificate byte sources are
|
|
310
|
+
// copied; the signer private key is held by reference (copying a secret is the worse defect).
|
|
311
|
+
var messageData = guard.bytes.snapshotSource(spec.messageData, ScepError, "scep/bad-input", "messageData");
|
|
312
|
+
var recipient = guard.bytes.snapshotSource(spec.recipient, ScepError, "scep/bad-input", "recipient");
|
|
313
|
+
_assertRsaRecipient(recipient);
|
|
314
|
+
var signerCert = guard.bytes.snapshotSource(spec.signer.cert, ScepError, "scep/bad-input", "signer.cert");
|
|
315
|
+
_assertSigningKeyUsage(signerCert, "signer certificate");
|
|
316
|
+
var signerKey = spec.signer.key;
|
|
317
|
+
var parsedCsr;
|
|
318
|
+
try { parsedCsr = schemaCsr.parse(messageData); }
|
|
319
|
+
catch (e) { throw _err("scep/bad-input", "messageData is not a valid PKCS#10 CertificationRequest (RFC 8894 sec. 3.3.1): " + e.message, e); }
|
|
320
|
+
if (!(await csrVerify.verifyCsrSignature(parsedCsr))) {
|
|
321
|
+
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");
|
|
322
|
+
}
|
|
323
|
+
// Inner EnvelopedData: encrypt the PKCS#10 to the recipient CA, AES-128-CBC (RFC 8894 sec. 3).
|
|
324
|
+
var pkcsPKIEnvelope;
|
|
325
|
+
try {
|
|
326
|
+
pkcsPKIEnvelope = await cmsEncrypt.encrypt(messageData, [{ cert: recipient }], { contentEncryptionAlgorithm: ENVELOPE_CIPHER });
|
|
327
|
+
} catch (e) {
|
|
328
|
+
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);
|
|
329
|
+
throw e;
|
|
330
|
+
}
|
|
331
|
+
// Outer SignedData over the envelope, carrying the transaction attributes as authenticated attributes.
|
|
332
|
+
var attrs = [
|
|
333
|
+
_attr("scepMessageType", b.printable(mtCode)),
|
|
334
|
+
_attr("scepTransactionId", b.printable(txnId)),
|
|
335
|
+
_attr("scepSenderNonce", b.octetString(senderNonce)),
|
|
336
|
+
];
|
|
337
|
+
try {
|
|
338
|
+
return await cmsSign.sign(pkcsPKIEnvelope, { cert: signerCert, key: signerKey }, { additionalSignedAttributes: attrs });
|
|
339
|
+
} catch (e) {
|
|
340
|
+
if (e && e.isPkiError) throw _err("scep/bad-input", "the pkiMessage could not be signed: " + e.message, e);
|
|
341
|
+
throw e;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
var _BUILD_KEYS = { messageType: 1, messageData: 1, recipient: 1, signer: 1, transactionId: 1, senderNonce: 1 };
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* @primitive pki.scep.parse
|
|
348
|
+
* @signature pki.scep.parse(bytes, opts?) -> Promise<verdict>
|
|
349
|
+
* @since 0.6.2
|
|
350
|
+
* @status stable
|
|
351
|
+
* @spec RFC 8894
|
|
352
|
+
* @related pki.scep.build, pki.cms.verify, pki.cms.decrypt
|
|
353
|
+
*
|
|
354
|
+
* Disassemble a SCEP pkiMessage (RFC 8894 sec. 3): verify the outer SignedData signature, read the
|
|
355
|
+
* transaction attributes BOUND to the verified signer (never from a separate untrusted parse), and,
|
|
356
|
+
* given the recipient key, decrypt the pkcsPKIEnvelope to recover the messageData. It reads the
|
|
357
|
+
* enrollment message types PKCSReq, RenewalReq, and CertRep; the CertPoll, GetCert, and GetCRL client
|
|
358
|
+
* queries a CA processes are refused (`scep/unsupported-message-type`). The verdict is
|
|
359
|
+
* `{ signatureValid, signerAuthenticated, signerCert, messageType, transactionId, senderNonce,
|
|
360
|
+
* recipientNonce, pkiStatus, failInfo, failInfoText, messageData, certificates, crls }` (fields null
|
|
361
|
+
* when the message does not carry them). For a decrypted SUCCESS CertRep, `certificates` and `crls`
|
|
362
|
+
* hold the issued certificate(s) and any CRL validated out of the certs-only messageData, raw for the
|
|
363
|
+
* caller to path-validate; a GetCRL response carries the CRL and no certificate (RFC 8894 sec. 3.3.4). A malformed message, a missing mandatory attribute, an unknown enumerant, or a
|
|
364
|
+
* nonce mismatch is a typed `ScepError`.
|
|
365
|
+
*
|
|
366
|
+
* `signatureValid` proves only that the message is self-consistent with the certificate it embeds; it
|
|
367
|
+
* does NOT authenticate the signer. A SCEP client MUST authenticate a CA response against the CA
|
|
368
|
+
* certificate it holds: pass `opts.signerCert` and this refuses a signer whose public key does not
|
|
369
|
+
* match (`scep/untrusted-signer`) and reports `signerAuthenticated: true`. A caller that omits it gets
|
|
370
|
+
* a crypto-only verdict (`signerAuthenticated: false`) and MUST authenticate the surfaced `signerCert`
|
|
371
|
+
* itself before acting on the transaction state.
|
|
372
|
+
*
|
|
373
|
+
* @opts
|
|
374
|
+
* - `recipientKey` -- `{ cert, key }` for the recipient, to decrypt the pkcsPKIEnvelope into `messageData`.
|
|
375
|
+
* - `signerCert` -- the expected signer certificate DER (the CA certificate for a CertRep); the message signer's public key must match it, or the message is refused.
|
|
376
|
+
* - `expectedSenderNonce` -- a 16-byte Buffer; a message whose `recipientNonce` does not echo it is refused.
|
|
377
|
+
* @example
|
|
378
|
+
* // requires: pkiMessage -- a SCEP pkiMessage DER (from pki.scep.build, or a CA's response)
|
|
379
|
+
* // requires: caCertDer -- the recipient certificate the pkcsPKIEnvelope was encrypted to
|
|
380
|
+
* // requires: caKeyPkcs8 -- the private key matching caCertDer, to decrypt the messageData
|
|
381
|
+
* var v = await pki.scep.parse(pkiMessage, { recipientKey: { cert: caCertDer, key: caKeyPkcs8 } });
|
|
382
|
+
* v.messageType; // "PKCSReq"
|
|
383
|
+
* v.messageData; // the recovered PKCS#10 DER
|
|
384
|
+
*/
|
|
385
|
+
async function parse(bytes, opts) {
|
|
386
|
+
opts = guard.identifier.optionsObject(opts, _err, "scep/bad-input", "pki.scep.parse options");
|
|
387
|
+
guard.identifier.assertKnownKeys(opts, _PARSE_KEYS, _err, "scep/bad-input", "pki.scep.parse has an unknown option: ");
|
|
388
|
+
// Snapshot the message bytes once at this synchronous door: parse verifies them twice (a detached
|
|
389
|
+
// FAILURE / PENDING is re-verified against an empty payload), so a caller that swaps a mutable Buffer
|
|
390
|
+
// between the two attempts must not be able to return one message's attributes for another.
|
|
391
|
+
var msgBytes = guard.bytes.snapshotSource(bytes, ScepError, "scep/bad-der", "pkiMessage");
|
|
392
|
+
// Capture every option read after the awaited verification at this synchronous door, so a caller that
|
|
393
|
+
// mutates or reuses opts while verification is pending cannot swap the authenticated signer, the echo
|
|
394
|
+
// nonce, or the decryption recipient after the checks that depend on them have started. Certificate
|
|
395
|
+
// byte sources are copied; the recipient private key is held by reference.
|
|
396
|
+
var expectedSignerCert = opts.signerCert != null ? guard.bytes.snapshotSource(opts.signerCert, ScepError, "scep/bad-input", "opts.signerCert") : null;
|
|
397
|
+
var expectedSenderNonce = opts.expectedSenderNonce != null ? _nonce(opts.expectedSenderNonce, "expectedSenderNonce") : null;
|
|
398
|
+
var recipientKey = _captureRecipientKey(opts.recipientKey);
|
|
399
|
+
var v;
|
|
400
|
+
try { v = await cmsVerify.verify(msgBytes); }
|
|
401
|
+
catch (e) {
|
|
402
|
+
// A conforming FAILURE or PENDING CertRep omits the pkcsPKIEnvelope, so the SignedData has no
|
|
403
|
+
// encapsulated content and cms.verify reports it as detached. Re-verify against an empty payload,
|
|
404
|
+
// which the signature accepts only when the message-digest was taken over no content; a message
|
|
405
|
+
// signed over any other bytes still fails (RFC 8894 sec. 3.3.2).
|
|
406
|
+
if (e && e.code === "cms/detached-content-required") {
|
|
407
|
+
try { v = await cmsVerify.verify(msgBytes, { content: EMPTY_CONTENT }); }
|
|
408
|
+
catch (e2) {
|
|
409
|
+
if (e2 && e2.isPkiError) throw _err("scep/bad-der", "the pkiMessage is not a verifiable CMS SignedData (RFC 8894 sec. 3): " + e2.message, e2);
|
|
410
|
+
throw e2;
|
|
411
|
+
}
|
|
412
|
+
} else if (e && e.isPkiError) {
|
|
413
|
+
throw _err("scep/bad-der", "the pkiMessage is not a verifiable CMS SignedData (RFC 8894 sec. 3): " + e.message, e);
|
|
414
|
+
} else { throw e; }
|
|
415
|
+
}
|
|
416
|
+
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);
|
|
417
|
+
// Fail closed: the transaction attributes are only trustworthy once the outer signature verifies.
|
|
418
|
+
// cms.verify returns a per-signer verdict rather than throwing, so a bad signature MUST be refused
|
|
419
|
+
// here before any attribute is read -- never surfaced as a false verdict alongside the attributes.
|
|
420
|
+
if (v.valid !== true) throw _err("scep/bad-signature", "the pkiMessage outer signature did not verify (RFC 8894 sec. 3.1)");
|
|
421
|
+
// The SignedData eContentType MUST be id-data (RFC 8894 sec. 3.2): a message that verifies as CMS but
|
|
422
|
+
// names another encapsulated content type is not a valid SCEP pkiMessage.
|
|
423
|
+
if (v.eContentType !== oid.byName("data")) throw _err("scep/bad-content-type", "the pkiMessage eContentType must be id-data (RFC 8894 sec. 3.2)");
|
|
424
|
+
var signer = v.signers[0];
|
|
425
|
+
// A SCEP signer's certificate must be authorized to sign (RFC 8894 sec. 2.3): a signature from a
|
|
426
|
+
// certificate whose keyUsage permits only encryption is not read as an authenticated pkiMessage.
|
|
427
|
+
_assertSigningKeyUsage(signer.cert, "pkiMessage signer certificate");
|
|
428
|
+
// A valid signature proves only that the message is self-consistent with the certificate it embeds,
|
|
429
|
+
// NOT that the signer is the expected party: anyone can mint a certificate and sign a forged CertRep
|
|
430
|
+
// that echoes an observed nonce. A SCEP client MUST authenticate a CA response against the CA
|
|
431
|
+
// certificate it already holds (RFC 8894 sec. 3.1). When opts.signerCert names the expected signer,
|
|
432
|
+
// require the message signer's public key to match it; a caller that omits it gets a crypto-only
|
|
433
|
+
// verdict (signerAuthenticated: false) and MUST authenticate the surfaced signerCert itself before
|
|
434
|
+
// acting on the transaction state.
|
|
435
|
+
var signerAuthenticated = false;
|
|
436
|
+
if (expectedSignerCert != null) {
|
|
437
|
+
// The certificate the caller pins is authenticated only if it is itself authorized to sign: a
|
|
438
|
+
// same-key certificate embedded in the message cannot lend its digitalSignature bit to a pinned
|
|
439
|
+
// certificate that permits only encryption (RFC 8894 sec. 2.3).
|
|
440
|
+
_assertSigningKeyUsage(expectedSignerCert, "opts.signerCert");
|
|
441
|
+
if (!_sameKey(signer.cert, expectedSignerCert)) {
|
|
442
|
+
throw _err("scep/untrusted-signer", "the pkiMessage signer's public key does not match opts.signerCert (RFC 8894 sec. 3.1)");
|
|
443
|
+
}
|
|
444
|
+
signerAuthenticated = true;
|
|
445
|
+
}
|
|
446
|
+
var attrs = signer.signedAttributes;
|
|
447
|
+
// The mandatory attributes (RFC 8894 sec. 3.2.1): transactionID, messageType, senderNonce.
|
|
448
|
+
var messageTypeCode = _readStr(attrs, "scepMessageType", _T_PRINTABLE);
|
|
449
|
+
var transactionId = _readStr(attrs, "scepTransactionId", _T_PRINTABLE);
|
|
450
|
+
var senderNonce = _readNonce(attrs, "scepSenderNonce");
|
|
451
|
+
if (messageTypeCode == null) throw _err("scep/missing-attribute", "the pkiMessage is missing its messageType attribute (RFC 8894 sec. 3.2.1)");
|
|
452
|
+
if (transactionId == null) throw _err("scep/missing-attribute", "the pkiMessage is missing its transactionID attribute (RFC 8894 sec. 3.2.1)");
|
|
453
|
+
if (senderNonce == null) throw _err("scep/missing-attribute", "the pkiMessage is missing its senderNonce attribute (RFC 8894 sec. 3.2.1)");
|
|
454
|
+
var messageType = MESSAGE_TYPE_BY_CODE[messageTypeCode];
|
|
455
|
+
if (messageType == null) throw _err("scep/bad-message-type", "unknown messageType " + JSON.stringify(messageTypeCode) + " (RFC 8894 sec. 3.2.1.2)");
|
|
456
|
+
// This release reads the enrollment messages a SCEP client builds and receives: PKCSReq, RenewalReq,
|
|
457
|
+
// and the CertRep response. The CertPoll / GetCert / GetCRL client queries a CA processes carry
|
|
458
|
+
// IssuerAndSubject / IssuerAndSerialNumber messageData this codec does not model, so they are refused
|
|
459
|
+
// rather than accepted with an unvalidated payload (RFC 8894 sec. 3.3.3, sec. 3.3.4).
|
|
460
|
+
if (_PARSE_SUPPORTED[messageType] !== 1) {
|
|
461
|
+
throw _err("scep/unsupported-message-type", "pki.scep.parse reads PKCSReq, RenewalReq, and CertRep; " + messageType + " is not read in this release");
|
|
462
|
+
}
|
|
463
|
+
var out = {
|
|
464
|
+
signatureValid: true, signerAuthenticated: signerAuthenticated, signerCert: signer.cert,
|
|
465
|
+
messageType: messageType, transactionId: transactionId, senderNonce: senderNonce,
|
|
466
|
+
recipientNonce: null, pkiStatus: null, failInfo: null, failInfoText: null, messageData: null,
|
|
467
|
+
certificates: null, crls: null,
|
|
468
|
+
};
|
|
469
|
+
// A CertRep carries pkiStatus (+ failInfo/failInfoText on FAILURE) and echoes the request's senderNonce.
|
|
470
|
+
if (messageType === "CertRep") {
|
|
471
|
+
var statusCode = _readStr(attrs, "scepPkiStatus", _T_PRINTABLE);
|
|
472
|
+
if (statusCode == null) throw _err("scep/missing-attribute", "a CertRep is missing its pkiStatus attribute (RFC 8894 sec. 3.2.1.3)");
|
|
473
|
+
out.pkiStatus = PKI_STATUS_BY_CODE[statusCode];
|
|
474
|
+
if (out.pkiStatus == null) throw _err("scep/bad-pki-status", "unknown pkiStatus " + JSON.stringify(statusCode) + " (RFC 8894 sec. 3.2.1.3)");
|
|
475
|
+
out.recipientNonce = _readNonce(attrs, "scepRecipientNonce");
|
|
476
|
+
// A CertRep MUST echo the request's senderNonce in a recipientNonce (RFC 8894 sec. 3.1), so a
|
|
477
|
+
// response missing it cannot be correlated to its request and is refused regardless of whether the
|
|
478
|
+
// caller supplied expectedSenderNonce.
|
|
479
|
+
if (out.recipientNonce == null) throw _err("scep/missing-attribute", "a CertRep is missing its recipientNonce attribute (RFC 8894 sec. 3.2.1.5)");
|
|
480
|
+
if (out.pkiStatus === "FAILURE") {
|
|
481
|
+
var failCode = _readStr(attrs, "scepFailInfo", _T_PRINTABLE);
|
|
482
|
+
if (failCode == null) throw _err("scep/missing-attribute", "a FAILURE CertRep is missing its failInfo attribute (RFC 8894 sec. 3.2.1.4)");
|
|
483
|
+
out.failInfo = FAIL_INFO_BY_CODE[failCode];
|
|
484
|
+
if (out.failInfo == null) throw _err("scep/bad-fail-info", "unknown failInfo " + JSON.stringify(failCode) + " (RFC 8894 sec. 3.2.1.4)");
|
|
485
|
+
out.failInfoText = _readStr(attrs, "scepFailInfoText", _T_UTF8);
|
|
486
|
+
} else if (_find(attrs, "scepFailInfo") != null || _find(attrs, "scepFailInfoText") != null) {
|
|
487
|
+
// failInfo and failInfoText are defined for a FAILURE response (RFC 8894 sec. 3.2.1.4): a SUCCESS
|
|
488
|
+
// or PENDING CertRep carrying them asserts a contradictory authenticated status and is refused.
|
|
489
|
+
throw _err("scep/unexpected-attribute", "a " + out.pkiStatus + " CertRep must not carry a failInfo or failInfoText attribute (RFC 8894 sec. 3.2.1.4)");
|
|
490
|
+
}
|
|
491
|
+
} else {
|
|
492
|
+
// A request (PKCSReq / RenewalReq) is an initial message: the response-only attributes pkiStatus,
|
|
493
|
+
// failInfo, failInfoText, and recipientNonce are defined for a CertRep and MUST NOT appear here
|
|
494
|
+
// (RFC 8894 sec. 3.2.1). A request carrying any of them is off-profile and refused.
|
|
495
|
+
if (_find(attrs, "scepPkiStatus") != null || _find(attrs, "scepFailInfo") != null ||
|
|
496
|
+
_find(attrs, "scepFailInfoText") != null || _find(attrs, "scepRecipientNonce") != null) {
|
|
497
|
+
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)");
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
// A caller-stated expected senderNonce echo (RFC 8894 sec. 3.2.1.5): a message whose recipientNonce
|
|
501
|
+
// does not match the nonce we sent MUST be rejected.
|
|
502
|
+
if (expectedSenderNonce != null) {
|
|
503
|
+
if (out.recipientNonce == null || !guard.crypto.constantTimeEqual(out.recipientNonce, expectedSenderNonce)) {
|
|
504
|
+
throw _err("scep/nonce-mismatch", "the pkiMessage recipientNonce does not echo the sent senderNonce (RFC 8894 sec. 3.2.1.5)");
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
// The pkcsPKIEnvelope is carried by a request and a SUCCESS CertRep, and is OMITTED by a FAILURE or
|
|
508
|
+
// PENDING CertRep, whose payload is the status itself (RFC 8894 sec. 3.3.2.1). Decrypt only when the
|
|
509
|
+
// message is one that carries the envelope, so a non-SUCCESS CertRep's authenticated status is
|
|
510
|
+
// returned rather than turned into a decrypt failure over content that is not an EnvelopedData. The
|
|
511
|
+
// eContent is bound to the verified signature (cms.verify).
|
|
512
|
+
var carriesEnvelope = !(messageType === "CertRep" && (out.pkiStatus === "FAILURE" || out.pkiStatus === "PENDING"));
|
|
513
|
+
if (carriesEnvelope) {
|
|
514
|
+
// For a message that carries messageData, the pkcsPKIEnvelope is mandatory, MUST be an EnvelopedData,
|
|
515
|
+
// and MUST encrypt id-data (RFC 8894 sec. 3.2, sec. 3.2.2). This is checked structurally, before any
|
|
516
|
+
// recipient-key decryption, so an absent or off-profile envelope is never accepted as a valid
|
|
517
|
+
// message with a null messageData, whether or not a recipient key was supplied.
|
|
518
|
+
_assertValidEnvelope(v.eContent, messageType);
|
|
519
|
+
} else {
|
|
520
|
+
// A FAILURE or PENDING CertRep MUST omit the pkcsPKIEnvelope (RFC 8894 sec. 3.3.2): its payload is
|
|
521
|
+
// the status itself, so the SignedData carries no encapsulated content (cms.verify surfaces it as a
|
|
522
|
+
// null eContent). Any present eContent -- an attached OCTET STRING even a zero-length one -- is
|
|
523
|
+
// off-profile and refused, so attacker-supplied bytes are never accepted under a non-SUCCESS verdict.
|
|
524
|
+
if (v.eContent != null) {
|
|
525
|
+
throw _err("scep/unexpected-envelope", "a " + out.pkiStatus + " CertRep must omit the pkcsPKIEnvelope (RFC 8894 sec. 3.3.2), but it carries encapsulated content");
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (recipientKey != null && carriesEnvelope && v.eContent != null) {
|
|
529
|
+
var dec;
|
|
530
|
+
try { dec = await cmsDecrypt.decrypt(v.eContent, recipientKey); }
|
|
531
|
+
catch (e) {
|
|
532
|
+
if (e && e.isPkiError) throw _err("scep/decrypt-failed", "the pkcsPKIEnvelope could not be decrypted with the recipient key: " + e.message, e);
|
|
533
|
+
throw e;
|
|
534
|
+
}
|
|
535
|
+
out.messageData = dec.content;
|
|
536
|
+
// A SUCCESS CertRep's messageData MUST be a degenerate certs-only CMS SignedData carrying the
|
|
537
|
+
// issued certificate(s) (RFC 8894 sec. 3.3.2, sec. 3.4). Validate that structure through the strict
|
|
538
|
+
// CMS parser and surface the certificates, so a decrypted payload that is not a valid issuance
|
|
539
|
+
// cannot read as a successful enrollment. The certificates are raw, for the caller to path-validate.
|
|
540
|
+
if (messageType === "CertRep" && out.pkiStatus === "SUCCESS") {
|
|
541
|
+
var payload = _readIssuancePayload(dec.content);
|
|
542
|
+
out.certificates = payload.certificates;
|
|
543
|
+
out.crls = payload.crls;
|
|
544
|
+
} else if (messageType === "PKCSReq" || messageType === "RenewalReq") {
|
|
545
|
+
// A request's messageData MUST be a PKCS#10 CertificationRequest (RFC 8894 sec. 3.3.1). Validate
|
|
546
|
+
// the structure so a decrypted payload that is not one cannot read as a valid request; the raw
|
|
547
|
+
// bytes stay in messageData for the caller to verify the proof-of-possession.
|
|
548
|
+
try { schemaCsr.parse(dec.content); }
|
|
549
|
+
catch (e) { throw _err("scep/bad-request-payload", "a " + messageType + " messageData must be a PKCS#10 CertificationRequest (RFC 8894 sec. 3.3.1)", e); }
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return out;
|
|
553
|
+
}
|
|
554
|
+
var _PARSE_KEYS = { recipientKey: 1, expectedSenderNonce: 1, signerCert: 1 };
|
|
555
|
+
|
|
556
|
+
module.exports = {
|
|
557
|
+
build: build,
|
|
558
|
+
parse: parse,
|
|
559
|
+
MESSAGE_TYPES: MESSAGE_TYPE,
|
|
560
|
+
PKI_STATUS: PKI_STATUS,
|
|
561
|
+
FAIL_INFO: FAIL_INFO,
|
|
562
|
+
};
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:54d1c599-3174-4b94-9968-3bf7cc9d4fd8",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-08-
|
|
8
|
+
"timestamp": "2026-08-30T21:10:28.418Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/pki@0.6.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.6.2",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.6.
|
|
25
|
+
"version": "0.6.2",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/pki@0.6.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.6.2",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/pki@0.6.
|
|
57
|
+
"ref": "@blamejs/pki@0.6.2",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|