@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
package/lib/cmc-build.js
ADDED
|
@@ -0,0 +1,880 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
//
|
|
5
|
+
// @internal -- the pki.cmc.build implementation. So the pki.cmc namespace has ONE @module home,
|
|
6
|
+
// the operator-facing @module pki.cmc + the @primitive pki.cmc.build documentation block live in
|
|
7
|
+
// cmc-verify.js, which re-exports this build function.
|
|
8
|
+
//
|
|
9
|
+
/**
|
|
10
|
+
* RFC 5272 Full PKI Request production -- the producing half of `pki.cmc`.
|
|
11
|
+
*
|
|
12
|
+
* `build(spec, signer, opts)` assembles a PKIData (all four sequences, each
|
|
13
|
+
* possibly empty), allocates a unique body part identifier for every element,
|
|
14
|
+
* attaches controls through an OID-keyed encoder registry that enforces the RFC
|
|
15
|
+
* 6402 placement rules, computes the Identity Proof / POP Link witnesses over the
|
|
16
|
+
* bytes it is about to EMIT, and signs the result through `pki.cms.sign` with
|
|
17
|
+
* `eContentType: id-cct-PKIData`.
|
|
18
|
+
*
|
|
19
|
+
* The witness rule is the load-bearing one. RFC 5272 sec. 6.2.1 step 1 says the
|
|
20
|
+
* value to be validated is "The PKIData reqSequence field (encoded exactly as it
|
|
21
|
+
* appears in the Full PKI Request including the sequence type and length)". So
|
|
22
|
+
* the reqSequence TLV is built ONCE and both the message and the witness are
|
|
23
|
+
* derived from that single buffer -- computing the witness over a second
|
|
24
|
+
* serialization would agree with itself while disagreeing with the wire the
|
|
25
|
+
* moment any encoding detail differed.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
var nodeCrypto = require("node:crypto");
|
|
29
|
+
var asn1 = require("./asn1-der");
|
|
30
|
+
var csr = require("./schema-csr"); // to read the Subject Key Identifier a tcr request declares
|
|
31
|
+
var crmf = require("./schema-crmf"); // ... and the one a crm request declares
|
|
32
|
+
var cmcFmt = require("./schema-cmc"); // to read the assembled message back before signing it
|
|
33
|
+
var oid = require("./oid");
|
|
34
|
+
var cmsSign = require("./cms-sign");
|
|
35
|
+
var guard = require("./guard-all");
|
|
36
|
+
var frameworkError = require("./framework-error");
|
|
37
|
+
|
|
38
|
+
var CmcError = frameworkError.CmcError;
|
|
39
|
+
var b = asn1.build;
|
|
40
|
+
function E(code, message, cause) { return new CmcError(code, message, cause); }
|
|
41
|
+
function O(name) { return oid.byName(name); }
|
|
42
|
+
|
|
43
|
+
var OID_IDENTITY_PROOF_V2 = O("id-cmc-identityProofV2");
|
|
44
|
+
var OID_POP_LINK_RANDOM = O("id-cmc-popLinkRandom");
|
|
45
|
+
var OID_IDENTIFICATION = O("id-cmc-identification");
|
|
46
|
+
var OID_SHA256 = O("sha256");
|
|
47
|
+
var OID_HMAC_SHA256 = O("hmacWithSHA256");
|
|
48
|
+
|
|
49
|
+
// The identity-bearing controls a renewal must NOT carry: RFC 5272 sec. 3.2 (a)
|
|
50
|
+
// -- "The Identification and Identity Proof controls are absent."
|
|
51
|
+
//
|
|
52
|
+
// "Identity Proof" is BOTH versions: the original id-cmc-identityProof (sec.
|
|
53
|
+
// 6.2.2) and identityProofV2 (sec. 6.2.1). Listing only V2 would leave a renewal
|
|
54
|
+
// able to carry the v1 control, which is the same nonconformance the rule names.
|
|
55
|
+
var RENEWAL_FORBIDDEN = {};
|
|
56
|
+
RENEWAL_FORBIDDEN[OID_IDENTIFICATION] = "Identification";
|
|
57
|
+
RENEWAL_FORBIDDEN[O("id-cmc-identityProof")] = "Identity Proof";
|
|
58
|
+
RENEWAL_FORBIDDEN[OID_IDENTITY_PROOF_V2] = "Identity Proof V2";
|
|
59
|
+
|
|
60
|
+
// RFC 6402 sec. 2.6: responseBody appears ONLY in a PKIResponse. Enforced at
|
|
61
|
+
// BUILD time as well as at parse, so a caller cannot emit a message its own
|
|
62
|
+
// decoder would refuse.
|
|
63
|
+
var OID_RESPONSE_BODY = O("id-cmc-responseBody");
|
|
64
|
+
|
|
65
|
+
// RFC 5272 sec. 6.2.1 says "Implementations MUST be able to support tokens at
|
|
66
|
+
// least 16 characters long" -- a requirement on what an implementation must be
|
|
67
|
+
// ABLE to accept, not a floor every token has to clear. Reading it as a minimum
|
|
68
|
+
// inverts it, and would refuse a shorter secret a CA legitimately provisioned.
|
|
69
|
+
// The shared secret's strength is the deploying CA's policy; what this layer can
|
|
70
|
+
// say is that an empty string is not a credential at all.
|
|
71
|
+
// PL1: R "SHOULD be at least 512 bits in length".
|
|
72
|
+
var POP_LINK_RANDOM_BYTES = 64;
|
|
73
|
+
|
|
74
|
+
// The spec fields pki.cmc.build understands. Anything else is a caller mistake
|
|
75
|
+
// caught at the entry point rather than silently dropped from the message.
|
|
76
|
+
var KNOWN_SPEC_KEYS = {
|
|
77
|
+
requests: 1, controls: 1, cmsSequence: 1, otherMsgSequence: 1,
|
|
78
|
+
// NOT `identification`: the Identification control is attached through
|
|
79
|
+
// identityProof.identity, which is what emits it. Listing a key nothing reads
|
|
80
|
+
// would recreate the very hole this table closes -- accepted at the door and
|
|
81
|
+
// silently absent from the message.
|
|
82
|
+
identityProof: 1, popLink: 1, renewal: 1,
|
|
83
|
+
// The exchange binding (RFC 5272 sec. 6.6 / 6.4). First-class here because
|
|
84
|
+
// pki.cmc.verify names these same three when checking the response: a builder
|
|
85
|
+
// that made the caller hand-encode them while the verifier took them by name is
|
|
86
|
+
// the asymmetry that lets a request ship with no replay defence.
|
|
87
|
+
transactionId: 1, senderNonce: 1, dataReturn: 1,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
var OID_TRANSACTION_ID = O("id-cmc-transactionId");
|
|
91
|
+
var OID_SENDER_NONCE = O("id-cmc-senderNonce");
|
|
92
|
+
var OID_DATA_RETURN = O("id-cmc-dataReturn");
|
|
93
|
+
|
|
94
|
+
// AlgorithmIdentifier { OID } with no parameters -- the form the MAC / hash
|
|
95
|
+
// algorithm identifiers in these two controls take.
|
|
96
|
+
function algId(o) { return b.sequence([b.oid(o)]); }
|
|
97
|
+
|
|
98
|
+
// ---- body part identity ---------------------------------------------
|
|
99
|
+
|
|
100
|
+
// RFC 5272 sec. 3.2.2: identifiers are unique across the WHOLE message and 0 is
|
|
101
|
+
// reserved for the reference to the current PKIData. The allocator therefore
|
|
102
|
+
// starts at 1, and a caller-supplied value is validated rather than adjusted --
|
|
103
|
+
// silently renumbering would break any control that already references it.
|
|
104
|
+
function makeIdAllocator() {
|
|
105
|
+
var used = Object.create(null);
|
|
106
|
+
// Reserved ahead of allocation and not yet consumed by the element that owns
|
|
107
|
+
// it. Kept distinct from `used` so an element re-claiming its OWN reservation
|
|
108
|
+
// is a no-op, while a second element asking for the same value is still the
|
|
109
|
+
// duplicate the rule forbids.
|
|
110
|
+
var pending = Object.create(null);
|
|
111
|
+
var next = 1;
|
|
112
|
+
function assertValid(requested, what) {
|
|
113
|
+
if (typeof requested !== "number" || !Number.isInteger(requested) || requested < 0 || requested > 4294967295) {
|
|
114
|
+
throw E("cmc/bad-input", "a bodyPartID must be an integer in 0..4294967295, got " + requested + " (" + what + ")");
|
|
115
|
+
}
|
|
116
|
+
if (requested === 0) {
|
|
117
|
+
throw E("cmc/bad-input",
|
|
118
|
+
"bodyPartID 0 is reserved as the reference to the current PKIData and cannot identify " + what + " (RFC 5272 sec. 3.2.2)");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
reserve: function (requested, what) {
|
|
123
|
+
assertValid(requested, what);
|
|
124
|
+
if (used[requested]) {
|
|
125
|
+
throw E("cmc/bad-input",
|
|
126
|
+
"bodyPartID " + requested + " is used more than once; identifiers MUST be unique within a single PKIData (RFC 5272 sec. 3.2.2)");
|
|
127
|
+
}
|
|
128
|
+
used[requested] = true;
|
|
129
|
+
pending[requested] = true;
|
|
130
|
+
return requested;
|
|
131
|
+
},
|
|
132
|
+
claim: function (requested, what) {
|
|
133
|
+
if (requested == null) {
|
|
134
|
+
while (used[next]) next += 1;
|
|
135
|
+
used[next] = true;
|
|
136
|
+
return next;
|
|
137
|
+
}
|
|
138
|
+
assertValid(requested, what);
|
|
139
|
+
if (pending[requested]) { delete pending[requested]; return requested; } // consume our own reservation
|
|
140
|
+
if (used[requested]) {
|
|
141
|
+
throw E("cmc/bad-input",
|
|
142
|
+
"bodyPartID " + requested + " is used more than once; identifiers MUST be unique within a single PKIData (RFC 5272 sec. 3.2.2)");
|
|
143
|
+
}
|
|
144
|
+
used[requested] = true;
|
|
145
|
+
return requested;
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---- requests --------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
// TaggedRequest ::= CHOICE { tcr [0], crm [1], orm [2] }, IMPLICIT tags. For the
|
|
153
|
+
// crm arm the identity is the CertReqMsg's own certReqId (sec. 3.2.2), so it is
|
|
154
|
+
// read back out of the supplied message rather than allocated -- allocating one
|
|
155
|
+
// would put a second, contradictory identifier on the wire.
|
|
156
|
+
// The identifier a request FIXES, or null when it leaves the choice to us. Read
|
|
157
|
+
// in a pre-pass so every caller-determined value is reserved before a single
|
|
158
|
+
// generated one is handed out -- otherwise acceptance depends on the order the
|
|
159
|
+
// caller happened to write the list in, which is not a property of the message.
|
|
160
|
+
// The fields a request descriptor understands -- the same door KNOWN_SPEC_KEYS
|
|
161
|
+
// closes on the spec, on the objects nested inside it. A misspelled `bodyPartID`
|
|
162
|
+
// is not a harmless extra: the request would be auto-allocated a DIFFERENT
|
|
163
|
+
// identifier while a control the caller wrote still references the intended one,
|
|
164
|
+
// and the builder would sign a message whose halves point at different requests.
|
|
165
|
+
var KNOWN_REQUEST_KEYS = { tcr: 1, crm: 1, orm: 1, bodyPartID: 1 };
|
|
166
|
+
// The same door on the other descriptors nested in a spec. A control's identifier
|
|
167
|
+
// is always allocated, so a `bodyPartID` written on one is ignored outright; and a
|
|
168
|
+
// misspelled `identity` does not merely go missing -- the Identification control
|
|
169
|
+
// it would have emitted is what tells the server to derive the Identity Proof key
|
|
170
|
+
// from the secret AND the identity, so the message would carry a witness no
|
|
171
|
+
// conforming server can reproduce.
|
|
172
|
+
var KNOWN_CONTROL_KEYS = { type: 1, value: 1 };
|
|
173
|
+
var KNOWN_IDENTITY_PROOF_KEYS = { identity: 1, secret: 1 };
|
|
174
|
+
var KNOWN_POP_LINK_KEYS = { secret: 1 };
|
|
175
|
+
|
|
176
|
+
function fixedRequestId(req, index) {
|
|
177
|
+
if (!req || typeof req !== "object") throw E("cmc/bad-input", "each request must be an object");
|
|
178
|
+
guard.identifier.assertKnownKeys(req, KNOWN_REQUEST_KEYS, E, "cmc/bad-input", function (k) {
|
|
179
|
+
return "unknown field " + JSON.stringify(k) + " on request " + index +
|
|
180
|
+
" -- a request names one of tcr / crm / orm, and optionally bodyPartID";
|
|
181
|
+
});
|
|
182
|
+
var arms = ["tcr", "crm", "orm"].filter(function (k) { return req[k] != null; });
|
|
183
|
+
if (arms.length !== 1) {
|
|
184
|
+
throw E("cmc/bad-input",
|
|
185
|
+
"each request names exactly one of tcr / crm / orm, got " + (arms.length ? arms.join(" + ") : "none") +
|
|
186
|
+
" (request " + index + ")");
|
|
187
|
+
}
|
|
188
|
+
// A crm arm's identity is its CertReqMsg's own certReqId, so it is ALWAYS fixed
|
|
189
|
+
// -- there is nothing to allocate and nothing that may displace it. A caller who
|
|
190
|
+
// ALSO writes bodyPartID is stating an identity, and the only reason to state one
|
|
191
|
+
// is that something already references it; taking the certReqId silently would
|
|
192
|
+
// sign a message whose control points at no request in it. Equal is accepted --
|
|
193
|
+
// the caller is agreeing with the message, not overriding it.
|
|
194
|
+
if (req.crm != null) {
|
|
195
|
+
var crmId = _certReqIdOf(_asCertReqMsg(_der(req.crm, "a crm CertReqMsg")));
|
|
196
|
+
if (req.bodyPartID != null && _asBigInt(req.bodyPartID, "a crm request bodyPartID") !== BigInt(crmId)) {
|
|
197
|
+
throw E("cmc/bad-input",
|
|
198
|
+
"a crm request's bodyPartID is its CertReqMsg's own certReqId (" + crmId + "), which cannot be " +
|
|
199
|
+
"overridden; request " + index + " supplies " + req.bodyPartID);
|
|
200
|
+
}
|
|
201
|
+
return crmId;
|
|
202
|
+
}
|
|
203
|
+
return req.bodyPartID == null ? null : req.bodyPartID;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function encodeRequest(req, ids, index) {
|
|
207
|
+
if (req.tcr != null) {
|
|
208
|
+
// A tcr arm IS a PKCS#10 CertificationRequest, so it is parsed rather than
|
|
209
|
+
// taken on the tag: the readback of the assembled message checks the CMC
|
|
210
|
+
// structure around it, and would pass an empty SEQUENCE here as happily as a
|
|
211
|
+
// real request. Signing a request body that is not one produces an enrolment
|
|
212
|
+
// no CA can act on, and the producer is where that is cheap to catch.
|
|
213
|
+
try { csr.parse(_der(req.tcr, "a tcr certificationRequest")); }
|
|
214
|
+
catch (e) {
|
|
215
|
+
throw E("cmc/bad-input", "a tcr request must be a PKCS#10 CertificationRequest: " +
|
|
216
|
+
(e && e.message ? e.message : "it did not parse as one"), e);
|
|
217
|
+
}
|
|
218
|
+
var id = ids.claim(req.bodyPartID, "a tcr request");
|
|
219
|
+
return { bodyPartID: id, der: b.contextConstructed(0, Buffer.concat([b.integer(BigInt(id)), _der(req.tcr, "a tcr certificationRequest")])) };
|
|
220
|
+
}
|
|
221
|
+
if (req.crm != null) {
|
|
222
|
+
var msg = _asCertReqMsg(_der(req.crm, "a crm CertReqMsg"));
|
|
223
|
+
// Parsed as the CertReqMsg it is, the same way a tcr is parsed as a
|
|
224
|
+
// CertificationRequest: the readback checks the CMC structure around the
|
|
225
|
+
// request, and would pass a SEQUENCE that merely starts like one.
|
|
226
|
+
try { crmf.parse(b.sequence([msg])); }
|
|
227
|
+
catch (e) {
|
|
228
|
+
throw E("cmc/bad-input", "a crm request must be an RFC 4211 CertReqMsg: " +
|
|
229
|
+
(e && e.message ? e.message : "it did not parse as one"), e);
|
|
230
|
+
}
|
|
231
|
+
// CLAIM, not just read. The certReqId was reserved in the pre-pass with every
|
|
232
|
+
// other caller-determined identifier; leaving the reservation outstanding lets
|
|
233
|
+
// a later cmsSequence or otherMsg element asking for the same number be taken
|
|
234
|
+
// for its owner, and the message would then carry the identifier twice -- a
|
|
235
|
+
// PKIData this toolkit's own parser refuses. The tcr and orm arms already
|
|
236
|
+
// claim; this arm is the same rule.
|
|
237
|
+
var certReqId = ids.claim(_certReqIdOf(msg), "a crm request");
|
|
238
|
+
// IMPLICIT [1]: the tag REPLACES the CertReqMsg SEQUENCE tag, so the content
|
|
239
|
+
// is re-headered rather than nested.
|
|
240
|
+
var node = asn1.decode(msg);
|
|
241
|
+
var headerLen = node.header.end - node.header.start;
|
|
242
|
+
return { bodyPartID: certReqId, der: b.contextConstructed(1, node.bytes.subarray(headerLen)) };
|
|
243
|
+
}
|
|
244
|
+
var orm = req.orm;
|
|
245
|
+
if (!orm || typeof orm !== "object" || orm.type == null || orm.value == null) {
|
|
246
|
+
throw E("cmc/bad-input", "an orm request is { type, value } (request " + index + ")");
|
|
247
|
+
}
|
|
248
|
+
var ormId = ids.claim(req.bodyPartID, "an orm request");
|
|
249
|
+
return {
|
|
250
|
+
bodyPartID: ormId,
|
|
251
|
+
der: b.contextConstructed(2, Buffer.concat([
|
|
252
|
+
b.integer(BigInt(ormId)), b.oid(_oidOf(orm.type)), _der(orm.value, "an orm requestMessageValue")])),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// A TaggedRequest's crm arm carries ONE CertReqMsg, but `pki.crmf.build` returns
|
|
257
|
+
// a CertReqMessages (SEQUENCE SIZE(1..MAX) OF CertReqMsg) -- which is what a
|
|
258
|
+
// caller naturally has in hand. Both are accepted, told apart structurally:
|
|
259
|
+
//
|
|
260
|
+
// CertReqMsg ::= SEQUENCE { certReq CertRequest, ... } first child is a
|
|
261
|
+
// SEQUENCE whose OWN first child is an INTEGER (certReqId)
|
|
262
|
+
// CertReqMessages ::= SEQUENCE OF CertReqMsg first child is a
|
|
263
|
+
// CertReqMsg, whose own first child is a SEQUENCE
|
|
264
|
+
//
|
|
265
|
+
// A CertReqMessages carrying more than one message is REFUSED rather than having
|
|
266
|
+
// its first taken: each CertReqMsg is its own TaggedRequest with its own identity,
|
|
267
|
+
// so silently dropping the rest would emit a request the caller did not ask for.
|
|
268
|
+
function _asCertReqMsg(der) {
|
|
269
|
+
var node = asn1.decode(der);
|
|
270
|
+
var first = node.children && node.children[0];
|
|
271
|
+
if (!first || !first.children || !first.children.length) {
|
|
272
|
+
throw E("cmc/bad-input", "a crm arm must be a CertReqMsg or a CertReqMessages carrying one");
|
|
273
|
+
}
|
|
274
|
+
var grand = first.children[0];
|
|
275
|
+
if (grand.tagClass === "universal" && grand.tagNumber === asn1.TAGS.INTEGER) return der; // already a CertReqMsg
|
|
276
|
+
if (node.children.length !== 1) {
|
|
277
|
+
throw E("cmc/bad-input",
|
|
278
|
+
"a crm arm carries exactly one CertReqMsg; this CertReqMessages holds " + node.children.length +
|
|
279
|
+
" -- pass each as its own request so each keeps its own certReqId");
|
|
280
|
+
}
|
|
281
|
+
return first.bytes;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// A CertReqMsg's identity: CertReqMsg ::= SEQUENCE { certReq CertRequest, ... },
|
|
285
|
+
// CertRequest ::= SEQUENCE { certReqId INTEGER, ... }.
|
|
286
|
+
function _certReqIdOf(msg) {
|
|
287
|
+
var node = asn1.decode(msg);
|
|
288
|
+
var certReq = node.children && node.children[0];
|
|
289
|
+
if (!certReq || !certReq.children || !certReq.children.length) {
|
|
290
|
+
throw E("cmc/bad-input", "a crm CertReqMsg must lead with a CertRequest whose first element is certReqId");
|
|
291
|
+
}
|
|
292
|
+
var v = asn1.read.integer(certReq.children[0]);
|
|
293
|
+
return guard.range.int(v, 0n, 4294967295n, E, "cmc/bad-input", "a crm certReqId");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// A pre-encoded TaggedContentInfo / OtherMsg supplied by the caller. Only its
|
|
297
|
+
// leading bodyPartID is read -- the payload is the caller's and stays untouched --
|
|
298
|
+
// so the identifier joins the same allocation space every other element draws
|
|
299
|
+
// from and a collision is refused here rather than discovered by the parser.
|
|
300
|
+
function _claimRawElement(el, ids, what) {
|
|
301
|
+
var der = _der(el, what);
|
|
302
|
+
var node = asn1.decode(der);
|
|
303
|
+
if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children || !node.children.length) {
|
|
304
|
+
throw E("cmc/bad-input", what + " must be a SEQUENCE leading with its bodyPartID");
|
|
305
|
+
}
|
|
306
|
+
var v = asn1.read.integer(node.children[0]);
|
|
307
|
+
var id = guard.range.int(v, 0n, 4294967295n, E, "cmc/bad-input", what + " bodyPartID");
|
|
308
|
+
ids.claim(id, what);
|
|
309
|
+
return der;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// The binding controls have a named spec field AND can be hand-encoded into
|
|
313
|
+
// spec.controls. Emitting both would put two of the same control in one message,
|
|
314
|
+
// and duplicates of these three are what decide what the response must echo -- two
|
|
315
|
+
// values means no value. Also refuses two hand-encoded copies, since the same
|
|
316
|
+
// ambiguity arrives that way.
|
|
317
|
+
var BINDING_BY_OID = {};
|
|
318
|
+
BINDING_BY_OID[O("id-cmc-transactionId")] = "transactionId";
|
|
319
|
+
BINDING_BY_OID[O("id-cmc-senderNonce")] = "senderNonce";
|
|
320
|
+
BINDING_BY_OID[O("id-cmc-dataReturn")] = "dataReturn";
|
|
321
|
+
|
|
322
|
+
// ... and the type each one carries. The CMC parser keeps every control value RAW,
|
|
323
|
+
// so nothing downstream of the builder objects to a Transaction Identifier encoded
|
|
324
|
+
// as an OCTET STRING -- but these three are read, not carried: pki.cmc.verify
|
|
325
|
+
// compares them against the response, and pki.est.fullcmc reads them back out of
|
|
326
|
+
// the request before it goes out. A hand-encoded value of the wrong type therefore
|
|
327
|
+
// signs a request that this toolkit's own client will refuse to send. The type is
|
|
328
|
+
// checked by READING the value, so a wrong tag and a malformed encoding of the
|
|
329
|
+
// right one are refused alike.
|
|
330
|
+
var BINDING_READER = {
|
|
331
|
+
transactionId: function (node) { return asn1.read.integer(node); },
|
|
332
|
+
senderNonce: function (node) { return asn1.read.octetString(node); },
|
|
333
|
+
dataReturn: function (node) { return asn1.read.octetString(node); },
|
|
334
|
+
};
|
|
335
|
+
var BINDING_TYPE_NAME = {
|
|
336
|
+
transactionId: "an INTEGER (RFC 5272 sec. 6.6)",
|
|
337
|
+
senderNonce: "an OCTET STRING (RFC 5272 sec. 6.6)",
|
|
338
|
+
dataReturn: "an OCTET STRING (RFC 5272 sec. 6.4)",
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
function _assertNoDuplicateBinding(callerControls, spec) {
|
|
342
|
+
var seen = {};
|
|
343
|
+
callerControls.forEach(function (c) {
|
|
344
|
+
var field = BINDING_BY_OID[_oidOf(c.type)];
|
|
345
|
+
if (!field) return;
|
|
346
|
+
try { BINDING_READER[field](asn1.decode(_der(c.value, "a control value"))); }
|
|
347
|
+
catch (e) {
|
|
348
|
+
throw E("cmc/bad-input",
|
|
349
|
+
"a hand-encoded " + field + " control must carry " + BINDING_TYPE_NAME[field] +
|
|
350
|
+
"; this one cannot be read as that, so the request would be signed with a binding value " +
|
|
351
|
+
"nothing can compare a response against", e);
|
|
352
|
+
}
|
|
353
|
+
if (seen[field] || spec[field] != null) {
|
|
354
|
+
throw E("cmc/bad-input",
|
|
355
|
+
"the " + field + " control would be emitted twice (spec." + field + " and a control in " +
|
|
356
|
+
"spec.controls, or two in spec.controls); a message carrying two leaves no single value the " +
|
|
357
|
+
"response can be bound to (RFC 5272 sec. 6.6 / 6.4)");
|
|
358
|
+
}
|
|
359
|
+
seen[field] = true;
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// A Transaction Identifier is an unbounded INTEGER on the wire; the shared authoring
|
|
364
|
+
// guard normalizes the number-or-bigint a caller naturally has.
|
|
365
|
+
function _asBigInt(v, what) { return guard.range.authoredInteger(v, E, "cmc/bad-input", what); }
|
|
366
|
+
|
|
367
|
+
function _der(v, what) {
|
|
368
|
+
if (Buffer.isBuffer(v)) return v;
|
|
369
|
+
if (v instanceof Uint8Array) return Buffer.from(v);
|
|
370
|
+
throw E("cmc/bad-input", what + " must be DER bytes");
|
|
371
|
+
}
|
|
372
|
+
function _oidOf(v) {
|
|
373
|
+
if (typeof v !== "string") throw E("cmc/bad-input", "an OID must be a name or a dotted string");
|
|
374
|
+
return /^\d+(\.\d+)+$/.test(v) ? v : O(v);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ---- controls --------------------------------------------------------
|
|
378
|
+
|
|
379
|
+
// TaggedAttribute ::= SEQUENCE { bodyPartID, attrType, attrValues SET OF ANY }.
|
|
380
|
+
// Controls this builder will not put in a Full PKI Request. A row per control with
|
|
381
|
+
// the reason it does not belong in this direction -- id-cmc-responseBody by an
|
|
382
|
+
// explicit RFC 6402 sec. 2.6 MUST NOT, the two status controls because RFC 5272
|
|
383
|
+
// sec. 6.1 makes them what a SERVER emits and a client processes in a PKI Response:
|
|
384
|
+
// a request has no verdict of its own to report, so one written here is a fabricated
|
|
385
|
+
// answer to a question nobody asked.
|
|
386
|
+
//
|
|
387
|
+
// Only the BUILDER refuses these. pki.schema.cmc keeps decoding them wherever they
|
|
388
|
+
// appear, because the spec states a placement MUST for id-cmc-responseBody alone --
|
|
389
|
+
// refusing the rest on the wire would reject a message the ASN.1 permits, and this
|
|
390
|
+
// decoder's job is to surface what arrived, not to judge who should have sent it.
|
|
391
|
+
var REQUEST_FORBIDDEN_CONTROL = {};
|
|
392
|
+
REQUEST_FORBIDDEN_CONTROL[OID_RESPONSE_BODY] =
|
|
393
|
+
"id-cmc-responseBody may appear only in the control sequence of a PKIResponse (RFC 6402 sec. 2.6)";
|
|
394
|
+
REQUEST_FORBIDDEN_CONTROL[O("id-cmc-statusInfo")] =
|
|
395
|
+
"a CMC Status Info control reports a server's verdict on a request, so it belongs in a PKI Response, " +
|
|
396
|
+
"not in the request itself (RFC 5272 sec. 6.1)";
|
|
397
|
+
REQUEST_FORBIDDEN_CONTROL[O("id-cmc-statusInfoV2")] =
|
|
398
|
+
"an Extended CMC Status Info control reports a server's verdict on a request, so it belongs in a PKI " +
|
|
399
|
+
"Response, not in the request itself (RFC 5272 sec. 6.1)";
|
|
400
|
+
|
|
401
|
+
function encodeControl(attrType, values, ids) {
|
|
402
|
+
if (REQUEST_FORBIDDEN_CONTROL[attrType]) {
|
|
403
|
+
throw E("cmc/control-misplaced", REQUEST_FORBIDDEN_CONTROL[attrType]);
|
|
404
|
+
}
|
|
405
|
+
var id = ids.claim(null, "a control");
|
|
406
|
+
return b.sequence([b.integer(BigInt(id)), b.oid(attrType), b.set(values)]);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ---- the witnesses ---------------------------------------------------
|
|
410
|
+
|
|
411
|
+
function _assertSecret(secret, what) {
|
|
412
|
+
if (typeof secret !== "string" || secret.length === 0) {
|
|
413
|
+
throw E("cmc/bad-input",
|
|
414
|
+
what + " requires the shared secret as a non-empty string; got " +
|
|
415
|
+
(typeof secret === "string" ? "an empty string" : typeof secret));
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// IdentifyProofV2 ::= SEQUENCE { hashAlgID, macAlgID, witness OCTET STRING }.
|
|
420
|
+
// key = hash(shared-secret as a UTF8 string); witness = MAC(reqSequenceBytes, key).
|
|
421
|
+
// `reqSequenceBytes` is the buffer that will BE the message's reqSequence -- not
|
|
422
|
+
// a re-encode of it (RFC 5272 sec. 6.2.1 step 1).
|
|
423
|
+
// Tracks a buffer this module allocated that carries secret material, so the caller clears it
|
|
424
|
+
// on every exit. The shared secret ARRIVES as a JS string, which is immutable and cannot be
|
|
425
|
+
// cleared; what this closes is every copy the toolkit itself makes of it.
|
|
426
|
+
function _ownSecret(buf, owned) {
|
|
427
|
+
owned.push(buf);
|
|
428
|
+
return buf;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function identityProofV2(secret, reqSequenceBytes, identity) {
|
|
432
|
+
_assertSecret(secret, "an Identity Proof V2 control");
|
|
433
|
+
// RFC 5272 sec. 6.2.3: the Identification control is OPTIONAL ("servers MAY
|
|
434
|
+
// require" it), but when it IS present the key derivation is ALTERED -- "the
|
|
435
|
+
// hash of the concatenation of the shared-secret and the UTF8 identity value
|
|
436
|
+
// (without the type and length bytes) are hashed rather than just the
|
|
437
|
+
// shared-secret". Same controls on the wire, different key: a producer that
|
|
438
|
+
// emits the Identification control while hashing the secret alone computes a
|
|
439
|
+
// witness every conforming server rejects.
|
|
440
|
+
// EVERY allocation that carries the shared secret is cleared, not only the derived key.
|
|
441
|
+
// The derivation input holds the secret in the clear, and with an identity present the
|
|
442
|
+
// concatenation leaves two more copies of it behind -- the operand and the joined buffer.
|
|
443
|
+
// Wiping the key while those survive clears the cheapest copy and keeps the rest. The
|
|
444
|
+
// derivation runs INSIDE the try so a throw partway still reaches the clear.
|
|
445
|
+
var owned = [];
|
|
446
|
+
var key = null;
|
|
447
|
+
try {
|
|
448
|
+
var material;
|
|
449
|
+
if (identity == null) {
|
|
450
|
+
material = _ownSecret(Buffer.from(secret, "utf8"), owned);
|
|
451
|
+
} else {
|
|
452
|
+
material = _ownSecret(Buffer.concat([
|
|
453
|
+
_ownSecret(Buffer.from(secret, "utf8"), owned),
|
|
454
|
+
_ownSecret(Buffer.from(identity, "utf8"), owned),
|
|
455
|
+
]), owned);
|
|
456
|
+
}
|
|
457
|
+
key = nodeCrypto.createHash("sha256").update(material).digest();
|
|
458
|
+
var witness = nodeCrypto.createHmac("sha256", key).update(reqSequenceBytes).digest();
|
|
459
|
+
return b.sequence([algId(OID_SHA256), algId(OID_HMAC_SHA256), b.octetString(witness)]);
|
|
460
|
+
} finally {
|
|
461
|
+
if (key) guard.secret.zeroize(key, CmcError, "cmc/bad-input", "the Identity Proof MAC key");
|
|
462
|
+
guard.secret.zeroizeAll(owned, CmcError, "cmc/bad-input", "the Identity Proof derivation input");
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// PopLinkWitnessV2 ::= SEQUENCE { keyGenAlgorithm, macAlgorithm, witness }.
|
|
467
|
+
// key = keyGen(shared-secret); witness = MAC(R, key), with R carried alongside in
|
|
468
|
+
// the POP Link Random control -- PL1 makes that control mandatory in the same
|
|
469
|
+
// request, which is why the two are produced together and never separately.
|
|
470
|
+
function popLinkWitnessV2(secret, R) {
|
|
471
|
+
_assertSecret(secret, "a POP Link Witness V2 control");
|
|
472
|
+
var key = nodeCrypto.createHash("sha256").update(secret, "utf8").digest();
|
|
473
|
+
var witness = nodeCrypto.createHmac("sha256", key).update(R).digest();
|
|
474
|
+
try {
|
|
475
|
+
return b.sequence([algId(OID_SHA256), algId(OID_HMAC_SHA256), b.octetString(witness)]);
|
|
476
|
+
} finally {
|
|
477
|
+
guard.secret.zeroize(key, CmcError, "cmc/bad-input", "the POP Link MAC key");
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// The operator-facing @primitive block for this function lives beside its
|
|
482
|
+
// re-export in cmc-verify.js, the pki.cmc @module home.
|
|
483
|
+
function build(spec, signer, opts) {
|
|
484
|
+
// Assembled SYNCHRONOUSLY. _build reads the spec, every request buffer and the
|
|
485
|
+
// signer as it goes, so deferring that work would read them a turn after the
|
|
486
|
+
// call -- and a caller reusing a pooled CSR buffer, or reaching back into the
|
|
487
|
+
// spec on the next line, would have the message signed over something other
|
|
488
|
+
// than what was handed in. Only the signing itself is async, and by then the
|
|
489
|
+
// bytes are fixed.
|
|
490
|
+
try {
|
|
491
|
+
return _build(spec, signer, opts);
|
|
492
|
+
} catch (e) {
|
|
493
|
+
return Promise.reject(e); // the surface stays promise-rejecting, never throwing
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function _build(spec, signer, opts) {
|
|
498
|
+
opts = opts || {};
|
|
499
|
+
if (!spec || typeof spec !== "object" || Array.isArray(spec) || Buffer.isBuffer(spec)) {
|
|
500
|
+
throw E("cmc/bad-input", "the CMC request spec must be an object");
|
|
501
|
+
}
|
|
502
|
+
if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw E("cmc/bad-input", "pki.cmc.build options must be an object");
|
|
503
|
+
// A misspelled or unsupported spec field fails OPEN in the quietest way there is:
|
|
504
|
+
// the message builds, is signed, and simply does not carry what was asked for.
|
|
505
|
+
// That is worst for the exchange-binding fields below -- a request built without
|
|
506
|
+
// them has no replay defence, and pki.cmc.verify cannot enforce a binding the
|
|
507
|
+
// client never sent, so the omission is invisible from both ends.
|
|
508
|
+
guard.identifier.assertKnownKeys(spec, KNOWN_SPEC_KEYS, E, "cmc/bad-input", "unknown spec field ");
|
|
509
|
+
var requests = spec.requests || [];
|
|
510
|
+
if (!Array.isArray(requests)) throw E("cmc/bad-input", "spec.requests must be an array");
|
|
511
|
+
var callerControls = spec.controls || [];
|
|
512
|
+
if (!Array.isArray(callerControls)) throw E("cmc/bad-input", "spec.controls must be an array");
|
|
513
|
+
|
|
514
|
+
var ids = makeIdAllocator();
|
|
515
|
+
|
|
516
|
+
// EVERY identifier the CALLER determines is reserved before a single generated
|
|
517
|
+
// one is handed out. There are four sources, and reserving only some of them
|
|
518
|
+
// makes acceptance depend on the order the caller wrote things in rather than
|
|
519
|
+
// on the message:
|
|
520
|
+
// 1. a request's explicit bodyPartID
|
|
521
|
+
// 2. a crm arm's certReqId, which is fixed by the CertReqMsg itself
|
|
522
|
+
// 3. a cmsSequence element's bodyPartID
|
|
523
|
+
// 4. an otherMsgSequence element's bodyPartID
|
|
524
|
+
// Auto-allocation walks upward from 1, so anything minted before these were
|
|
525
|
+
// claimed could take a value the caller already spent -- and the caller's own
|
|
526
|
+
// element would then be rejected as a duplicate of whatever displaced it.
|
|
527
|
+
requests.forEach(function (r, i) {
|
|
528
|
+
var fixed = fixedRequestId(r, i);
|
|
529
|
+
if (fixed != null) ids.reserve(fixed, "request " + i);
|
|
530
|
+
});
|
|
531
|
+
var cmsSequence = (spec.cmsSequence || []).map(function (el, i) {
|
|
532
|
+
return _claimRawElement(el, ids, "a cmsSequence element (index " + i + ")");
|
|
533
|
+
});
|
|
534
|
+
var otherMsgSequence = (spec.otherMsgSequence || []).map(function (el, i) {
|
|
535
|
+
return _claimRawElement(el, ids, "an otherMsgSequence element (index " + i + ")");
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// Now the requests: a fixed identifier is re-claimed as a no-op against the
|
|
539
|
+
// reservation above, and only the ones that omitted it are allocated. The
|
|
540
|
+
// reqSequence bytes this produces are what the Identity Proof witness covers.
|
|
541
|
+
var encodedRequests = requests.map(function (r, i) { return encodeRequest(r, ids, i); });
|
|
542
|
+
var reqSequenceBytes = b.sequence(encodedRequests.map(function (r) { return r.der; }));
|
|
543
|
+
|
|
544
|
+
// A renewal carries neither identity control. Refusing the combination is the
|
|
545
|
+
// point: dropping the control silently would emit a message that looks like a
|
|
546
|
+
// renewal to us and an identity-proofed request to nobody.
|
|
547
|
+
if (spec.renewal) {
|
|
548
|
+
if (spec.identityProof) {
|
|
549
|
+
throw E("cmc/bad-input",
|
|
550
|
+
"a renewal request carries no Identity Proof control -- the Identification and Identity Proof controls are absent (RFC 5272 sec. 3.2 (a))");
|
|
551
|
+
}
|
|
552
|
+
for (var ci = 0; ci < callerControls.length; ci++) {
|
|
553
|
+
var forbidden = RENEWAL_FORBIDDEN[_oidOf(callerControls[ci].type)];
|
|
554
|
+
if (forbidden) {
|
|
555
|
+
throw E("cmc/bad-input",
|
|
556
|
+
"a renewal request carries no " + forbidden + " control (RFC 5272 sec. 3.2 (a))");
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
// A renewal is authenticated by the certificate being renewed: sec. 6.3.3 says
|
|
560
|
+
// "the outermost signature layer is created using the current signing
|
|
561
|
+
// certificate, which allows the original identity to be associated with the
|
|
562
|
+
// certification request". That identity is the whole mechanism -- it is what
|
|
563
|
+
// replaces the Identity Proof this mode just refused. A key-only signer has no
|
|
564
|
+
// certificate and so carries no prior identity, leaving a message with nothing
|
|
565
|
+
// for the CA to authenticate the renewal against.
|
|
566
|
+
var renewalSigners = Array.isArray(signer) ? signer : [signer];
|
|
567
|
+
for (var si = 0; si < renewalSigners.length; si++) {
|
|
568
|
+
var rs = renewalSigners[si];
|
|
569
|
+
if (rs && rs.cert == null && rs.spki != null) {
|
|
570
|
+
throw E("cmc/bad-signer",
|
|
571
|
+
"a renewal request is signed with the certificate being renewed, which is what associates the " +
|
|
572
|
+
"original identity with it (RFC 5272 sec. 6.3.3); a key-only signer carries no such identity");
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
var controls = [];
|
|
578
|
+
callerControls.forEach(function (c) {
|
|
579
|
+
if (!c || typeof c !== "object" || c.type == null || c.value == null) {
|
|
580
|
+
throw E("cmc/bad-input", "each control is { type, value }");
|
|
581
|
+
}
|
|
582
|
+
guard.identifier.assertKnownKeys(c, KNOWN_CONTROL_KEYS, E, "cmc/bad-input", function (k) {
|
|
583
|
+
return "unknown field " + JSON.stringify(k) + " on a control -- a control is { type, value }, " +
|
|
584
|
+
"and its bodyPartID is allocated by the builder";
|
|
585
|
+
});
|
|
586
|
+
controls.push(encodeControl(_oidOf(c.type), [_der(c.value, "a control value")], ids));
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
// The exchange binding (RFC 5272 sec. 6.6 Transaction Identifier / Sender Nonce,
|
|
590
|
+
// sec. 6.4 Data Return). These are what pki.cmc.verify checks the response
|
|
591
|
+
// against, and a request that omits them has no replay defence -- so they are
|
|
592
|
+
// named fields here rather than something the caller hand-encodes into
|
|
593
|
+
// spec.controls and can silently get wrong.
|
|
594
|
+
// A named field and a hand-encoded control of the same type would emit the
|
|
595
|
+
// control TWICE, and a message carrying two of them has no single value the
|
|
596
|
+
// response can be bound to -- this toolkit's own /fullcmc refuses exactly that.
|
|
597
|
+
// Refuse it at the source rather than sign something no one can bind to.
|
|
598
|
+
_assertNoDuplicateBinding(callerControls, spec);
|
|
599
|
+
if (spec.transactionId != null) {
|
|
600
|
+
controls.push(encodeControl(OID_TRANSACTION_ID,
|
|
601
|
+
[b.integer(_asBigInt(spec.transactionId, "spec.transactionId"))], ids));
|
|
602
|
+
}
|
|
603
|
+
if (spec.senderNonce != null) {
|
|
604
|
+
controls.push(encodeControl(OID_SENDER_NONCE,
|
|
605
|
+
[b.octetString(_der(spec.senderNonce, "spec.senderNonce"))], ids));
|
|
606
|
+
}
|
|
607
|
+
if (spec.dataReturn != null) {
|
|
608
|
+
controls.push(encodeControl(OID_DATA_RETURN,
|
|
609
|
+
[b.octetString(_der(spec.dataReturn, "spec.dataReturn"))], ids));
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (spec.identityProof) {
|
|
613
|
+
guard.identifier.assertKnownKeys(spec.identityProof, KNOWN_IDENTITY_PROOF_KEYS, E, "cmc/bad-input",
|
|
614
|
+
function (k) { return "unknown field " + JSON.stringify(k) + " on identityProof -- it is { secret, identity? }"; });
|
|
615
|
+
var identity = spec.identityProof.identity;
|
|
616
|
+
if (identity != null && typeof identity !== "string") {
|
|
617
|
+
throw E("cmc/bad-input", "identityProof.identity is the UTF8String the Identification control carries");
|
|
618
|
+
}
|
|
619
|
+
// The pair is emitted together when an identity is given, because the two are
|
|
620
|
+
// coupled: the Identification control tells the server WHICH shared secret to
|
|
621
|
+
// look up, and its presence is what changes how the key is derived. Letting a
|
|
622
|
+
// caller supply one without the other would put that coupling out of reach.
|
|
623
|
+
if (identity != null) controls.push(encodeControl(OID_IDENTIFICATION, [b.utf8(identity)], ids));
|
|
624
|
+
controls.push(encodeControl(OID_IDENTITY_PROOF_V2,
|
|
625
|
+
[identityProofV2(spec.identityProof.secret, reqSequenceBytes, identity)], ids));
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (spec.popLink) {
|
|
629
|
+
guard.identifier.assertKnownKeys(spec.popLink, KNOWN_POP_LINK_KEYS, E, "cmc/bad-input",
|
|
630
|
+
function (k) { return "unknown field " + JSON.stringify(k) + " on popLink -- it is { secret }"; });
|
|
631
|
+
var rBytes = opts.popLinkRandomBytes == null ? POP_LINK_RANDOM_BYTES : opts.popLinkRandomBytes;
|
|
632
|
+
if (typeof rBytes !== "number" || !Number.isInteger(rBytes) || rBytes < 1) {
|
|
633
|
+
throw E("cmc/bad-input", "popLinkRandomBytes must be a positive integer");
|
|
634
|
+
}
|
|
635
|
+
var R = nodeCrypto.randomBytes(rBytes);
|
|
636
|
+
// Both are emitted together: PL1 makes the Random control mandatory whenever
|
|
637
|
+
// the witness is present, so producing one without the other is not an option
|
|
638
|
+
// the API offers.
|
|
639
|
+
controls.push(encodeControl(OID_POP_LINK_RANDOM, [b.octetString(R)], ids));
|
|
640
|
+
controls.push(encodeControl(O("id-cmc-popLinkWitnessV2"),
|
|
641
|
+
[popLinkWitnessV2(spec.popLink.secret, R)], ids));
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// PKIData ::= SEQUENCE { controlSequence, reqSequence, cmsSequence,
|
|
645
|
+
// otherMsgSequence } -- all four emitted, the unused ones empty (sec. 3.2.1).
|
|
646
|
+
// reqSequenceBytes is spliced in as the SAME buffer the witness was computed
|
|
647
|
+
// over, which is the whole of IP1.
|
|
648
|
+
var pkiData = b.sequence([
|
|
649
|
+
b.sequence(controls),
|
|
650
|
+
b.raw(reqSequenceBytes),
|
|
651
|
+
b.sequence(cmsSequence),
|
|
652
|
+
b.sequence(otherMsgSequence),
|
|
653
|
+
]);
|
|
654
|
+
|
|
655
|
+
// Read the assembled message back through the shipped parser before signing it.
|
|
656
|
+
// Every arm here splices CALLER-supplied DER -- a tcr's CertificationRequest, a
|
|
657
|
+
// cmsSequence TaggedContentInfo, an otherMsg's value -- and checking each shape
|
|
658
|
+
// by hand would restate the parser's rules in a second place, where they would
|
|
659
|
+
// drift and where a newly added arm would silently miss them. One round-trip
|
|
660
|
+
// covers them all, including arms not yet written: whatever the parser refuses,
|
|
661
|
+
// this refuses at build time rather than emitting a message whose recipient --
|
|
662
|
+
// this toolkit's own decoder included -- cannot read it.
|
|
663
|
+
try {
|
|
664
|
+
cmcFmt.parsePkiData(pkiData);
|
|
665
|
+
} catch (e) {
|
|
666
|
+
throw E("cmc/bad-input",
|
|
667
|
+
"the assembled PKIData does not parse as one, so it would be refused by its recipient: " +
|
|
668
|
+
(e && e.message ? e.message : "malformed"), e);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
_assertKeyOnlySigner(signer, requests);
|
|
672
|
+
|
|
673
|
+
// The signer is copied for the same reason the message was assembled at the
|
|
674
|
+
// call: cms.sign reads `key` inside its own promise chain, so a caller who
|
|
675
|
+
// swaps signer.key on the next line would have the request signed by the
|
|
676
|
+
// replacement while the original certificate stays embedded -- a message whose
|
|
677
|
+
// signature does not belong to the certificate beside it.
|
|
678
|
+
//
|
|
679
|
+
// cms.sign resolves eContentType through oid.byName, so it takes the registry
|
|
680
|
+
// NAME; handing it the dotted value resolves to undefined.
|
|
681
|
+
// Copying a private key makes a SECOND copy of a secret, so it is cleared once signing has
|
|
682
|
+
// settled rather than left for the collector -- the same discipline the MAC keys above follow.
|
|
683
|
+
// The clear covers the rejecting path too: a wrong or malformed key is the case a caller can
|
|
684
|
+
// reach, and a success-only wipe would keep the secret exactly when it matters.
|
|
685
|
+
var ownedKeyBytes = [];
|
|
686
|
+
var copiedSigner = _copySigners(signer, ownedKeyBytes);
|
|
687
|
+
function _wipeOwnedKeys() {
|
|
688
|
+
if (ownedKeyBytes.length) guard.secret.zeroizeAll(ownedKeyBytes, CmcError, "cmc/bad-input", "the signer key copy");
|
|
689
|
+
ownedKeyBytes.length = 0;
|
|
690
|
+
}
|
|
691
|
+
var pending;
|
|
692
|
+
try {
|
|
693
|
+
pending = cmsSign.sign(pkiData, copiedSigner, { eContentType: "id-cct-PKIData", pem: opts.pem });
|
|
694
|
+
} catch (e) { _wipeOwnedKeys(); throw e; }
|
|
695
|
+
return pending.then(function (out) { _wipeOwnedKeys(); return out; },
|
|
696
|
+
function (e) { _wipeOwnedKeys(); throw e; });
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Each signer descriptor, and every byte value in it. cms.sign reads these inside
|
|
700
|
+
// its own promise chain, so both levels matter: re-pointing signer.key is one way
|
|
701
|
+
// to change who signs, and rewriting the PKCS#8 buffer it already points at is
|
|
702
|
+
// the other. A CryptoKey handle is passed through as-is -- it is an opaque
|
|
703
|
+
// reference the caller is meant to share, and there is nothing to copy.
|
|
704
|
+
// `owned` collects the buffers THIS function allocated that hold PRIVATE KEY material, so the
|
|
705
|
+
// caller can clear them once signing has settled. Only the key is listed: the other copied
|
|
706
|
+
// values are certificates and identifiers, which are public and outlive the call by design.
|
|
707
|
+
// The caller's own key is never written to -- only our copy of it.
|
|
708
|
+
function _copySigners(signer, owned) {
|
|
709
|
+
function one(s) {
|
|
710
|
+
if (!s || typeof s !== "object") return s;
|
|
711
|
+
var out = {}, k;
|
|
712
|
+
for (k in s) {
|
|
713
|
+
if (!Object.prototype.hasOwnProperty.call(s, k)) continue;
|
|
714
|
+
out[k] = copyValue(s[k], k === "key");
|
|
715
|
+
}
|
|
716
|
+
return out;
|
|
717
|
+
}
|
|
718
|
+
// One level further for a COMPOSITE key, which is an object of component keys
|
|
719
|
+
// ({ mldsa, trad }) rather than a buffer. Copying only the top level would leave
|
|
720
|
+
// those components -- the actual PKCS#8 bytes that sign -- the caller's to
|
|
721
|
+
// replace or zeroize while cms.sign reads them in a later turn. A CryptoKey is
|
|
722
|
+
// an opaque handle and is passed through as-is.
|
|
723
|
+
function copyValue(v, isSecret) {
|
|
724
|
+
if (Buffer.isBuffer(v) || v instanceof Uint8Array) return _own(Buffer.from(v), isSecret);
|
|
725
|
+
if (!v || typeof v !== "object" || typeof v.type === "string") return v; // CryptoKey / scalar
|
|
726
|
+
var c = {}, ck;
|
|
727
|
+
for (ck in v) {
|
|
728
|
+
if (!Object.prototype.hasOwnProperty.call(v, ck)) continue;
|
|
729
|
+
var cv = v[ck];
|
|
730
|
+
c[ck] = (Buffer.isBuffer(cv) || cv instanceof Uint8Array) ? _own(Buffer.from(cv), isSecret) : cv;
|
|
731
|
+
}
|
|
732
|
+
return c;
|
|
733
|
+
}
|
|
734
|
+
// A composite key's components are listed too: each is PKCS#8 bytes that actually sign, so
|
|
735
|
+
// leaving them out would wipe the wrapper and keep the secrets it was wrapping.
|
|
736
|
+
function _own(buf, isSecret) {
|
|
737
|
+
if (isSecret && owned) owned.push(buf);
|
|
738
|
+
return buf;
|
|
739
|
+
}
|
|
740
|
+
return Array.isArray(signer) ? signer.map(one) : one(signer);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* RFC 5272 sec. 3.2, the three rules that apply when the signature is made with
|
|
745
|
+
* the private key of a certification request the message carries rather than with
|
|
746
|
+
* an already-certified key:
|
|
747
|
+
*
|
|
748
|
+
* a. that request MUST include a Subject Key Identifier extension;
|
|
749
|
+
* b. the subjectKeyIdentifier form of SignerIdentifier MUST be used;
|
|
750
|
+
* c. its value MUST be the Subject Key Identifier that request specifies.
|
|
751
|
+
*
|
|
752
|
+
* (b) is structural and pki.cms.sign already emits only that form for a key-only
|
|
753
|
+
* signer. (a) and (c) are about agreement between the signer and the requests
|
|
754
|
+
* beside it, which is checked here: an identifier the requests never declare
|
|
755
|
+
* leaves the CA unable to tie the signature to the key being enrolled, so the
|
|
756
|
+
* request is signed but unusable -- and it is the producer's job to catch that,
|
|
757
|
+
* not the CA's to guess.
|
|
758
|
+
*
|
|
759
|
+
* This lives HERE rather than in pki.cms.sign because it is a CMC rule: only this
|
|
760
|
+
* layer knows the content is a PKIData and which requests are in it. Teaching the
|
|
761
|
+
* generic CMS signer to parse CMC content would put the protocol's rules in the
|
|
762
|
+
* wrong module.
|
|
763
|
+
*
|
|
764
|
+
* A signer WITH a certificate is untouched -- it identifies itself by that
|
|
765
|
+
* certificate, and the clause does not reach it.
|
|
766
|
+
*/
|
|
767
|
+
function _assertKeyOnlySigner(signer, requests) {
|
|
768
|
+
var all = Array.isArray(signer) ? signer : [signer];
|
|
769
|
+
var keyOnly = all.filter(function (x) { return x && x.cert == null && x.spki != null; });
|
|
770
|
+
if (!keyOnly.length) return;
|
|
771
|
+
// Sec. 3.2's fourth rule: "If the request key is used for signing, there MUST be
|
|
772
|
+
// only one SignerInfo in the SignedData." A request key has no certificate and
|
|
773
|
+
// so no independent identity; letting it sign alongside others would produce a
|
|
774
|
+
// message whose signer set the CA cannot reason about.
|
|
775
|
+
if (all.length !== 1) {
|
|
776
|
+
throw E("cmc/bad-signer",
|
|
777
|
+
"a Full PKI Request signed with a certification request's own key MUST carry exactly one SignerInfo, " +
|
|
778
|
+
"got " + all.length + " (RFC 5272 sec. 3.2)");
|
|
779
|
+
}
|
|
780
|
+
keyOnly.forEach(function (so) { _assertKeyOnlySignerBinding(so, requests); });
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// The sec. 3.2a/3.2c binding for ONE key-only signer.
|
|
784
|
+
function _assertKeyOnlySignerBinding(so, requests) {
|
|
785
|
+
var id = so.keyIdentifier;
|
|
786
|
+
var idBytes = (Buffer.isBuffer(id) || id instanceof Uint8Array) ? Buffer.from(id) : null;
|
|
787
|
+
if (!idBytes) {
|
|
788
|
+
throw E("cmc/bad-signer",
|
|
789
|
+
"a key-only signer must name the Subject Key Identifier its certification request declares (RFC 5272 sec. 3.2)");
|
|
790
|
+
}
|
|
791
|
+
var signerSpki = (Buffer.isBuffer(so.spki) || so.spki instanceof Uint8Array) ? Buffer.from(so.spki) : null;
|
|
792
|
+
if (!signerSpki) {
|
|
793
|
+
throw E("cmc/bad-signer", "a key-only signer must carry its spki as DER bytes (RFC 5272 sec. 3.2)");
|
|
794
|
+
}
|
|
795
|
+
var declaredSki = false, sawRequest = false;
|
|
796
|
+
for (var i = 0; i < requests.length; i++) {
|
|
797
|
+
var req = requests[i];
|
|
798
|
+
// BOTH key-bearing arms: sec. 3.2 says the signing key may belong to a request
|
|
799
|
+
// "included in the TaggedRequest tcr or crm fields", so reading only PKCS#10
|
|
800
|
+
// would reject every conforming CRMF enrolment of a brand-new key.
|
|
801
|
+
var declaredBy;
|
|
802
|
+
try {
|
|
803
|
+
declaredBy = req && req.tcr != null ? _csrKeyIdentity(req.tcr)
|
|
804
|
+
: (req && req.crm != null ? _crmKeyIdentity(req.crm) : null);
|
|
805
|
+
} catch (_e) { declaredBy = null; } // a request this layer cannot read declares nothing
|
|
806
|
+
if (req && (req.tcr != null || req.crm != null)) sawRequest = true;
|
|
807
|
+
if (!declaredBy || !declaredBy.ski) continue;
|
|
808
|
+
declaredSki = true;
|
|
809
|
+
// The identifier AND the key. Matching the identifier alone would accept a
|
|
810
|
+
// signer holding key A while the request it points at asks to certify key B --
|
|
811
|
+
// the SKI is caller-chosen, so the two can be made to agree while the keys do
|
|
812
|
+
// not, and a CA resolving the SID to the requested key could then not verify
|
|
813
|
+
// the carrier at all. The identifier says WHICH request; the key is what makes
|
|
814
|
+
// the claim true. The comparison runs through the toolkit's shared byte
|
|
815
|
+
// equality; neither value here is secret, but there is one way to compare.
|
|
816
|
+
if (guard.crypto.constantTimeEqual(declaredBy.ski, idBytes) &&
|
|
817
|
+
declaredBy.spki && guard.crypto.constantTimeEqual(declaredBy.spki, signerSpki)) {
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
if (!sawRequest) {
|
|
822
|
+
throw E("cmc/bad-signer",
|
|
823
|
+
"a key-only signer signs with the key of a certification request in this message, but this PKIData carries none (RFC 5272 sec. 3.2)");
|
|
824
|
+
}
|
|
825
|
+
if (!declaredSki) {
|
|
826
|
+
throw E("cmc/bad-signer",
|
|
827
|
+
"no certification request in this PKIData declares a Subject Key Identifier, which sec. 3.2 requires " +
|
|
828
|
+
"of the request whose key signs the message (RFC 5272 sec. 3.2)");
|
|
829
|
+
}
|
|
830
|
+
throw E("cmc/bad-signer",
|
|
831
|
+
"the key-only signer does not match any certification request in this PKIData: sec. 3.2 requires the " +
|
|
832
|
+
"SignerInfo to name the Subject Key Identifier of the request whose key is signing, AND that request " +
|
|
833
|
+
"to be the one asking for this very public key (RFC 5272 sec. 3.2)");
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// { ski, spki } for a CRMF certification request -- the identifier it declares and
|
|
837
|
+
// the key it asks to have certified. The CertTemplate carries extensions directly,
|
|
838
|
+
// so there is no attribute wrapper to unwrap; the extnValue is the same DER
|
|
839
|
+
// SubjectKeyIdentifier OCTET STRING.
|
|
840
|
+
function _crmKeyIdentity(crmDer) {
|
|
841
|
+
var msg = _asCertReqMsg(_der(crmDer, "a crm CertReqMsg"));
|
|
842
|
+
// pki.schema.crmf reads CertReqMessages (SEQUENCE OF CertReqMsg); one message is
|
|
843
|
+
// wrapped rather than decoded here, so the CRMF rules stay in the CRMF parser.
|
|
844
|
+
var msgs = crmf.parse(b.sequence([msg])).messages;
|
|
845
|
+
var tmpl = msgs && msgs[0] && msgs[0].certReq && msgs[0].certReq.certTemplate;
|
|
846
|
+
var found = null;
|
|
847
|
+
((tmpl && tmpl.extensions) || []).forEach(function (e) {
|
|
848
|
+
if (e.name !== "subjectKeyIdentifier" || found) return;
|
|
849
|
+
found = asn1.read.octetString(asn1.decode(e.value));
|
|
850
|
+
});
|
|
851
|
+
// RFC 4211 sec. 4.1: the requested key may live in the signature POP's
|
|
852
|
+
// POPOSigningKeyInput rather than the CertTemplate, and the CRMF parser surfaces
|
|
853
|
+
// it there. Reading only the template would refuse a key-only signer whose
|
|
854
|
+
// request is perfectly conforming.
|
|
855
|
+
var msg0 = msgs && msgs[0];
|
|
856
|
+
var pk = (tmpl && tmpl.publicKey) ||
|
|
857
|
+
(msg0 && msg0.popo && msg0.popo.poposkInput && msg0.popo.poposkInput.publicKey);
|
|
858
|
+
return { ski: found, spki: Buffer.isBuffer(pk) ? pk : (pk && pk.bytes) || null };
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// { ski, spki } for a PKCS#10 certification request. Read through the shipped CSR
|
|
862
|
+
// parser and the extensionRequest attribute it already decodes, so the extension
|
|
863
|
+
// rules are not restated here.
|
|
864
|
+
function _csrKeyIdentity(csrDer) {
|
|
865
|
+
var parsed = csr.parse(_der(csrDer, "a tcr certificationRequest"));
|
|
866
|
+
var found = null;
|
|
867
|
+
(parsed.attributes || []).forEach(function (a) {
|
|
868
|
+
if (a.name !== "extensionRequest") return;
|
|
869
|
+
(a.extensions || []).forEach(function (e) {
|
|
870
|
+
if (e.name !== "subjectKeyIdentifier" || found) return;
|
|
871
|
+
// extnValue holds a DER SubjectKeyIdentifier ::= OCTET STRING; the identifier
|
|
872
|
+
// is its contents, not the TLV.
|
|
873
|
+
found = asn1.read.octetString(asn1.decode(e.value));
|
|
874
|
+
});
|
|
875
|
+
});
|
|
876
|
+
var spki = parsed.subjectPublicKeyInfo;
|
|
877
|
+
return { ski: found, spki: (spki && spki.bytes) || null };
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
module.exports = { build: build };
|