@blamejs/pki 0.5.7 → 0.5.8
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 +405 -386
- package/MIGRATING.md +43 -0
- package/README.md +12 -12
- package/lib/acme.js +31 -31
- package/lib/asn1-der.js +10 -10
- package/lib/attrcert-sign.js +19 -19
- package/lib/byte-reader.js +6 -6
- package/lib/byte-writer.js +5 -5
- package/lib/cbor-det.js +27 -24
- package/lib/cmc-build.js +97 -92
- package/lib/cmc-verify.js +106 -30
- package/lib/cmp-build.js +25 -25
- package/lib/cmp-session.js +70 -70
- package/lib/cmp-verify.js +71 -57
- package/lib/cms-compress.js +7 -7
- package/lib/cms-decrypt.js +90 -72
- package/lib/cms-encrypt.js +32 -32
- package/lib/cms-sign.js +74 -55
- package/lib/cms-verify.js +97 -75
- package/lib/composite-sig.js +13 -13
- package/lib/constants.js +4 -4
- package/lib/crl-sign.js +22 -22
- package/lib/crl-verify.js +7 -6
- package/lib/crmf-sign.js +14 -14
- package/lib/csr-sign.js +8 -8
- package/lib/ct.js +37 -37
- package/lib/edwards-point.js +7 -7
- package/lib/est.js +98 -55
- package/lib/framework-error.js +5 -5
- package/lib/guard-all.js +3 -3
- package/lib/guard-async.js +4 -4
- package/lib/guard-bytes.js +79 -79
- package/lib/guard-compress.js +17 -17
- package/lib/guard-crypto.js +1 -1
- package/lib/guard-encoding.js +15 -15
- package/lib/guard-header.js +3 -3
- package/lib/guard-identifier.js +16 -16
- package/lib/guard-json.js +15 -15
- package/lib/guard-limits.js +7 -7
- package/lib/guard-name.js +81 -16
- package/lib/guard-parsed.js +80 -80
- package/lib/guard-range.js +19 -19
- package/lib/guard-secret.js +11 -10
- package/lib/guard-text.js +6 -6
- package/lib/guard-time.js +10 -10
- package/lib/hpke.js +18 -17
- package/lib/http-digest.js +35 -35
- package/lib/http-retry-after.js +13 -13
- package/lib/http-transport.js +20 -19
- package/lib/inspect.js +53 -53
- package/lib/ip-utils.js +2 -2
- package/lib/jose.js +13 -13
- package/lib/key.js +16 -16
- package/lib/lint.js +51 -51
- package/lib/merkle.js +51 -36
- package/lib/mime.js +18 -18
- package/lib/ocsp-verify.js +10 -10
- package/lib/ocsp.js +13 -13
- package/lib/oid.js +29 -29
- package/lib/path-validate.js +114 -113
- package/lib/pbes2.js +16 -16
- package/lib/pkcs12-build.js +53 -53
- package/lib/pki-build.js +21 -19
- package/lib/rc2.js +1 -1
- package/lib/rfc3339.js +5 -5
- package/lib/schema-all.js +31 -31
- package/lib/schema-attrcert.js +12 -12
- package/lib/schema-c509.js +144 -142
- package/lib/schema-cmc.js +58 -58
- package/lib/schema-cmp.js +43 -43
- package/lib/schema-cms.js +45 -45
- package/lib/schema-crl.js +7 -7
- package/lib/schema-crmf.js +28 -28
- package/lib/schema-csr.js +12 -12
- package/lib/schema-csrattrs.js +16 -16
- package/lib/schema-engine.js +18 -18
- package/lib/schema-ocsp.js +15 -15
- package/lib/schema-pkcs12.js +20 -20
- package/lib/schema-pkcs8.js +2 -2
- package/lib/schema-pkix.js +131 -126
- package/lib/schema-smime.js +19 -19
- package/lib/schema-tsp.js +12 -12
- package/lib/schema-x509.js +3 -3
- package/lib/shbs.js +18 -18
- package/lib/sign-scheme.js +13 -13
- package/lib/sigstore.js +10 -11
- package/lib/sleep.js +1 -1
- package/lib/smime.js +308 -96
- package/lib/tls-cert-compress.js +18 -18
- package/lib/trust.js +27 -27
- package/lib/tsp-sign.js +17 -17
- package/lib/validator-all.js +1 -1
- package/lib/validator-attcert.js +1 -1
- package/lib/validator-cose.js +43 -44
- package/lib/validator-keydesc.js +3 -3
- package/lib/validator-sig.js +13 -13
- package/lib/validator-tls.js +11 -11
- package/lib/validator-tpm.js +20 -19
- package/lib/webauthn-mds.js +66 -66
- package/lib/webauthn.js +33 -33
- package/lib/webcrypto.js +15 -15
- package/lib/x509-sign.js +13 -13
- package/package.json +3 -2
- package/sbom.cdx.json +6 -6
package/lib/cmc-verify.js
CHANGED
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
* @intro Interpret an RFC 5272 Full PKI Response. `verify(response, sent)` takes the response
|
|
9
9
|
* a CA returned and the state the client retained from its request, binds the two together
|
|
10
10
|
* (transaction identifier, the Sender/Recipient Nonce echo, the Data Return echo), reads the
|
|
11
|
-
* ordered status verdicts, and reduces them to
|
|
11
|
+
* ordered status verdicts, and reduces them to one terminal outcome: `issued`, `pending`,
|
|
12
12
|
* `confirm-required`, `pop-required` or `rejected`. The issued certificates come from the CMS
|
|
13
13
|
* certificate bag, where RFC 5272 sec. 4.2 puts them. Nothing here is trusted: the bag and any
|
|
14
14
|
* Publish Trust Anchors control are surfaced as DATA for the caller to validate through
|
|
15
15
|
* `pki.path.validate`.
|
|
16
16
|
* @spec RFC 5272, RFC 5273, RFC 6402
|
|
17
|
-
* @card Interpret a CMC Full PKI Response into one terminal verdict
|
|
17
|
+
* @card Interpret a CMC Full PKI Response into one terminal verdict: transaction and nonce
|
|
18
18
|
* binding, the status verdicts, the certificate bag surfaced untrusted.
|
|
19
19
|
*/
|
|
20
20
|
|
|
@@ -53,7 +53,7 @@ var OUTCOME_BY_STATUS = {
|
|
|
53
53
|
|
|
54
54
|
// Which outcome governs when a response carries SEVERAL status controls (sec.
|
|
55
55
|
// 6.1 requires a client to cope with that). Ranked worst-first so the verdict can
|
|
56
|
-
// never be improved by the
|
|
56
|
+
// never be improved by the order the controls happen to appear in: reporting
|
|
57
57
|
// the first one seen would let a server bury a failure behind a success.
|
|
58
58
|
var OUTCOME_SEVERITY = { rejected: 4, "pop-required": 3, "confirm-required": 2, pending: 1, issued: 0 };
|
|
59
59
|
|
|
@@ -67,9 +67,9 @@ function _singleValue(control, what) {
|
|
|
67
67
|
// The single instance of a control, or null when absent.
|
|
68
68
|
//
|
|
69
69
|
// Body-part identity is unique per element, so a responder can legally carry the
|
|
70
|
-
//
|
|
70
|
+
// same control type twice under different bodyPartIDs. Taking the first match
|
|
71
71
|
// would let an attacker pair one correct echo with a contradictory one and still
|
|
72
|
-
// satisfy the binding
|
|
72
|
+
// satisfy the binding, so a duplicate is refused as ambiguous instead of
|
|
73
73
|
// resolved. The exchange either has one answer or it has none.
|
|
74
74
|
function _findControl(controls, attrType, what) {
|
|
75
75
|
var found = null;
|
|
@@ -90,7 +90,7 @@ function _octets(control, what) {
|
|
|
90
90
|
// The certificate / CRL bag, from whichever carrier holds it. A SignedData keeps
|
|
91
91
|
// them at the top level; an AuthenticatedData keeps them under `originatorInfo`
|
|
92
92
|
// (RFC 5652 sec. 9.1). Both carriers are accepted for a Full PKI Response, so
|
|
93
|
-
// reading only the SignedData shape would silently return an empty bag
|
|
93
|
+
// reading only the SignedData shape would silently return an empty bag, and
|
|
94
94
|
// "no certificates were issued" is exactly the wrong thing to report when some
|
|
95
95
|
// were.
|
|
96
96
|
function _bagOf(parsedCms, originatorKey, topKey) {
|
|
@@ -106,10 +106,30 @@ function _bagOf(parsedCms, originatorKey, topKey) {
|
|
|
106
106
|
* Both halves are CONDITIONAL on what the client sent, which is the subtlety: a
|
|
107
107
|
* client that sent no transactionId cannot demand one back, and a client that
|
|
108
108
|
* sent no senderNonce has nothing to compare a recipientNonce against. What is
|
|
109
|
-
*
|
|
109
|
+
* not conditional is the converse: having sent one, an absent or differing echo
|
|
110
110
|
* is a refusal, because that is exactly the replay the nonce exists to stop.
|
|
111
|
+
*
|
|
112
|
+
* Returns which halves ran, because "conditional" and "checked" are different
|
|
113
|
+
* facts and the verdict has to be able to tell them apart.
|
|
111
114
|
*/
|
|
112
115
|
function _assertBound(body, sent) {
|
|
116
|
+
// A zero-length value binds nothing. An empty senderNonce echoes equal in every
|
|
117
|
+
// exchange that also used one, so counting its presence would report boundToRequest
|
|
118
|
+
// true for a response captured from any of them -- the exact replay this gate exists
|
|
119
|
+
// to refuse, passing because the comparison it ran was vacuous. The same holds for an
|
|
120
|
+
// empty transactionId or dataReturn: a binding has to carry entropy to be a binding.
|
|
121
|
+
function _carries(v) {
|
|
122
|
+
if (v == null) return false;
|
|
123
|
+
if (typeof v === "string") return v.length > 0;
|
|
124
|
+
if (Buffer.isBuffer(v) || v instanceof Uint8Array) return v.length > 0;
|
|
125
|
+
return true; // an integer or other scalar identifier is its own value
|
|
126
|
+
}
|
|
127
|
+
var bound = {
|
|
128
|
+
transactionId: _carries(sent.transactionId),
|
|
129
|
+
senderNonce: _carries(sent.senderNonce),
|
|
130
|
+
dataReturn: _carries(sent.dataReturn),
|
|
131
|
+
bodyPartIDs: Array.isArray(sent.bodyPartIDs),
|
|
132
|
+
};
|
|
113
133
|
if (sent.transactionId != null) {
|
|
114
134
|
var txControl = _findControl(body.controls, OID_TRANSACTION_ID, "Transaction Identifier");
|
|
115
135
|
if (!txControl) {
|
|
@@ -132,8 +152,8 @@ function _assertBound(body, sent) {
|
|
|
132
152
|
throw E("cmc/nonce-mismatch",
|
|
133
153
|
"the request carried a Sender Nonce, so the response MUST reflect it back as a Recipient Nonce control (RFC 5272 sec. 6.6)");
|
|
134
154
|
}
|
|
135
|
-
// Constant-time and by
|
|
136
|
-
// length first, so a truncation or a prefix is an honest false
|
|
155
|
+
// Constant-time and by full value: guard.crypto.constantTimeEqual gates the
|
|
156
|
+
// length first, so a truncation or a prefix is an honest false and not a
|
|
137
157
|
// match on the bytes that happen to line up.
|
|
138
158
|
if (!guard.crypto.constantTimeEqual(_octets(rn, "Recipient Nonce"), Buffer.from(sent.senderNonce))) {
|
|
139
159
|
throw E("cmc/nonce-mismatch", "the response Recipient Nonce does not match the Sender Nonce the request sent");
|
|
@@ -142,7 +162,7 @@ function _assertBound(body, sent) {
|
|
|
142
162
|
|
|
143
163
|
// RFC 5272 sec. 6.4: "If the Data Return control appears in a Full PKI Request,
|
|
144
164
|
// the server MUST return it as part of the PKI Response." The data is opaque to
|
|
145
|
-
// the server, so the check is that the
|
|
165
|
+
// the server, so the check is that the same bytes came back.
|
|
146
166
|
if (sent.dataReturn != null) {
|
|
147
167
|
var dr = _findControl(body.controls, OID_DATA_RETURN, "Data Return");
|
|
148
168
|
if (!dr) {
|
|
@@ -175,14 +195,14 @@ function _assertBound(body, sent) {
|
|
|
175
195
|
var known = Object.create(null);
|
|
176
196
|
sent.bodyPartIDs.forEach(function (id) { known[String(id)] = true; });
|
|
177
197
|
// 0 is reserved as the reference to the enclosing PKIData itself (sec. 3.2.1),
|
|
178
|
-
// so a status about the request
|
|
198
|
+
// so a status about the request as a whole is in the set by definition.
|
|
179
199
|
known["0"] = true;
|
|
180
200
|
body.statuses.forEach(function (s) {
|
|
181
201
|
(s.bodyList || []).forEach(function (ref) {
|
|
182
202
|
var path = ref.bodyPartPath;
|
|
183
203
|
// A bodyPartPath descends INTO a nested message. Checking only its head would
|
|
184
|
-
// accept `[a part we sent, 999]
|
|
185
|
-
// reference that passes
|
|
204
|
+
// accept `[a part we sent, 999]`, a status about something arbitrary wearing a
|
|
205
|
+
// reference that passes, so the whole path is matched against the paths the
|
|
186
206
|
// request actually composed. Those are retained by reading each nested message
|
|
187
207
|
// back; a nested message that could not be read back contributes none, so a path
|
|
188
208
|
// into it finds no match and is refused. Either way the tail is never waved through.
|
|
@@ -201,6 +221,34 @@ function _assertBound(body, sent) {
|
|
|
201
221
|
});
|
|
202
222
|
});
|
|
203
223
|
}
|
|
224
|
+
|
|
225
|
+
// Every check above is conditional on the caller having retained something, so a
|
|
226
|
+
// caller who retained nothing runs none of them and still gets a full verdict
|
|
227
|
+
// (`issued`, signature verified, certificates surfaced) off a Full PKI Response
|
|
228
|
+
// captured from any earlier exchange with the same CA. That is the CWE-294 this
|
|
229
|
+
// module names as its defence, and leaving it opt-in leaves the defence off for
|
|
230
|
+
// whoever did not know to ask.
|
|
231
|
+
//
|
|
232
|
+
// The posture here is the one _assertAuthentic already takes for the carrier
|
|
233
|
+
// signature, applied to the other question: check it when the caller supplies
|
|
234
|
+
// what it takes, and refuse when nothing does, unless the caller names that. The
|
|
235
|
+
// two opt-outs stay separate because they answer separate questions. A replayed
|
|
236
|
+
// response is authentic, so `allowUnverified` covering it would let "I could not
|
|
237
|
+
// check the signature" stand in for "I did not check which exchange this
|
|
238
|
+
// answers".
|
|
239
|
+
//
|
|
240
|
+
// bodyPartIDs does not count toward it. Identifiers are allocated per message
|
|
241
|
+
// from a small range (sec. 3.2.2), so the same set recurs across requests and a
|
|
242
|
+
// status naming one says nothing about which request it answers.
|
|
243
|
+
bound.boundToRequest = bound.transactionId || bound.senderNonce || bound.dataReturn;
|
|
244
|
+
if (!bound.boundToRequest && sent.allowUnbound !== true) {
|
|
245
|
+
throw E("cmc/unbound-response",
|
|
246
|
+
"nothing ties this response to a request. Pass what the request retained (`transactionId`, " +
|
|
247
|
+
"`senderNonce`, whose echo is the replay defence of RFC 5272 sec. 6.6, or `dataReturn`) so the " +
|
|
248
|
+
"echo can be checked, or `allowUnbound: true` to interpret a response that could be a replay of " +
|
|
249
|
+
"any earlier exchange with this CA");
|
|
250
|
+
}
|
|
251
|
+
return bound;
|
|
204
252
|
}
|
|
205
253
|
|
|
206
254
|
// The worst outcome across every status control, with success assumed when there
|
|
@@ -234,7 +282,7 @@ function _governingStatus(statuses, outcome) {
|
|
|
234
282
|
* @defends cmc-response-replay (CWE-294)
|
|
235
283
|
* @related pki.schema.cmc.parse, pki.path.validate, pki.cms.verify
|
|
236
284
|
*
|
|
237
|
-
* Interpret a Full PKI Response into
|
|
285
|
+
* Interpret a Full PKI Response into one terminal verdict. `response` is the DER or a PEM `CMS`
|
|
238
286
|
* block; an already-parsed message is interpreted too, but only under `allowUnverified: true`,
|
|
239
287
|
* because both carriers authenticate over BYTES and a parsed object the caller still owns pins
|
|
240
288
|
* none. `sent` is what the client retained from its request --
|
|
@@ -245,6 +293,16 @@ function _governingStatus(statuses, outcome) {
|
|
|
245
293
|
* with `cmc/body-part-unknown`, which the transaction and nonce cannot catch, since a server can
|
|
246
294
|
* echo both correctly while reporting on something else.
|
|
247
295
|
*
|
|
296
|
+
* Because each half is conditional, a caller who retains nothing runs none of them. A Full PKI
|
|
297
|
+
* Response captured from any earlier exchange with the same CA would then read as this request's
|
|
298
|
+
* answer, correctly signed. That case is refused as `cmc/unbound-response` unless the caller names
|
|
299
|
+
* it with `allowUnbound: true`, a separate opt-out from `allowUnverified` because it answers a
|
|
300
|
+
* separate question: a replayed response is authentic. `bodyPartIDs` alone does not satisfy it,
|
|
301
|
+
* since identifiers are allocated per message from a small range (RFC 5272 sec. 3.2.2) and the
|
|
302
|
+
* same set recurs across requests. The verdict reports which halves ran as `bound.transactionId`,
|
|
303
|
+
* `bound.senderNonce`, `bound.dataReturn` and `bound.bodyPartIDs`, with `boundToRequest` for the
|
|
304
|
+
* roll-up.
|
|
305
|
+
*
|
|
248
306
|
* The verdict carries the response's own `cmsSequence` and `otherMsgs` raw, because a request whose
|
|
249
307
|
* only arm was the other-message form has no certificate to return and RFC 5272 sec. 4.1 puts its
|
|
250
308
|
* answer there instead.
|
|
@@ -269,9 +327,9 @@ function _governingStatus(statuses, outcome) {
|
|
|
269
327
|
*
|
|
270
328
|
* The other carrier sec. 3.2 permits, AuthenticatedData, is authenticated by its MAC instead: pass
|
|
271
329
|
* `recipient` with the key material and the MAC is checked through `pki.cms.decrypt`, so a caller
|
|
272
|
-
* who holds the key gets an authenticated verdict
|
|
330
|
+
* who holds the key gets an authenticated verdict instead of the unauthenticated opt-out.
|
|
273
331
|
*
|
|
274
|
-
* Nothing is trusted here. `certificates` is the CMS certificate bag
|
|
332
|
+
* Nothing is trusted here. `certificates` is the CMS certificate bag, where RFC 5272 sec. 4.2
|
|
275
333
|
* puts the issued certificates -- surfaced raw for the caller to run through `pki.path.validate`,
|
|
276
334
|
* and a Publish Trust Anchors control is surfaced as `publishTrustAnchors` with `trusted: false`,
|
|
277
335
|
* never added to any store (RFC 5272 sec. 6.15 makes accepting one a four-part manual decision).
|
|
@@ -288,6 +346,9 @@ function _governingStatus(statuses, outcome) {
|
|
|
288
346
|
* `signatureVerified: true`; the content it authenticates is bound to the content the verdict
|
|
289
347
|
* was read from, so a MAC over other bytes cannot stand in for it.
|
|
290
348
|
* - `allowUnverified` (boolean) -- interpret without verifying the carrier; sets `signatureVerified: false`.
|
|
349
|
+
* - `allowUnbound` (boolean) -- interpret a response nothing ties to a request; sets
|
|
350
|
+
* `boundToRequest: false`. Needed only when none of `transactionId`, `senderNonce` or
|
|
351
|
+
* `dataReturn` was retained, and what it accepts is a possible replay.
|
|
291
352
|
* @example
|
|
292
353
|
* var b = pki.asn1.build, oid = pki.oid;
|
|
293
354
|
* var sid = b.sequence([b.sequence([b.set([b.sequence([b.oid(oid.byName("commonName")),
|
|
@@ -301,9 +362,10 @@ function _governingStatus(statuses, outcome) {
|
|
|
301
362
|
* var encap = b.sequence([b.oid(oid.byName("id-cct-PKIResponse")), b.explicit(0, b.octetString(body))]);
|
|
302
363
|
* var sd = b.sequence([b.integer(3n), b.set([]), encap, b.set([si])]);
|
|
303
364
|
* var der = b.sequence([b.oid(oid.byName("signedData")), b.explicit(0, sd)]);
|
|
304
|
-
* var v = await pki.cmc.verify(der, { allowUnverified: true });
|
|
365
|
+
* var v = await pki.cmc.verify(der, { allowUnverified: true, allowUnbound: true });
|
|
305
366
|
* v.outcome; // "issued" -- no status control means success is assumed
|
|
306
367
|
* v.signatureVerified; // false -- the opt-out was named, so nothing was checked
|
|
368
|
+
* v.boundToRequest; // false: nothing ties this response to a request either
|
|
307
369
|
*/
|
|
308
370
|
function verify(response, sent) {
|
|
309
371
|
// Both snapshots are taken SYNCHRONOUSLY, here, before anything is deferred.
|
|
@@ -398,7 +460,10 @@ function _snapshotSent(sent) {
|
|
|
398
460
|
out.recipient = r;
|
|
399
461
|
}
|
|
400
462
|
if (Array.isArray(out.certs)) out.certs = out.certs.map(_copyAnyBytes);
|
|
463
|
+
// Both opt-outs normalized to a strict boolean off the ORIGINAL object, so a
|
|
464
|
+
// truthy-but-not-true value cannot switch a check off by accident.
|
|
401
465
|
out.allowUnverified = sent.allowUnverified === true;
|
|
466
|
+
out.allowUnbound = sent.allowUnbound === true;
|
|
402
467
|
return out;
|
|
403
468
|
}
|
|
404
469
|
|
|
@@ -415,8 +480,7 @@ function _verify(response, sent) {
|
|
|
415
480
|
}
|
|
416
481
|
|
|
417
482
|
return _assertAuthentic(body, sent, response).then(function (signatureVerified) {
|
|
418
|
-
_assertBound(body, sent);
|
|
419
|
-
return _shape(body, sent, signatureVerified);
|
|
483
|
+
return _shape(body, sent, signatureVerified, _assertBound(body, sent));
|
|
420
484
|
});
|
|
421
485
|
}
|
|
422
486
|
|
|
@@ -555,7 +619,7 @@ function _assertAuthentic(body, sent, responseBytes) {
|
|
|
555
619
|
});
|
|
556
620
|
}
|
|
557
621
|
|
|
558
|
-
function _shape(body, sent, signatureVerified) {
|
|
622
|
+
function _shape(body, sent, signatureVerified, bound) {
|
|
559
623
|
var outcome = _reduceOutcome(body.statuses);
|
|
560
624
|
var governing = _governingStatus(body.statuses, outcome);
|
|
561
625
|
var anchors = _findControl(body.controls, OID_TRUSTED_ANCHORS, "Publish Trust Anchors");
|
|
@@ -591,6 +655,18 @@ function _shape(body, sent, signatureVerified) {
|
|
|
591
655
|
// Whether the CARRIER's signature was checked. False only via the explicit
|
|
592
656
|
// allowUnverified opt-out -- there is no path that leaves it false silently.
|
|
593
657
|
signatureVerified: signatureVerified,
|
|
658
|
+
// Which halves of the exchange binding ran (RFC 5272 sec. 6.6 / 6.4). Each is
|
|
659
|
+
// conditional on the caller having retained the value, so a bare `outcome`
|
|
660
|
+
// cannot say whether this response answers this request or an earlier one.
|
|
661
|
+
// These can. `boundToRequest` is false only via the explicit allowUnbound
|
|
662
|
+
// opt-out; nothing leaves it false silently.
|
|
663
|
+
bound: {
|
|
664
|
+
transactionId: bound.transactionId,
|
|
665
|
+
senderNonce: bound.senderNonce,
|
|
666
|
+
dataReturn: bound.dataReturn,
|
|
667
|
+
bodyPartIDs: bound.bodyPartIDs,
|
|
668
|
+
},
|
|
669
|
+
boundToRequest: bound.boundToRequest,
|
|
594
670
|
// Whether anything in here was TRUSTED, which is never: the certificate bag
|
|
595
671
|
// and any Publish Trust Anchors control are the caller's to path-validate.
|
|
596
672
|
trusted: false,
|
|
@@ -610,33 +686,33 @@ function _shape(body, sent, signatureVerified) {
|
|
|
610
686
|
* @related pki.cmc.verify, pki.schema.cmc.parse, pki.cms.sign
|
|
611
687
|
*
|
|
612
688
|
* Build and sign an RFC 5272 Full PKI Request. `spec.requests` is the list of certification
|
|
613
|
-
* requests, each naming exactly one arm
|
|
689
|
+
* requests, each naming exactly one arm: `tcr` (a PKCS#10 CSR), `crm` (a CRMF CertReqMsg, or the
|
|
614
690
|
* CertReqMessages `pki.crmf.build` returns when it carries exactly one) or `orm`
|
|
615
691
|
* (`{ type, value }`). `spec.controls` are additional controls as `{ type, value }`, and `signer`
|
|
616
692
|
* is the `{ cert, key }` that signs the enclosing CMS SignedData.
|
|
617
693
|
*
|
|
618
694
|
* Body part identifiers are allocated automatically, unique across the whole message and never 0
|
|
619
|
-
* (RFC 5272 sec. 3.2.2). A caller may pin one, and a clash is
|
|
695
|
+
* (RFC 5272 sec. 3.2.2). A caller may pin one, and a clash is refused, never renumbered, because
|
|
620
696
|
* silently moving an identifier would break any control that already referenced it. For a `crm`
|
|
621
697
|
* arm the identity is the CertReqMsg's own `certReqId`, read back out of the supplied message.
|
|
622
698
|
*
|
|
623
699
|
* `spec.identityProof: { secret, identity? }` attaches an Identity Proof V2 control whose witness is
|
|
624
|
-
* computed over the reqSequence bytes exactly as they are emitted (sec. 6.2.1 step 1
|
|
700
|
+
* computed over the reqSequence bytes exactly as they are emitted (sec. 6.2.1 step 1: "encoded
|
|
625
701
|
* exactly as it appears in the Full PKI Request including the sequence type and length"). Supplying
|
|
626
|
-
* `identity` also emits the Identification control naming the shared secret
|
|
627
|
-
* derives the MAC key from `hash(secret || identity)`
|
|
702
|
+
* `identity` also emits the Identification control naming the shared secret and, per sec. 6.2.3,
|
|
703
|
+
* derives the MAC key from `hash(secret || identity)` in place of `hash(secret)`: the two travel
|
|
628
704
|
* together because the control's presence is what changes the derivation. And
|
|
629
705
|
* `spec.popLink: { secret }` attaches a POP Link Witness V2 together with the POP Link Random
|
|
630
706
|
* control that PL1 requires in the same request. `spec.renewal: true` marks a renewal, which MUST
|
|
631
|
-
* carry neither Identification nor Identity Proof (sec. 3.2 (a))
|
|
632
|
-
*
|
|
707
|
+
* carry neither Identification nor Identity Proof (sec. 3.2 (a)), so asking for both is refused
|
|
708
|
+
* and never silently dropped.
|
|
633
709
|
*
|
|
634
710
|
* `spec.transactionId` (number|bigint), `spec.senderNonce` and `spec.dataReturn` (bytes) attach the
|
|
635
|
-
* exchange-binding controls (RFC 5272 sec. 6.6 / 6.4)
|
|
636
|
-
* response against. They are named fields
|
|
711
|
+
* exchange-binding controls (RFC 5272 sec. 6.6 / 6.4), the same three `pki.cmc.verify` checks the
|
|
712
|
+
* response against. They are named fields, not something to hand-encode into
|
|
637
713
|
* `spec.controls`, because a request that quietly omits them has no replay defence and neither end
|
|
638
714
|
* can tell: the verifier only enforces the halves the client says it sent. An unrecognized spec
|
|
639
|
-
* field is refused for the same reason
|
|
715
|
+
* field is refused for the same reason: a misspelling would otherwise build and sign a message
|
|
640
716
|
* that simply does not carry what was asked for.
|
|
641
717
|
*
|
|
642
718
|
* @opts
|
package/lib/cmp-build.js
CHANGED
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
* @card Build a CMP PKIMessage with signature or PBMAC1 protection.
|
|
20
20
|
*/
|
|
21
21
|
//
|
|
22
|
-
// RFC 9810 Appendix A is DEFINITIONS EXPLICIT TAGS: every context tag in the PKIMessage envelope
|
|
23
|
-
// each PKIHeader [0..8] optional, every PKIBody [n] arm, protection [0], extraCerts [1]
|
|
22
|
+
// RFC 9810 Appendix A is DEFINITIONS EXPLICIT TAGS: every context tag in the PKIMessage envelope
|
|
23
|
+
// (each PKIHeader [0..8] optional, every PKIBody [n] arm, protection [0], extraCerts [1]) is an EXPLICIT
|
|
24
24
|
// wrapper (asn1.build.explicit), the exact inverse of the RFC 4211 CRMF interior (IMPLICIT TAGS). The two
|
|
25
25
|
// IMPLICIT islands are foreign imports produced wholesale by pki.crmf.build (the CertReqMessages /
|
|
26
26
|
// CertTemplate interior) and embedded verbatim, never re-encoded here. Protection is computed over the
|
|
@@ -67,7 +67,7 @@ var KNOWN_BODY_KEYS = {
|
|
|
67
67
|
ir: 1, cr: 1, kur: 1, p10cr: 1, certConf: 1, pollReq: 1, genm: 1, rr: 1, // request-side
|
|
68
68
|
ip: 1, cp: 1, kup: 1, ccp: 1, krp: 1, rp: 1, genp: 1, error: 1, pollRep: 1, pkiconf: 1, // CA/responder-side
|
|
69
69
|
};
|
|
70
|
-
// The PKIBody arm -> its EXPLICIT context tag number (schema-cmp.js BODY_ARMS). rr is [11],
|
|
70
|
+
// The PKIBody arm -> its EXPLICIT context tag number (schema-cmp.js BODY_ARMS). rr is [11], not [15].
|
|
71
71
|
var BODY_TAG = {
|
|
72
72
|
ir: 0, cr: 2, p10cr: 4, kur: 7, rr: 11, genm: 21, certConf: 24, pollReq: 25,
|
|
73
73
|
ip: 1, cp: 3, kup: 8, krp: 10, rp: 12, ccp: 14, pkiconf: 19, genp: 22, error: 23, pollRep: 26,
|
|
@@ -88,7 +88,7 @@ var PBMAC1_MIN_ITER = 1000; // RFC 8018 sec. 4.2 recommended minimum -- re
|
|
|
88
88
|
var PBMAC1_PRF = { "SHA-256": "hmacWithSHA256", "SHA-384": "hmacWithSHA384", "SHA-512": "hmacWithSHA512" };
|
|
89
89
|
var PBMAC1_MAC_OID = { "SHA-256": "hmacWithSHA256", "SHA-384": "hmacWithSHA384", "SHA-512": "hmacWithSHA512" };
|
|
90
90
|
var PBMAC1_PRF_HLEN = { "SHA-256": 32, "SHA-384": 48, "SHA-512": 64 }; // PBKDF2 PRF output length (one derived block)
|
|
91
|
-
// PKIFailureInfo named bits (RFC 9810 sec. 5.2.3), position = bit index
|
|
91
|
+
// PKIFailureInfo named bits (RFC 9810 sec. 5.2.3), position = bit index, mirroring schema-cmp's decode list;
|
|
92
92
|
// the build -> parse round-trip cross-checks the positions against the parser.
|
|
93
93
|
var FAIL_INFO_NAMES = ["badAlg", "badMessageCheck", "badRequest", "badTime", "badCertId", "badDataFormat",
|
|
94
94
|
"wrongAuthority", "incorrectData", "missingTimeStamp", "badPOP", "certRevoked", "certConfirmed",
|
|
@@ -97,7 +97,7 @@ var FAIL_INFO_NAMES = ["badAlg", "badMessageCheck", "badRequest", "badTime", "ba
|
|
|
97
97
|
"unsupportedVersion", "notAuthorized", "systemUnavail", "systemFailure", "duplicateCertReq"];
|
|
98
98
|
var FAIL_INFO_INDEX = {};
|
|
99
99
|
FAIL_INFO_NAMES.forEach(function (n, i) { FAIL_INFO_INDEX[n] = i; });
|
|
100
|
-
// CertStatus.hashAlg names the hash used to compute certHash
|
|
100
|
+
// CertStatus.hashAlg names the hash used to compute certHash, restricted to hash algorithms and not any OID.
|
|
101
101
|
var CERT_CONF_HASH_ALGS = { sha1: 1, sha256: 1, sha384: 1, sha512: 1, "sha3-256": 1, "sha3-512": 1 };
|
|
102
102
|
|
|
103
103
|
// ---- small shared encoders (byte-exact inverses of schema-cmp.js readers) ----
|
|
@@ -107,7 +107,7 @@ function _reqOctets(v, what) {
|
|
|
107
107
|
return b.octetString(buf);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
// PKIFreeText ::= SEQUENCE SIZE (1..MAX) OF UTF8String
|
|
110
|
+
// PKIFreeText ::= SEQUENCE SIZE (1..MAX) OF UTF8String: non-empty, every element UTF8String.
|
|
111
111
|
function _encodePkiFreeText(strings, code, what) {
|
|
112
112
|
if (!Array.isArray(strings) || !strings.length) throw _err(code, what + " must be a non-empty array of strings");
|
|
113
113
|
return b.sequence(strings.map(function (s) {
|
|
@@ -193,7 +193,7 @@ function _encodeHeader(headerSpec, protectionAlgDer, pvno) {
|
|
|
193
193
|
// or a bigint (a large value beyond 2^53), reject a non-integer / other type. Returns a BigInt for b.integer.
|
|
194
194
|
function _reqIdInt(v, code, what) { return guard.range.authoredInteger(v, _err, code, what); }
|
|
195
195
|
|
|
196
|
-
// PKIFailureInfo ::= BIT STRING (named bits, RFC 9810 sec. 5.2.3)
|
|
196
|
+
// PKIFailureInfo ::= BIT STRING (named bits, RFC 9810 sec. 5.2.3): a minimal NamedBitList from bit names.
|
|
197
197
|
function _encodeFailInfo(names, code) {
|
|
198
198
|
if (!Array.isArray(names)) throw _err(code, "statusInfo.failInfo must be an array of PKIFailureInfo bit names");
|
|
199
199
|
return b.namedBitString(names.map(function (n) {
|
|
@@ -288,7 +288,7 @@ function _encodeCertOrEncCert(coec, state) {
|
|
|
288
288
|
}
|
|
289
289
|
if (coec.encryptedCert != null) {
|
|
290
290
|
// encryptedCert [1] wraps an EncryptedKey (pre-encoded DER hatch in v1). Like the privateKey, only the
|
|
291
|
-
// EnvelopedData [0] form is cmp2021
|
|
291
|
+
// EnvelopedData [0] form is cmp2021; the deprecated EncryptedValue (a universal SEQUENCE) is cmp2000.
|
|
292
292
|
var ec = _b.reqDer(coec.encryptedCert, "certifiedKeyPair.encryptedCert (a pre-encoded EncryptedKey DER)");
|
|
293
293
|
var ecNode;
|
|
294
294
|
try { ecNode = asn1.decode(ec); } catch (e) { throw _err("cmp/bad-cert-response", "certifiedKeyPair.encryptedCert is not valid DER", e); }
|
|
@@ -475,7 +475,7 @@ function _encodeBody(bodySpec, key, opts) {
|
|
|
475
475
|
|
|
476
476
|
|
|
477
477
|
// Resolve the protection selector to { protectionAlgDer, computeBits(protectedPartDer)->Promise<Buffer> },
|
|
478
|
-
// senderSpki, senderScheme }
|
|
478
|
+
// senderSpki, senderScheme } before the header is built (protectionAlg is opts-derived, not header-derived).
|
|
479
479
|
function _resolveProtection(opts) {
|
|
480
480
|
var hasSig = opts.key != null || opts.cert != null;
|
|
481
481
|
var hasMac = opts.mac != null;
|
|
@@ -517,11 +517,11 @@ function _resolveProtection(opts) {
|
|
|
517
517
|
if (!PBMAC1_PRF[prf]) throw _err("cmp/bad-input", "opts.mac.prf must be SHA-256 / SHA-384 / SHA-512");
|
|
518
518
|
var iterationCount = m.iterationCount != null ? m.iterationCount : PBMAC1_DEFAULT_ITER;
|
|
519
519
|
if (typeof iterationCount !== "number" || !Number.isInteger(iterationCount) || iterationCount < PBMAC1_MIN_ITER) throw _err("cmp/bad-input", "opts.mac.iterationCount must be at least " + PBMAC1_MIN_ITER + " (RFC 8018 sec. 4.2)");
|
|
520
|
-
// Bound the PBKDF2 work factors
|
|
520
|
+
// Bound the PBKDF2 work factors before deriving: a huge iterationCount or keyLength is self-inflicted work.
|
|
521
521
|
if (iterationCount > constants.LIMITS.PBKDF2_MAX_ITERATIONS) throw _err("cmp/bad-input", "opts.mac.iterationCount exceeds the PBKDF2 work-factor cap " + constants.LIMITS.PBKDF2_MAX_ITERATIONS);
|
|
522
522
|
var keyLength = m.keyLength != null ? m.keyLength : PBMAC1_DEFAULT_KEYLEN;
|
|
523
523
|
// RFC 9579 sec. 9 floor (>= 20 bytes): produce only messages the verifier (pki.cmp.verify, same floor) can
|
|
524
|
-
// accept
|
|
524
|
+
// accept, so a shorter derived key is refused at production time and never emitted as an unverifiable message.
|
|
525
525
|
if (typeof keyLength !== "number" || !Number.isInteger(keyLength) || keyLength < PBMAC1_MIN_KEYLEN) throw _err("cmp/bad-input", "opts.mac.keyLength must be an integer >= " + PBMAC1_MIN_KEYLEN + " bytes (RFC 9579 sec. 9)");
|
|
526
526
|
if (keyLength > PBMAC1_MAX_KEYLEN) throw _err("cmp/bad-input", "opts.mac.keyLength exceeds the cap " + PBMAC1_MAX_KEYLEN + " bytes");
|
|
527
527
|
// Bound the COMBINED work: a keyLength spanning multiple PRF blocks costs iterationCount HMACs per block, so
|
|
@@ -530,7 +530,7 @@ function _resolveProtection(opts) {
|
|
|
530
530
|
var blocks = Math.ceil(keyLength / PBMAC1_PRF_HLEN[prf]);
|
|
531
531
|
if (iterationCount * blocks > constants.LIMITS.PBKDF2_MAX_ITERATIONS) throw _err("cmp/bad-input", "opts.mac combined work (iterationCount " + iterationCount + " x " + blocks + " derived blocks) exceeds the PBKDF2 work-factor cap " + constants.LIMITS.PBKDF2_MAX_ITERATIONS);
|
|
532
532
|
var salt = m.salt != null ? _b.reqDer(m.salt, "opts.mac.salt") : Buffer.from(webcrypto.webcrypto.getRandomValues(new Uint8Array(PBMAC1_DEFAULT_SALT_BYTES)));
|
|
533
|
-
// RFC 8018 sec. 4.1 (64-bit) floor: an empty
|
|
533
|
+
// RFC 8018 sec. 4.1 (64-bit) floor: an empty or short salt loses precomputation resistance, so produce only
|
|
534
534
|
// messages pki.cmp.verify (same floor) accepts, refusing a below-minimum salt at construction time.
|
|
535
535
|
if (salt.length < PBMAC1_MIN_SALT) throw _err("cmp/bad-input", "opts.mac.salt must be at least " + PBMAC1_MIN_SALT + " octets (RFC 8018 sec. 4.1)");
|
|
536
536
|
if (salt.length > constants.LIMITS.PBKDF2_MAX_SALT) throw _err("cmp/bad-input", "opts.mac.salt exceeds " + constants.LIMITS.PBKDF2_MAX_SALT + " bytes");
|
|
@@ -550,7 +550,7 @@ function _resolveProtection(opts) {
|
|
|
550
550
|
// ---- orchestrator ----
|
|
551
551
|
|
|
552
552
|
function build(message, opts) {
|
|
553
|
-
// Every caller-owned argument copied at entry and released when the call settles
|
|
553
|
+
// Every caller-owned argument copied at entry and released when the call settles; see the note
|
|
554
554
|
// on the same call in x509-sign. `opts.mac.secret` is why the copy has to be deep: it sits a
|
|
555
555
|
// level below the options object and is read by the PBMAC1 derivation after the first turn.
|
|
556
556
|
return guard.bytes.fixedCall(CmpError, "cmp/bad-input", [
|
|
@@ -626,23 +626,23 @@ function _collectExtraCerts(opts, protCertDer) {
|
|
|
626
626
|
* single-key object naming the arm. Request-side: `{ ir }` / `{ cr }` / `{ kur }` (a `CertReqMessages` spec
|
|
627
627
|
* delegated to `pki.crmf.build`; the proof-of-possession key is `key` on the arm spec), `{ p10cr }` (a PKCS#10
|
|
628
628
|
* CertificationRequest DER), `{ certConf }`, `{ pollReq }`, `{ genm }`, `{ rr }`. CA/responder-side: `{ ip }` /
|
|
629
|
-
* `{ cp }` / `{ kup }` / `{ ccp }` (a `CertRepMessage
|
|
629
|
+
* `{ cp }` / `{ kup }` / `{ ccp }` (a `CertRepMessage`: `caPubs` plus `response` of `CertResponse` each with a
|
|
630
630
|
* `PKIStatusInfo` and, under a granting status, a `certifiedKeyPair`), `{ rp }` (`RevRepContent`), `{ genp }`,
|
|
631
631
|
* `{ error }` (`ErrorMsgContent`), `{ pollRep }`, `{ krp }` (`KeyRecRepContent`), `{ pkiconf }` (NULL). The
|
|
632
|
-
* message is protected: `opts` carries
|
|
633
|
-
*
|
|
632
|
+
* message is protected: `opts` carries exactly
|
|
633
|
+
* one of `{ key, cert }` (a signature under the sender key over the message, using any registry algorithm: RSA
|
|
634
634
|
* / ECDSA / EdDSA / ML-DSA / SLH-DSA / composite, resolved from the certificate) or `{ mac }` (a PBMAC1
|
|
635
635
|
* shared-secret MAC). The protection is computed over the exact DER of the virtual
|
|
636
636
|
* `ProtectedPart ::= SEQUENCE { header, body }` and self-verified before the message is returned. The
|
|
637
637
|
* emitted PKIMessage round-trips byte-identically through `pki.schema.cmp.parse`.
|
|
638
638
|
*
|
|
639
639
|
* @opts
|
|
640
|
-
* - `key` (Buffer|CryptoKey) + `cert` (Buffer)
|
|
640
|
+
* - `key` (Buffer|CryptoKey) + `cert` (Buffer): signature protection under the sender key; `cert` is
|
|
641
641
|
* the signer certificate (its SPKI resolves the algorithm) and is placed in `extraCerts`.
|
|
642
|
-
* - `mac` ({ secret, salt?, iterationCount?, prf?, keyLength? })
|
|
643
|
-
* - `extraCerts` (array of Buffer)
|
|
644
|
-
* - `pem` (boolean|string)
|
|
645
|
-
* - `pss` (boolean) / `digestAlgorithm` (string)
|
|
642
|
+
* - `mac` ({ secret, salt?, iterationCount?, prf?, keyLength? }): PBMAC1 protection from a shared secret.
|
|
643
|
+
* - `extraCerts` (array of Buffer): additional certificates to carry in `extraCerts [1]`.
|
|
644
|
+
* - `pem` (boolean|string): return a PEM `CMP` block instead of DER.
|
|
645
|
+
* - `pss` (boolean) / `digestAlgorithm` (string): signature-protection algorithm options.
|
|
646
646
|
*
|
|
647
647
|
* @example
|
|
648
648
|
* var pair = await pki.key.generate("Ed25519");
|
|
@@ -839,17 +839,17 @@ function wellKnownUrl(base, opts) {
|
|
|
839
839
|
* @related pki.cmp.build, pki.cmp.wellKnownUrl, pki.schema.cmp.parse, pki.transport.https
|
|
840
840
|
*
|
|
841
841
|
* POST a DER `PKIMessage` to a CMP endpoint and return the parsed response `PKIMessage`, over the shared
|
|
842
|
-
* `pki.transport`. RFC 9811 transfers every CMP exchange identically
|
|
843
|
-
* one response PKIMessage
|
|
842
|
+
* `pki.transport`. RFC 9811 transfers every CMP exchange identically, as one HTTP POST of a DER PKIMessage
|
|
843
|
+
* and one response PKIMessage, so a single stateless verb carries `ir` / `cr` / `kur` / `p10cr` / `certConf`
|
|
844
844
|
* / `pollReq` / `rr` / `genm` and their responses; the caller builds and protects the message upstream with
|
|
845
845
|
* `pki.cmp.build` and hands the finished bytes here. `message` is a DER `Buffer`/`Uint8Array` or a PEM `CMP`
|
|
846
|
-
* string, sent
|
|
846
|
+
* string, sent verbatim (the message-layer protection covers these exact bytes; they are never re-encoded).
|
|
847
847
|
* The response is classified fail-closed: HTTP 200 carrying an `application/pkixcmp` body is parsed and
|
|
848
848
|
* resolved; another 2xx is `cmp/unexpected-status` (RFC 9811 requires 200); a 3xx is `cmp/redirect-not-followed`
|
|
849
849
|
* (never auto-followed, sec. 3.1/5); a 4xx/5xx carrying a well-formed CMP `error` PKIMessage FORWARDS that
|
|
850
850
|
* integrity-protected verdict (sec. 1.2/3.1) with the HTTP status surfaced as data, while a 4xx/5xx that is
|
|
851
851
|
* not a CMP `error` message (no body, an undecodable body, or a non-error arm) is `cmp/http-error`.
|
|
852
|
-
* Protection is
|
|
852
|
+
* Protection is surfaced, not verified: the client confers no trust, and the
|
|
853
853
|
* caller (or a future `pki.cmp.verify`) checks the response protection using the raw `headerBytes`/`bodyBytes`.
|
|
854
854
|
* By default the transport is https-only and requires an explicit trust anchor; there is no client scheme
|
|
855
855
|
* gate, so an operator who injects an http-capable transport reaches the RFC-9811-permitted plain-HTTP path.
|