@blamejs/pki 0.4.15 → 0.5.0
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 +37 -1
- package/MIGRATING.md +2 -2
- package/README.md +142 -137
- package/index.js +4 -0
- package/lib/acme.js +73 -1
- package/lib/asn1-der.js +2 -0
- package/lib/attrcert-sign.js +4 -0
- package/lib/cbor-det.js +32 -16
- package/lib/cmc-build.js +880 -0
- package/lib/cmc-verify.js +657 -0
- package/lib/cmp-build.js +8 -7
- package/lib/cmp-verify.js +11 -1
- package/lib/cms-sign.js +170 -8
- package/lib/cms-verify.js +80 -14
- package/lib/crl-sign.js +22 -0
- package/lib/crmf-sign.js +5 -2
- package/lib/csr-sign.js +3 -0
- package/lib/ct.js +72 -0
- package/lib/est.js +828 -32
- package/lib/framework-error.js +13 -0
- package/lib/guard-bytes.js +37 -1
- package/lib/guard-range.js +23 -1
- package/lib/http-transport.js +9 -3
- package/lib/inspect.js +28 -5
- package/lib/jose.js +15 -0
- package/lib/lint.js +4 -0
- package/lib/merkle.js +5 -5
- package/lib/ocsp.js +139 -11
- package/lib/oid.js +69 -1
- package/lib/path-validate.js +27 -4
- package/lib/pkcs12-build.js +12 -0
- package/lib/schema-all.js +19 -1
- package/lib/schema-attrcert.js +27 -0
- package/lib/schema-c509.js +6 -0
- package/lib/schema-cmc.js +791 -0
- package/lib/schema-cmp.js +25 -0
- package/lib/schema-cms.js +17 -1
- package/lib/schema-crl.js +23 -1
- package/lib/schema-crmf.js +13 -0
- package/lib/schema-csr.js +11 -0
- package/lib/schema-csrattrs.js +6 -0
- package/lib/schema-engine.js +6 -2
- package/lib/schema-ocsp.js +41 -0
- package/lib/schema-pkcs12.js +16 -0
- package/lib/schema-pkcs8.js +8 -0
- package/lib/schema-smime.js +4 -4
- package/lib/schema-tsp.js +32 -1
- package/lib/schema-x509.js +14 -1
- package/lib/shbs.js +12 -4
- package/lib/sigstore.js +4 -0
- package/lib/smime.js +28 -7
- package/lib/tls-cert-compress.js +15 -3
- package/lib/trust.js +27 -4
- package/lib/tsp-sign.js +41 -6
- package/lib/vendor/README.md +19 -19
- package/lib/webauthn.js +895 -26
- package/lib/x509-sign.js +3 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @module pki.cmc
|
|
6
|
+
* @nav Enrollment
|
|
7
|
+
* @title CMC
|
|
8
|
+
* @intro Interpret an RFC 5272 Full PKI Response. `verify(response, sent)` takes the response
|
|
9
|
+
* a CA returned and the state the client retained from its request, binds the two together
|
|
10
|
+
* (transaction identifier, the Sender/Recipient Nonce echo, the Data Return echo), reads the
|
|
11
|
+
* ordered status verdicts, and reduces them to ONE terminal outcome: `issued`, `pending`,
|
|
12
|
+
* `confirm-required`, `pop-required` or `rejected`. The issued certificates come from the CMS
|
|
13
|
+
* certificate bag, where RFC 5272 sec. 4.2 puts them. Nothing here is trusted: the bag and any
|
|
14
|
+
* Publish Trust Anchors control are surfaced as DATA for the caller to validate through
|
|
15
|
+
* `pki.path.validate`.
|
|
16
|
+
* @spec RFC 5272, RFC 5273, RFC 6402
|
|
17
|
+
* @card Interpret a CMC Full PKI Response into one terminal verdict -- transaction and nonce
|
|
18
|
+
* binding, the status verdicts, the certificate bag surfaced untrusted.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
var asn1 = require("./asn1-der");
|
|
22
|
+
var oid = require("./oid");
|
|
23
|
+
var cmc = require("./schema-cmc");
|
|
24
|
+
var cmsVerify = require("./cms-verify"); // PD14: the carrier's signature MUST be verified
|
|
25
|
+
var cmsDecrypt = require("./cms-decrypt"); // ... and an AuthenticatedData carrier by its MAC
|
|
26
|
+
var guard = require("./guard-all");
|
|
27
|
+
var frameworkError = require("./framework-error");
|
|
28
|
+
|
|
29
|
+
var CmcError = frameworkError.CmcError;
|
|
30
|
+
function E(code, message, cause) { return new CmcError(code, message, cause); }
|
|
31
|
+
function O(name) { return oid.byName(name); }
|
|
32
|
+
|
|
33
|
+
var OID_TRANSACTION_ID = O("id-cmc-transactionId");
|
|
34
|
+
var OID_SENDER_NONCE = O("id-cmc-senderNonce");
|
|
35
|
+
var OID_RECIPIENT_NONCE = O("id-cmc-recipientNonce");
|
|
36
|
+
var OID_DATA_RETURN = O("id-cmc-dataReturn");
|
|
37
|
+
var OID_TRUSTED_ANCHORS = O("id-cmc-trustedAnchors");
|
|
38
|
+
var OID_CONFIRM_CERT_ACCEPTANCE = O("id-cmc-confirmCertAcceptance");
|
|
39
|
+
|
|
40
|
+
// CMCStatus -> the terminal outcome a caller acts on. RFC 5272 sec. 6.1.3 gives
|
|
41
|
+
// seven statuses; four of them mean "this exchange did not produce a certificate
|
|
42
|
+
// and will not without another message", so they collapse to `rejected` while the
|
|
43
|
+
// status list keeps the detail.
|
|
44
|
+
var OUTCOME_BY_STATUS = {
|
|
45
|
+
success: "issued",
|
|
46
|
+
pending: "pending",
|
|
47
|
+
partial: "pending",
|
|
48
|
+
confirmRequired: "confirm-required",
|
|
49
|
+
popRequired: "pop-required",
|
|
50
|
+
failed: "rejected",
|
|
51
|
+
noSupport: "rejected",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Which outcome governs when a response carries SEVERAL status controls (sec.
|
|
55
|
+
// 6.1 requires a client to cope with that). Ranked worst-first so the verdict can
|
|
56
|
+
// never be improved by the ORDER the controls happen to appear in -- reporting
|
|
57
|
+
// the first one seen would let a server bury a failure behind a success.
|
|
58
|
+
var OUTCOME_SEVERITY = { rejected: 4, "pop-required": 3, "confirm-required": 2, pending: 1, issued: 0 };
|
|
59
|
+
|
|
60
|
+
// The single AttributeValue of a control that carries exactly one.
|
|
61
|
+
function _singleValue(control, what) {
|
|
62
|
+
if (control.values.length !== 1) {
|
|
63
|
+
throw E("cmc/bad-control", "the " + what + " control carries exactly one value, got " + control.values.length);
|
|
64
|
+
}
|
|
65
|
+
return control.values[0];
|
|
66
|
+
}
|
|
67
|
+
// The single instance of a control, or null when absent.
|
|
68
|
+
//
|
|
69
|
+
// Body-part identity is unique per element, so a responder can legally carry the
|
|
70
|
+
// SAME control type twice under different bodyPartIDs. Taking the first match
|
|
71
|
+
// would let an attacker pair one correct echo with a contradictory one and still
|
|
72
|
+
// satisfy the binding -- so a duplicate is refused as ambiguous rather than
|
|
73
|
+
// resolved. The exchange either has one answer or it has none.
|
|
74
|
+
function _findControl(controls, attrType, what) {
|
|
75
|
+
var found = null;
|
|
76
|
+
for (var i = 0; i < controls.length; i++) {
|
|
77
|
+
if (controls[i].attrType !== attrType) continue;
|
|
78
|
+
if (found) {
|
|
79
|
+
throw E("cmc/duplicate-control",
|
|
80
|
+
"the response carries more than one " + what + " control; which one binds the exchange is ambiguous, so it is refused");
|
|
81
|
+
}
|
|
82
|
+
found = controls[i];
|
|
83
|
+
}
|
|
84
|
+
return found;
|
|
85
|
+
}
|
|
86
|
+
function _octets(control, what) {
|
|
87
|
+
return asn1.read.octetString(asn1.decode(_singleValue(control, what)));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The certificate / CRL bag, from whichever carrier holds it. A SignedData keeps
|
|
91
|
+
// them at the top level; an AuthenticatedData keeps them under `originatorInfo`
|
|
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 -- and
|
|
94
|
+
// "no certificates were issued" is exactly the wrong thing to report when some
|
|
95
|
+
// were.
|
|
96
|
+
function _bagOf(parsedCms, originatorKey, topKey) {
|
|
97
|
+
if (!parsedCms) return [];
|
|
98
|
+
var list = parsedCms[topKey];
|
|
99
|
+
if ((!list || !list.length) && parsedCms.originatorInfo) list = parsedCms.originatorInfo[originatorKey];
|
|
100
|
+
return (list || []).map(function (c) { return c.bytes; });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The transaction binding -- RFC 5272 sec. 6.6, RFC 5273 sec. 6.
|
|
105
|
+
*
|
|
106
|
+
* Both halves are CONDITIONAL on what the client sent, which is the subtlety: a
|
|
107
|
+
* client that sent no transactionId cannot demand one back, and a client that
|
|
108
|
+
* sent no senderNonce has nothing to compare a recipientNonce against. What is
|
|
109
|
+
* NOT conditional is the converse -- having sent one, an absent or differing echo
|
|
110
|
+
* is a refusal, because that is exactly the replay the nonce exists to stop.
|
|
111
|
+
*/
|
|
112
|
+
function _assertBound(body, sent) {
|
|
113
|
+
if (sent.transactionId != null) {
|
|
114
|
+
var txControl = _findControl(body.controls, OID_TRANSACTION_ID, "Transaction Identifier");
|
|
115
|
+
if (!txControl) {
|
|
116
|
+
throw E("cmc/transaction-mismatch",
|
|
117
|
+
"the request carried a Transaction Identifier control, so the response MUST include the same one (RFC 5272 sec. 6.6)");
|
|
118
|
+
}
|
|
119
|
+
var got = asn1.read.integer(asn1.decode(_singleValue(txControl, "Transaction Identifier")));
|
|
120
|
+
// Compared against the value the authoring guard validated, never a fresh
|
|
121
|
+
// conversion of the caller's input -- a number too large to hold an integer
|
|
122
|
+
// exactly has already lost the digits that distinguish it from its neighbour.
|
|
123
|
+
if (got !== sent.transactionIdValue) {
|
|
124
|
+
throw E("cmc/transaction-mismatch",
|
|
125
|
+
"the response Transaction Identifier " + got + " does not match the request's " + sent.transactionId);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (sent.senderNonce != null) {
|
|
130
|
+
var rn = _findControl(body.controls, OID_RECIPIENT_NONCE, "Recipient Nonce");
|
|
131
|
+
if (!rn) {
|
|
132
|
+
throw E("cmc/nonce-mismatch",
|
|
133
|
+
"the request carried a Sender Nonce, so the response MUST reflect it back as a Recipient Nonce control (RFC 5272 sec. 6.6)");
|
|
134
|
+
}
|
|
135
|
+
// Constant-time and by FULL value: guard.crypto.constantTimeEqual gates the
|
|
136
|
+
// length first, so a truncation or a prefix is an honest false rather than a
|
|
137
|
+
// match on the bytes that happen to line up.
|
|
138
|
+
if (!guard.crypto.constantTimeEqual(_octets(rn, "Recipient Nonce"), Buffer.from(sent.senderNonce))) {
|
|
139
|
+
throw E("cmc/nonce-mismatch", "the response Recipient Nonce does not match the Sender Nonce the request sent");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// RFC 5272 sec. 6.4: "If the Data Return control appears in a Full PKI Request,
|
|
144
|
+
// 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 SAME bytes came back.
|
|
146
|
+
if (sent.dataReturn != null) {
|
|
147
|
+
var dr = _findControl(body.controls, OID_DATA_RETURN, "Data Return");
|
|
148
|
+
if (!dr) {
|
|
149
|
+
throw E("cmc/data-return-missing",
|
|
150
|
+
"the request carried a Data Return control, so the server MUST return it in the PKI Response (RFC 5272 sec. 6.4)");
|
|
151
|
+
}
|
|
152
|
+
if (!_octets(dr, "Data Return").equals(Buffer.from(sent.dataReturn))) {
|
|
153
|
+
throw E("cmc/data-return-mismatch", "the returned Data Return control does not carry the bytes the request sent");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// A status names, in its bodyList, the body parts it is ABOUT (RFC 5272
|
|
158
|
+
// sec. 6.1.1). A status about a body part the request never carried is not an
|
|
159
|
+
// answer to this request -- and the binding controls do not catch it, because a
|
|
160
|
+
// server can echo the transaction and nonce correctly while reporting on
|
|
161
|
+
// something else entirely. Held to the same asymmetry as every other half of the
|
|
162
|
+
// binding: checked only when the client kept the set, and once kept, a reference
|
|
163
|
+
// outside it is a refusal.
|
|
164
|
+
// Element-by-element, and the lengths must agree: a retained `[a, b]` must not admit a
|
|
165
|
+
// reported `[a, b, c]`, which names a part one level deeper than anything sent.
|
|
166
|
+
function _pathRetained(retained, want) {
|
|
167
|
+
if (!Array.isArray(retained)) return false;
|
|
168
|
+
return retained.some(function (have) {
|
|
169
|
+
return Array.isArray(have) && have.length === want.length && have.every(function (seg, i) {
|
|
170
|
+
return String(seg) === String(want[i]);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
if (Array.isArray(sent.bodyPartIDs)) {
|
|
175
|
+
var known = Object.create(null);
|
|
176
|
+
sent.bodyPartIDs.forEach(function (id) { known[String(id)] = true; });
|
|
177
|
+
// 0 is reserved as the reference to the enclosing PKIData itself (sec. 3.2.1),
|
|
178
|
+
// so a status about the request AS A WHOLE is in the set by definition.
|
|
179
|
+
known["0"] = true;
|
|
180
|
+
body.statuses.forEach(function (s) {
|
|
181
|
+
(s.bodyList || []).forEach(function (ref) {
|
|
182
|
+
var path = ref.bodyPartPath;
|
|
183
|
+
// A bodyPartPath descends INTO a nested message. Checking only its head would
|
|
184
|
+
// accept `[a part we sent, 999]` -- a status about something arbitrary, wearing a
|
|
185
|
+
// reference that passes -- so the WHOLE path is matched against the paths the
|
|
186
|
+
// request actually composed. Those are retained by reading each nested message
|
|
187
|
+
// back; a nested message that could not be read back contributes none, so a path
|
|
188
|
+
// into it finds no match and is refused. Either way the tail is never waved through.
|
|
189
|
+
if (path && path.length > 1) {
|
|
190
|
+
if (_pathRetained(sent.bodyPartPaths, path)) return;
|
|
191
|
+
throw E("cmc/body-part-unknown",
|
|
192
|
+
"a status control reports on a body part nested inside another message (path " + path.join("/") +
|
|
193
|
+
"), which this request never sent -- nothing here can confirm what that names " +
|
|
194
|
+
"(RFC 5272 sec. 6.1.1)");
|
|
195
|
+
}
|
|
196
|
+
var id = ref.bodyPartID != null ? ref.bodyPartID : (path && path.length ? path[0] : null);
|
|
197
|
+
if (id == null || known[String(id)]) return;
|
|
198
|
+
throw E("cmc/body-part-unknown",
|
|
199
|
+
"a status control reports on body part " + id + ", which this request never sent -- the response " +
|
|
200
|
+
"is about a different message (RFC 5272 sec. 6.1.1)");
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// The worst outcome across every status control, with success assumed when there
|
|
207
|
+
// is none at all (RFC 5272 sec. 6.1.2: "If no status exists for a Simple or Full
|
|
208
|
+
// PKI Request, then the value of success is assumed").
|
|
209
|
+
function _reduceOutcome(statuses) {
|
|
210
|
+
var outcome = "issued";
|
|
211
|
+
for (var i = 0; i < statuses.length; i++) {
|
|
212
|
+
var candidate = OUTCOME_BY_STATUS[statuses[i].status];
|
|
213
|
+
if (OUTCOME_SEVERITY[candidate] > OUTCOME_SEVERITY[outcome]) outcome = candidate;
|
|
214
|
+
}
|
|
215
|
+
return outcome;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// The pendInfo / failInfo detail belonging to the status that GOVERNED, so a
|
|
219
|
+
// caller reading `pendToken` gets the one attached to the pending verdict rather
|
|
220
|
+
// than whichever status happened to carry a token.
|
|
221
|
+
function _governingStatus(statuses, outcome) {
|
|
222
|
+
for (var i = 0; i < statuses.length; i++) {
|
|
223
|
+
if (OUTCOME_BY_STATUS[statuses[i].status] === outcome) return statuses[i];
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* @primitive pki.cmc.verify
|
|
230
|
+
* @signature pki.cmc.verify(response, sent?) -> Promise<verdict>
|
|
231
|
+
* @since 0.4.16
|
|
232
|
+
* @status experimental
|
|
233
|
+
* @spec RFC 5272, RFC 5273, RFC 6402
|
|
234
|
+
* @defends cmc-response-replay (CWE-294)
|
|
235
|
+
* @related pki.schema.cmc.parse, pki.path.validate, pki.cms.verify
|
|
236
|
+
*
|
|
237
|
+
* Interpret a Full PKI Response into ONE terminal verdict. `response` is the DER or a PEM `CMS`
|
|
238
|
+
* block; an already-parsed message is interpreted too, but only under `allowUnverified: true`,
|
|
239
|
+
* because both carriers authenticate over BYTES and a parsed object the caller still owns pins
|
|
240
|
+
* none. `sent` is what the client retained from its request --
|
|
241
|
+
* `transactionId`, `senderNonce`, `dataReturn`, and `bodyPartIDs` (every identifier the request
|
|
242
|
+
* carried). Each of those is checked only if it was sent, and once sent an absent or differing
|
|
243
|
+
* echo is a refusal: that asymmetry is the replay defence. `bodyPartIDs` is the same rule applied
|
|
244
|
+
* to what the response is ABOUT -- a status naming a body part the request never sent is refused
|
|
245
|
+
* with `cmc/body-part-unknown`, which the transaction and nonce cannot catch, since a server can
|
|
246
|
+
* echo both correctly while reporting on something else.
|
|
247
|
+
*
|
|
248
|
+
* The verdict carries the response's own `cmsSequence` and `otherMsgs` raw, because a request whose
|
|
249
|
+
* only arm was the other-message form has no certificate to return and RFC 5272 sec. 4.1 puts its
|
|
250
|
+
* answer there instead.
|
|
251
|
+
*
|
|
252
|
+
* The verdict's `outcome` is `issued`, `pending`, `confirm-required`, `pop-required` or
|
|
253
|
+
* `rejected`. When a response carries several status controls the WORST governs, so a failure
|
|
254
|
+
* cannot be hidden behind a success earlier in the sequence.
|
|
255
|
+
*
|
|
256
|
+
* The carrier's signature MUST be verified (RFC 5272 sec. 3.2.1.3.4). A conforming SignedData
|
|
257
|
+
* carries its own signer certificate, so the ordinary case needs nothing from the caller: the
|
|
258
|
+
* signature is checked and the verdict reports `signatureVerified: true`. Where the signer cannot be
|
|
259
|
+
* found -- not embedded and not supplied -- the posture is fail-closed with a NAMED opt-out rather
|
|
260
|
+
* than a silent one: pass `certs` with the responder's certificate, or `allowUnverified: true` and
|
|
261
|
+
* the verdict reports `signatureVerified: false`. Doing neither is refused, because that is the case
|
|
262
|
+
* where a caller would otherwise assume a check happened. The opt-out covers "could not check",
|
|
263
|
+
* never "checked and it failed": a signature that is present and wrong is always a refusal. A
|
|
264
|
+
* carrier with no SignerInfo at all is not a Full PKI Response and is refused outright.
|
|
265
|
+
*
|
|
266
|
+
* `certs` SUPPLEMENTS the certificates the message carries; it does not pin the signer (a SignedData
|
|
267
|
+
* names its own, which is what the signature is checked against) and it establishes no trust.
|
|
268
|
+
* Deciding the signer is acceptable is path validation, which is the caller's.
|
|
269
|
+
*
|
|
270
|
+
* The other carrier sec. 3.2 permits, AuthenticatedData, is authenticated by its MAC instead: pass
|
|
271
|
+
* `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 rather than the unauthenticated opt-out.
|
|
273
|
+
*
|
|
274
|
+
* Nothing is trusted here. `certificates` is the CMS certificate bag -- where RFC 5272 sec. 4.2
|
|
275
|
+
* puts the issued certificates -- surfaced raw for the caller to run through `pki.path.validate`,
|
|
276
|
+
* and a Publish Trust Anchors control is surfaced as `publishTrustAnchors` with `trusted: false`,
|
|
277
|
+
* never added to any store (RFC 5272 sec. 6.15 makes accepting one a four-part manual decision).
|
|
278
|
+
* The CMS signature is likewise the caller's to verify through `pki.cms.verify`.
|
|
279
|
+
*
|
|
280
|
+
* @opts
|
|
281
|
+
* - `transactionId` (number|bigint) -- the Transaction Identifier the request sent.
|
|
282
|
+
* - `senderNonce` (Buffer) -- the Sender Nonce the request sent, echoed back as Recipient Nonce.
|
|
283
|
+
* - `dataReturn` (Buffer) -- the Data Return payload the request sent, echoed verbatim.
|
|
284
|
+
* - `certs` (Buffer[]) -- extra certificates for signer lookup, for a message that does not carry
|
|
285
|
+
* its own signer; the certificates the message carries are searched either way.
|
|
286
|
+
* - `recipient` (object) -- key material for an AuthenticatedData carrier, in the shape
|
|
287
|
+
* `pki.cms.decrypt` takes. Its MAC is then checked and the verdict reports
|
|
288
|
+
* `signatureVerified: true`; the content it authenticates is bound to the content the verdict
|
|
289
|
+
* was read from, so a MAC over other bytes cannot stand in for it.
|
|
290
|
+
* - `allowUnverified` (boolean) -- interpret without verifying the carrier; sets `signatureVerified: false`.
|
|
291
|
+
* @example
|
|
292
|
+
* var b = pki.asn1.build, oid = pki.oid;
|
|
293
|
+
* var sid = b.sequence([b.sequence([b.set([b.sequence([b.oid(oid.byName("commonName")),
|
|
294
|
+
* b.utf8("CA")])])]), b.integer(1n)]);
|
|
295
|
+
* var attrs = b.contextConstructed(0, Buffer.concat([
|
|
296
|
+
* b.sequence([b.oid(oid.byName("contentType")), b.set([b.oid(oid.byName("id-cct-PKIResponse"))])]),
|
|
297
|
+
* b.sequence([b.oid(oid.byName("messageDigest")), b.set([b.octetString(Buffer.alloc(32, 3))])])]));
|
|
298
|
+
* var si = b.sequence([b.integer(1n), sid, b.sequence([b.oid(oid.byName("sha256")), b.nullValue()]),
|
|
299
|
+
* attrs, b.sequence([b.oid(oid.byName("rsaEncryption")), b.nullValue()]), b.octetString(Buffer.alloc(8, 1))]);
|
|
300
|
+
* var body = b.sequence([b.sequence([]), b.sequence([]), b.sequence([])]);
|
|
301
|
+
* var encap = b.sequence([b.oid(oid.byName("id-cct-PKIResponse")), b.explicit(0, b.octetString(body))]);
|
|
302
|
+
* var sd = b.sequence([b.integer(3n), b.set([]), encap, b.set([si])]);
|
|
303
|
+
* var der = b.sequence([b.oid(oid.byName("signedData")), b.explicit(0, sd)]);
|
|
304
|
+
* var v = await pki.cmc.verify(der, { allowUnverified: true });
|
|
305
|
+
* v.outcome; // "issued" -- no status control means success is assumed
|
|
306
|
+
* v.signatureVerified; // false -- the opt-out was named, so nothing was checked
|
|
307
|
+
*/
|
|
308
|
+
function verify(response, sent) {
|
|
309
|
+
// Both snapshots are taken SYNCHRONOUSLY, here, before anything is deferred.
|
|
310
|
+
// Taking them inside the async body would be too late by one microtask: a caller
|
|
311
|
+
// that mutates the buffer or the options object on the line after this call has
|
|
312
|
+
// already changed them before that body ever runs, which is the easiest version
|
|
313
|
+
// of the race to hit by accident.
|
|
314
|
+
var frozenResponse, frozenSent;
|
|
315
|
+
try {
|
|
316
|
+
frozenSent = _snapshotSent(_assertOpts(sent));
|
|
317
|
+
frozenResponse = _snapshotIfBytes(response);
|
|
318
|
+
} catch (e) {
|
|
319
|
+
return Promise.reject(e); // the surface stays promise-rejecting, never throwing
|
|
320
|
+
}
|
|
321
|
+
return Promise.resolve().then(function () { return _verify(frozenResponse, frozenSent); });
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function _assertOpts(sent) {
|
|
325
|
+
if (sent == null) return {};
|
|
326
|
+
if (typeof sent !== "object" || Array.isArray(sent) || Buffer.isBuffer(sent)) {
|
|
327
|
+
throw E("cmc/bad-input", "pki.cmc.verify options must be an object");
|
|
328
|
+
}
|
|
329
|
+
return sent;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// A private copy of the input when -- and only when -- it is memory the caller can
|
|
333
|
+
// still write to. Anything else is returned untouched so this cannot narrow the
|
|
334
|
+
// input contract of the parse it feeds; pki.schema.cmc.parse stays the one place
|
|
335
|
+
// that decides what an acceptable input is, and keeps raising its own typed error.
|
|
336
|
+
function _snapshotIfBytes(input) {
|
|
337
|
+
// EVERY byte form the parser accepts, not just the two most common. It takes a
|
|
338
|
+
// BufferSource, so an ArrayBuffer or a DataView reaches the decoder too -- and
|
|
339
|
+
// leaving those aliased would reopen the window for exactly the inputs that came
|
|
340
|
+
// in by the wider door.
|
|
341
|
+
if (ArrayBuffer.isView(input) || input instanceof ArrayBuffer) {
|
|
342
|
+
return guard.bytes.snapshotSource(input, CmcError, "cmc/bad-input", "pki.cmc.verify");
|
|
343
|
+
}
|
|
344
|
+
return input;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// A private copy of the exchange state the binding checks compare against. Scalars
|
|
348
|
+
// are copied by value and every byte buffer is copied, so nothing the caller still
|
|
349
|
+
// holds a reference to can change what "what we sent" means once verification has
|
|
350
|
+
// begun. Unknown fields are carried through untouched -- this fixes the values the
|
|
351
|
+
// checks below read, it does not narrow what the options object may contain.
|
|
352
|
+
// EVERY byte form, not just the two most common: a DataView or a bare ArrayBuffer arrives by the
|
|
353
|
+
// same door as a Buffer, and copying only Buffer and Uint8Array would leave those aliased across
|
|
354
|
+
// the very gap this closes -- reopening the window for exactly the inputs that came in by the
|
|
355
|
+
// wider one. A DataView is copied over its OWN window, not the whole buffer it happens to sit in.
|
|
356
|
+
function _copyAnyBytes(v) {
|
|
357
|
+
if (Buffer.isBuffer(v) || v instanceof Uint8Array) return Buffer.from(v);
|
|
358
|
+
if (ArrayBuffer.isView(v)) return Buffer.from(new Uint8Array(v.buffer, v.byteOffset, v.byteLength));
|
|
359
|
+
if (v instanceof ArrayBuffer) return Buffer.from(new Uint8Array(v));
|
|
360
|
+
return v;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function _snapshotSent(sent) {
|
|
364
|
+
var out = {}, k;
|
|
365
|
+
for (k in sent) { if (Object.prototype.hasOwnProperty.call(sent, k)) out[k] = sent[k]; }
|
|
366
|
+
// Normalized through the SAME authoring guard pki.cmc.build puts an authored
|
|
367
|
+
// integer through. A Transaction Identifier is an unbounded INTEGER on the wire,
|
|
368
|
+
// so one above Number.MAX_SAFE_INTEGER has already been rounded by the time it
|
|
369
|
+
// arrives as a `number` -- 9007199254740993 is 9007199254740992 before this code
|
|
370
|
+
// sees it, and a response echoing that NEIGHBOURING identifier would compare
|
|
371
|
+
// equal. Held here rather than converted, so a value too large to be a number is
|
|
372
|
+
// refused with the shape to use instead (a bigint) rather than silently bound to
|
|
373
|
+
// something adjacent.
|
|
374
|
+
// Validated, not replaced: the verdict echoes `transactionId` as the caller gave
|
|
375
|
+
// it, so the check is added without changing what comes back.
|
|
376
|
+
// Through ONE helper so nothing here can copy "the two common byte forms" and leave the rest
|
|
377
|
+
// aliased -- the narrowing that reopens this window for whatever came in by the wider door.
|
|
378
|
+
if (out.transactionId != null) {
|
|
379
|
+
out.transactionIdValue = guard.range.authoredInteger(out.transactionId, E, "cmc/bad-input", "sent.transactionId");
|
|
380
|
+
}
|
|
381
|
+
out.senderNonce = _copyAnyBytes(out.senderNonce);
|
|
382
|
+
out.dataReturn = _copyAnyBytes(out.dataReturn);
|
|
383
|
+
// Copied for the same reason the nonce is: the check runs after an await, and an
|
|
384
|
+
// array the caller still holds could have body parts appended to it in the gap,
|
|
385
|
+
// widening what the response is allowed to report on.
|
|
386
|
+
if (Array.isArray(out.bodyPartIDs)) out.bodyPartIDs = out.bodyPartIDs.slice();
|
|
387
|
+
// Both levels: the outer array AND each path, since appending a segment to a retained path
|
|
388
|
+
// in the gap would widen what the response may report on exactly as appending an id does.
|
|
389
|
+
if (Array.isArray(out.bodyPartPaths)) {
|
|
390
|
+
out.bodyPartPaths = out.bodyPartPaths.map(function (p) { return Array.isArray(p) ? p.slice() : p; });
|
|
391
|
+
}
|
|
392
|
+
if (out.recipient && typeof out.recipient === "object") {
|
|
393
|
+
var r = {}, rk;
|
|
394
|
+
for (rk in out.recipient) {
|
|
395
|
+
if (!Object.prototype.hasOwnProperty.call(out.recipient, rk)) continue;
|
|
396
|
+
r[rk] = _copyAnyBytes(out.recipient[rk]);
|
|
397
|
+
}
|
|
398
|
+
out.recipient = r;
|
|
399
|
+
}
|
|
400
|
+
if (Array.isArray(out.certs)) out.certs = out.certs.map(_copyAnyBytes);
|
|
401
|
+
out.allowUnverified = sent.allowUnverified === true;
|
|
402
|
+
return out;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// `response` and `sent` arrive already frozen by verify() above -- BOTH sides of
|
|
406
|
+
// every comparison, not just the response bytes. The checks below run across an
|
|
407
|
+
// await and compare what the CA returned against what the caller says it sent, so
|
|
408
|
+
// freezing one side while reading the other live would leave half of each
|
|
409
|
+
// comparison mutable.
|
|
410
|
+
function _verify(response, sent) {
|
|
411
|
+
var body = cmc.parse(response);
|
|
412
|
+
if (body.kind !== "pkiResponse") {
|
|
413
|
+
throw E("cmc/not-a-response",
|
|
414
|
+
"pki.cmc.verify interprets a Full PKI Response (id-cct-PKIResponse); this message is a " + body.kind);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return _assertAuthentic(body, sent, response).then(function (signatureVerified) {
|
|
418
|
+
_assertBound(body, sent);
|
|
419
|
+
return _shape(body, sent, signatureVerified);
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* PD14 -- RFC 5272 sec. 3.2.1.3.4: "As part of processing a PKI Request/Response,
|
|
425
|
+
* the signature(s) MUST be verified."
|
|
426
|
+
*
|
|
427
|
+
* A Full PKI Response is a SignedData (sec. 4.2), so a carrier with no SignerInfo
|
|
428
|
+
* at all is not one -- that is the Simple response's shape, and reading an
|
|
429
|
+
* `issued` verdict off it would report an enrollment result nobody signed.
|
|
430
|
+
*
|
|
431
|
+
* Verification needs the signer's certificate, which this layer cannot invent.
|
|
432
|
+
* So the posture is fail-closed with a NAMED opt-out rather than a silent one:
|
|
433
|
+
* supply `certs` and the signature is checked; supply `allowUnverified: true` and
|
|
434
|
+
* the verdict says `signatureVerified: false` so nothing downstream can mistake
|
|
435
|
+
* it for an authenticated answer. Doing neither is refused, because that is the
|
|
436
|
+
* case where a caller would otherwise believe a check happened.
|
|
437
|
+
*/
|
|
438
|
+
function _assertAuthentic(body, sent, responseBytes) {
|
|
439
|
+
return Promise.resolve().then(function () {
|
|
440
|
+
// Authentication is over BYTES, and that is true of BOTH carriers -- a MAC and
|
|
441
|
+
// a signature alike. The already-parsed input form hands this layer an object
|
|
442
|
+
// the CALLER still owns: the verdict is read from it synchronously, the check
|
|
443
|
+
// runs a microtask later, and a caller that swaps the encapsulated content,
|
|
444
|
+
// the signers and the certificate bag in the gap would get one message's
|
|
445
|
+
// verdict reported beside another message's verified signature. There are no
|
|
446
|
+
// bytes to pin, so the honest answer is that nothing was authenticated.
|
|
447
|
+
var haveBytes = Buffer.isBuffer(responseBytes) || responseBytes instanceof Uint8Array ||
|
|
448
|
+
typeof responseBytes === "string";
|
|
449
|
+
// The two accepted carriers are authenticated DIFFERENTLY, so the rule has to
|
|
450
|
+
// be written for both: a SignedData by its SignerInfo signatures, an
|
|
451
|
+
// AuthenticatedData by a MAC over a key this layer never holds. Checking only
|
|
452
|
+
// the SignedData shape would reject every conforming AuthenticatedData
|
|
453
|
+
// response for having no signer -- which is not a defect in the message.
|
|
454
|
+
if (body.cms && body.cms.contentTypeName === "authData") {
|
|
455
|
+
// The MAC needs key material this layer does not invent -- but a caller who
|
|
456
|
+
// HAS it should not be forced through the unauthenticated opt-out, which
|
|
457
|
+
// would report signatureVerified:false for a response whose MAC checked out.
|
|
458
|
+
// pki.cms.decrypt owns AuthenticatedData; `recipient` is handed to it rather
|
|
459
|
+
// than the MAC being recomputed here.
|
|
460
|
+
if (sent.recipient != null) {
|
|
461
|
+
// pki.cms.decrypt deliberately refuses a pre-parsed object too, so nothing
|
|
462
|
+
// can hand it a parse result that skipped DER validation. When the caller
|
|
463
|
+
// used the already-parsed input form there are no bytes to check, and
|
|
464
|
+
// saying exactly that is the honest answer -- reporting "did not
|
|
465
|
+
// authenticate" would blame the message for what is a missing input.
|
|
466
|
+
if (!haveBytes) {
|
|
467
|
+
throw E("cmc/bad-input",
|
|
468
|
+
"authenticating an AuthenticatedData response needs the response as DER bytes or PEM, " +
|
|
469
|
+
"because its MAC is computed over them -- pass the encoded response rather than a parsed one");
|
|
470
|
+
}
|
|
471
|
+
// It re-parses, and the content check below then ties its result back to
|
|
472
|
+
// what was interpreted here.
|
|
473
|
+
return cmsDecrypt.decrypt(responseBytes, sent.recipient).then(null, function (e) {
|
|
474
|
+
// The CMS layer's code does not escape this surface. cms/decrypt-failed is
|
|
475
|
+
// deliberately oracle-free -- every secret-dependent failure collapses into
|
|
476
|
+
// it -- and that property is preserved by re-throwing it as this domain's
|
|
477
|
+
// own verdict with the cause chained, rather than adding any detail.
|
|
478
|
+
throw E("cmc/unverified-response",
|
|
479
|
+
"the AuthenticatedData response did not authenticate under the supplied recipient key", e);
|
|
480
|
+
}).then(function (res) {
|
|
481
|
+
if (!res || res.authenticated !== true) {
|
|
482
|
+
throw E("cmc/unverified-response", "the AuthenticatedData response did not authenticate under the supplied recipient key");
|
|
483
|
+
}
|
|
484
|
+
// The MAC covers BYTES; bind them to the ones that were interpreted.
|
|
485
|
+
// Verifying a MAC over content and then reporting a verdict read from
|
|
486
|
+
// somewhere else is the same defect as checking a signature over the
|
|
487
|
+
// wrong region -- authentic, and about a different message.
|
|
488
|
+
// Both are already Buffers -- pki.cms.decrypt returns one, and the parse
|
|
489
|
+
// surfaces one -- so they are compared directly rather than re-wrapped.
|
|
490
|
+
var authed = res.content;
|
|
491
|
+
var eContent = body.cms.encapContentInfo && body.cms.encapContentInfo.eContent;
|
|
492
|
+
if (!eContent || !Buffer.isBuffer(authed) || !guard.crypto.constantTimeEqual(authed, eContent)) {
|
|
493
|
+
throw E("cmc/unverified-response",
|
|
494
|
+
"the authenticated content is not the content this verdict was read from");
|
|
495
|
+
}
|
|
496
|
+
return true;
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
if (sent.allowUnverified === true) return false;
|
|
500
|
+
throw E("cmc/unverified-response",
|
|
501
|
+
"an AuthenticatedData response is authenticated by its MAC, which needs the recipient key this layer does not hold -- pass `recipient` with the key material (the shape pki.cms.decrypt takes), or `allowUnverified: true` to interpret it unauthenticated");
|
|
502
|
+
}
|
|
503
|
+
var signerInfos = (body.cms && body.cms.signerInfos) || [];
|
|
504
|
+
if (!signerInfos.length) {
|
|
505
|
+
throw E("cmc/unsigned-response",
|
|
506
|
+
"a Full PKI Response is a SignedData carrying at least one SignerInfo; this carrier has none, so there is no signature to verify (RFC 5272 sec. 4.2 / 3.2.1.3.4)");
|
|
507
|
+
}
|
|
508
|
+
// Same rule as the MAC arm above, applied where a signature is equally over
|
|
509
|
+
// bytes. The carrier's SHAPE is judged first -- an unsigned one is refused
|
|
510
|
+
// whatever form it arrived in -- and only the act of authenticating needs the
|
|
511
|
+
// bytes. A caller who has nothing but a parsed message can still interpret it
|
|
512
|
+
// through the named opt-out, which reports signatureVerified:false; what is
|
|
513
|
+
// refused is the combination that would claim a check nothing pinned.
|
|
514
|
+
if (!haveBytes) {
|
|
515
|
+
if (sent.allowUnverified === true) return false;
|
|
516
|
+
throw E("cmc/bad-input",
|
|
517
|
+
"verifying a CMC response signature needs the response as DER bytes or PEM, because the " +
|
|
518
|
+
"signature is over them -- pass the encoded response rather than a parsed one, or " +
|
|
519
|
+
"`allowUnverified: true` to interpret it unauthenticated");
|
|
520
|
+
}
|
|
521
|
+
// Verified over the SAME immutable snapshot the verdict was parsed from, not
|
|
522
|
+
// over the parse result: the two cannot describe different messages when both
|
|
523
|
+
// come from one private copy of the bytes.
|
|
524
|
+
//
|
|
525
|
+
// TRY first, and only report "cannot verify" when the signer certificate is
|
|
526
|
+
// genuinely nowhere to be found. A conforming SignedData carries its own signer
|
|
527
|
+
// certificate, which is the shape pki.cmc.build emits and the one a CA sends --
|
|
528
|
+
// demanding that the caller ALSO pass it would make the ordinary
|
|
529
|
+
// build-then-verify flow fail for a message that already contains everything
|
|
530
|
+
// needed. `certs` supplements the embedded bag; it was never meant to be the
|
|
531
|
+
// only source. What does not change is the posture: a signature that is present
|
|
532
|
+
// and does not verify is a refusal, and an unverifiable one still needs the
|
|
533
|
+
// named opt-out rather than passing quietly.
|
|
534
|
+
return cmsVerify.verify(responseBytes, sent.certs && sent.certs.length ? { certs: sent.certs } : {})
|
|
535
|
+
.then(function (res) {
|
|
536
|
+
if (res.valid) return true;
|
|
537
|
+
// EVERY failing signer, not just the first. A response may carry several,
|
|
538
|
+
// and the opt-out may only be honoured when none of them was actually
|
|
539
|
+
// checked and rejected: one signer whose certificate is missing must not
|
|
540
|
+
// let a DIFFERENT signer's failed signature through beside it.
|
|
541
|
+
var failed = (res.signers || []).filter(function (s) { return !s.ok; });
|
|
542
|
+
var code = failed.length ? failed[0].code : null;
|
|
543
|
+
var onlyMissing = failed.length > 0 && failed.every(function (s) {
|
|
544
|
+
return s.code === "cms/signer-cert-not-found";
|
|
545
|
+
});
|
|
546
|
+
// No signer certificate anywhere is "could not check", which the opt-out
|
|
547
|
+
// covers. A signature that failed against a certificate we DID find is a
|
|
548
|
+
// real failure, and no opt-out excuses it.
|
|
549
|
+
if (onlyMissing && sent.allowUnverified === true) return false;
|
|
550
|
+
throw E("cmc/unverified-response",
|
|
551
|
+
"the CMC response signature did not verify" + (code ? " (" + code + ")" : "") +
|
|
552
|
+
" -- pass `certs` with the responder's certificate if it is not carried in the message, or " +
|
|
553
|
+
"`allowUnverified: true` to accept an unauthenticated response deliberately");
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function _shape(body, sent, signatureVerified) {
|
|
559
|
+
var outcome = _reduceOutcome(body.statuses);
|
|
560
|
+
var governing = _governingStatus(body.statuses, outcome);
|
|
561
|
+
var anchors = _findControl(body.controls, OID_TRUSTED_ANCHORS, "Publish Trust Anchors");
|
|
562
|
+
var confirm = _findControl(body.controls, OID_CONFIRM_CERT_ACCEPTANCE, "Confirm Certificate Acceptance");
|
|
563
|
+
var serverNonce = _findControl(body.controls, OID_SENDER_NONCE, "Sender Nonce");
|
|
564
|
+
|
|
565
|
+
return {
|
|
566
|
+
outcome: outcome,
|
|
567
|
+
statuses: body.statuses,
|
|
568
|
+
// The detail belonging to the governing verdict, flattened for the common read.
|
|
569
|
+
failInfo: governing ? governing.failInfoName : null,
|
|
570
|
+
failInfoValue: governing ? governing.failInfo : null,
|
|
571
|
+
extendedFailInfo: governing ? governing.extendedFailInfo : null,
|
|
572
|
+
pendToken: governing && governing.pendInfo ? governing.pendInfo.pendToken : null,
|
|
573
|
+
pendTime: governing && governing.pendInfo ? governing.pendInfo.pendTime : null,
|
|
574
|
+
statusString: governing ? governing.statusString : null,
|
|
575
|
+
transactionId: sent.transactionId != null ? sent.transactionId : null,
|
|
576
|
+
// The responder's OWN nonce, retained by the caller for the next leg of the
|
|
577
|
+
// same transaction (RFC 5272 sec. 6.6).
|
|
578
|
+
senderNonce: serverNonce ? _octets(serverNonce, "Sender Nonce") : null,
|
|
579
|
+
// Surfaced, never acted on -- see the primitive's note on trust.
|
|
580
|
+
certificates: _bagOf(body.cms, "certs", "certificates"),
|
|
581
|
+
crls: _bagOf(body.cms, "crls", "crls"),
|
|
582
|
+
// The response's OWN two sequences, raw. For a request whose only arm was the
|
|
583
|
+
// other-message form there is no certificate to return, and RFC 5272 sec. 4.1
|
|
584
|
+
// puts the answer here instead -- so leaving them out would give such an
|
|
585
|
+
// exchange a successful verdict with its result unreachable, and send the
|
|
586
|
+
// caller back to the encapsulated bytes to re-parse what was already decoded.
|
|
587
|
+
cmsSequence: (body.cmsSequence || []).slice(),
|
|
588
|
+
otherMsgs: (body.otherMsgs || []).slice(),
|
|
589
|
+
publishTrustAnchors: anchors ? anchors.values.slice() : null,
|
|
590
|
+
confirmCertId: confirm ? _singleValue(confirm, "Confirm Certificate Acceptance") : null,
|
|
591
|
+
// Whether the CARRIER's signature was checked. False only via the explicit
|
|
592
|
+
// allowUnverified opt-out -- there is no path that leaves it false silently.
|
|
593
|
+
signatureVerified: signatureVerified,
|
|
594
|
+
// Whether anything in here was TRUSTED, which is never: the certificate bag
|
|
595
|
+
// and any Publish Trust Anchors control are the caller's to path-validate.
|
|
596
|
+
trusted: false,
|
|
597
|
+
controls: body.controls,
|
|
598
|
+
unhandled: body.unhandled,
|
|
599
|
+
cms: body.cms,
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* @primitive pki.cmc.build
|
|
605
|
+
* @signature pki.cmc.build(spec, signer, opts?) -> Promise<Buffer|string>
|
|
606
|
+
* @since 0.4.16
|
|
607
|
+
* @status experimental
|
|
608
|
+
* @spec RFC 5272, RFC 6402
|
|
609
|
+
* @defends enrollment-request-substitution (CWE-345)
|
|
610
|
+
* @related pki.cmc.verify, pki.schema.cmc.parse, pki.cms.sign
|
|
611
|
+
*
|
|
612
|
+
* Build and sign an RFC 5272 Full PKI Request. `spec.requests` is the list of certification
|
|
613
|
+
* requests, each naming exactly one arm -- `tcr` (a PKCS#10 CSR), `crm` (a CRMF CertReqMsg, or the
|
|
614
|
+
* CertReqMessages `pki.crmf.build` returns when it carries exactly one) or `orm`
|
|
615
|
+
* (`{ type, value }`). `spec.controls` are additional controls as `{ type, value }`, and `signer`
|
|
616
|
+
* is the `{ cert, key }` that signs the enclosing CMS SignedData.
|
|
617
|
+
*
|
|
618
|
+
* 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 REFUSED rather than renumbered --
|
|
620
|
+
* silently moving an identifier would break any control that already referenced it. For a `crm`
|
|
621
|
+
* arm the identity is the CertReqMsg's own `certReqId`, read back out of the supplied message.
|
|
622
|
+
*
|
|
623
|
+
* `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 -- "encoded
|
|
625
|
+
* 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, and -- per sec. 6.2.3 --
|
|
627
|
+
* derives the MAC key from `hash(secret || identity)` rather than `hash(secret)`: the two travel
|
|
628
|
+
* together because the control's presence is what changes the derivation. And
|
|
629
|
+
* `spec.popLink: { secret }` attaches a POP Link Witness V2 together with the POP Link Random
|
|
630
|
+
* 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)) -- asking for both is refused
|
|
632
|
+
* rather than silently dropped.
|
|
633
|
+
*
|
|
634
|
+
* `spec.transactionId` (number|bigint), `spec.senderNonce` and `spec.dataReturn` (bytes) attach the
|
|
635
|
+
* exchange-binding controls (RFC 5272 sec. 6.6 / 6.4) -- the same three `pki.cmc.verify` checks the
|
|
636
|
+
* response against. They are named fields rather than something to hand-encode into
|
|
637
|
+
* `spec.controls`, because a request that quietly omits them has no replay defence and neither end
|
|
638
|
+
* can tell: the verifier only enforces the halves the client says it sent. An unrecognized spec
|
|
639
|
+
* field is refused for the same reason -- a misspelling would otherwise build and sign a message
|
|
640
|
+
* that simply does not carry what was asked for.
|
|
641
|
+
*
|
|
642
|
+
* @opts
|
|
643
|
+
* - `pem` (boolean) -- return a PEM `CMS` block instead of DER.
|
|
644
|
+
* - `popLinkRandomBytes` (number) -- the length of R; default 64 (PL1 SHOULD: >= 512 bits).
|
|
645
|
+
* @example
|
|
646
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
647
|
+
* var key = await pki.key.export(pair.privateKey);
|
|
648
|
+
* var spki = await pki.key.export(pair.publicKey);
|
|
649
|
+
* var cert = await pki.x509.sign({ subject: "client", subjectPublicKey: spki,
|
|
650
|
+
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: key });
|
|
651
|
+
* var csr = await pki.csr.sign({ subject: "client", subjectPublicKey: spki }, { key: key });
|
|
652
|
+
* var req = await pki.cmc.build({ requests: [{ tcr: csr }] }, { cert: cert, key: key });
|
|
653
|
+
* pki.schema.cmc.parse(req).requests[0].arm; // "tcr"
|
|
654
|
+
*/
|
|
655
|
+
var build = require("./cmc-build").build;
|
|
656
|
+
|
|
657
|
+
module.exports = { verify: verify, build: build };
|