@blamejs/pki 0.4.14 → 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 +61 -0
- 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 +438 -100
- 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-pkix.js +6 -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,791 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @module pki.schema.cmc
|
|
6
|
+
* @nav Schema
|
|
7
|
+
* @title CMC
|
|
8
|
+
* @order 185
|
|
9
|
+
* @slug cmc
|
|
10
|
+
*
|
|
11
|
+
* @intro
|
|
12
|
+
* RFC 5272 CMC message-layer decoding, as updated by RFC 6402. A Full PKI
|
|
13
|
+
* Request (`PKIData`) and a Full PKI Response (`PKIResponse`) both ride inside
|
|
14
|
+
* a CMS SignedData (or AuthenticatedData) as the encapsulated content, named by
|
|
15
|
+
* the eContentType `id-cct-PKIData` / `id-cct-PKIResponse` -- so `parse` peels
|
|
16
|
+
* the CMS layer, dispatches on that content type, and decodes the message.
|
|
17
|
+
*
|
|
18
|
+
* Every body part carries a `BodyPartID`, and the identifier MUST be unique
|
|
19
|
+
* across the WHOLE message rather than within one sequence; 0 is reserved as
|
|
20
|
+
* the reference to the enclosing PKIData and is never an element's own
|
|
21
|
+
* identity. Controls are surfaced as an ordered list with their values RAW: an
|
|
22
|
+
* unrecognized control is DATA, not a fault, because the rule that a server
|
|
23
|
+
* fails an unrecognized control binds the server, not the client reading a
|
|
24
|
+
* response. The status controls (`CMCStatusInfo` v1 and `CMCStatusInfoV2`) are
|
|
25
|
+
* collected in wire order -- a response may carry several verdicts, and the
|
|
26
|
+
* ABSENCE of one means success.
|
|
27
|
+
*
|
|
28
|
+
* The `reqSequence` byte range is surfaced exactly as it appeared on the wire,
|
|
29
|
+
* tag and length included, because the Identity Proof V2 witness is computed
|
|
30
|
+
* over those bytes and a re-serialization would silently differ.
|
|
31
|
+
*
|
|
32
|
+
* @spec RFC 5272, RFC 5273, RFC 5274, RFC 6402
|
|
33
|
+
* @card
|
|
34
|
+
* Decode RFC 5272 CMC Full PKI Requests and Responses inside CMS -- controls
|
|
35
|
+
* with raw values, tagged requests (PKCS#10 / CRMF / other), ordered status
|
|
36
|
+
* verdicts with the RFC 6402 two-module OtherStatusInfo ambiguity resolved,
|
|
37
|
+
* whole-message body-part identity, raw reqSequence bytes, fail-closed.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
var asn1 = require("./asn1-der");
|
|
41
|
+
var schema = require("./schema-engine");
|
|
42
|
+
var pkix = require("./schema-pkix");
|
|
43
|
+
var oid = require("./oid");
|
|
44
|
+
var cms = require("./schema-cms");
|
|
45
|
+
var guard = require("./guard-all");
|
|
46
|
+
var frameworkError = require("./framework-error");
|
|
47
|
+
|
|
48
|
+
var CmcError = frameworkError.CmcError;
|
|
49
|
+
|
|
50
|
+
var NS = pkix.makeNS("cmc", CmcError, oid);
|
|
51
|
+
var TAGS = asn1.TAGS;
|
|
52
|
+
|
|
53
|
+
function E(code, message, cause) { return new CmcError(code, message, cause); }
|
|
54
|
+
function O(name) { return oid.byName(name); }
|
|
55
|
+
|
|
56
|
+
// Registry constants -- never a dotted literal in a format module.
|
|
57
|
+
var OID_PKI_DATA = O("id-cct-PKIData");
|
|
58
|
+
var OID_PKI_RESPONSE = O("id-cct-PKIResponse");
|
|
59
|
+
var OID_STATUS_INFO = O("id-cmc-statusInfo");
|
|
60
|
+
var OID_STATUS_INFO_V2 = O("id-cmc-statusInfoV2");
|
|
61
|
+
var OID_RESPONSE_BODY = O("id-cmc-responseBody");
|
|
62
|
+
var OID_RA_IDENTITY_WITNESS = O("id-cmc-raIdentityWitness");
|
|
63
|
+
|
|
64
|
+
// BodyPartID ::= INTEGER (0..4294967295) -- RFC 5272 sec. 3.2.2.
|
|
65
|
+
var BODY_PART_MAX = 4294967295n;
|
|
66
|
+
|
|
67
|
+
// CMCStatus ::= INTEGER { success(0), failed(2), pending(3), noSupport(4),
|
|
68
|
+
// confirmRequired(5), popRequired(6), partial(7) } -- RFC 5272 sec. 6.1.3.
|
|
69
|
+
// 1 is RESERVED: it is neither a success nor a failure, so it is refused rather
|
|
70
|
+
// than folded into either.
|
|
71
|
+
var STATUS_BY_VALUE = {
|
|
72
|
+
0: "success", 2: "failed", 3: "pending", 4: "noSupport",
|
|
73
|
+
5: "confirmRequired", 6: "popRequired", 7: "partial",
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// CMCFailInfo ::= INTEGER { ... } -- RFC 5272 sec. 6.1.4. Values outside this set
|
|
77
|
+
// are surfaced numerically rather than refused: sec. 6.1.4 reserves 1000..1999
|
|
78
|
+
// for private extended reasons, so an unknown number is legal data.
|
|
79
|
+
var FAIL_INFO_BY_VALUE = {
|
|
80
|
+
0: "badAlg", 1: "badMessageCheck", 2: "badRequest", 3: "badTime", 4: "badCertId",
|
|
81
|
+
5: "unsupportedExt", 6: "mustArchiveKeys", 7: "badIdentity", 8: "popRequired",
|
|
82
|
+
9: "popFailed", 10: "noKeyReuse", 11: "internalCAError", 12: "tryLater",
|
|
83
|
+
13: "authDataFail",
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// The CMS content types a Full PKI Request / Response may ride in -- RFC 5272
|
|
87
|
+
// sec. 3.2 ("encapsulated in either a SignedData or an AuthenticatedData") and
|
|
88
|
+
// sec. 4.2, which says the same of the response.
|
|
89
|
+
// The names are the OID REGISTRY's, read off `oid.name(...)` rather than the
|
|
90
|
+
// spec's prose: RFC 5652 calls the type AuthenticatedData, the registry row is
|
|
91
|
+
// `authData`, and hard-coding the prose spelling would reject every conforming
|
|
92
|
+
// AuthenticatedData response while looking correct.
|
|
93
|
+
var CMC_CARRIERS = { signedData: 1, authData: 1 };
|
|
94
|
+
|
|
95
|
+
// Control PLACEMENT -- RFC 6402 sec. 2.5 / 2.6 give two MUSTs about where a
|
|
96
|
+
// control may appear. An OID-keyed table states them once, in the shape of the
|
|
97
|
+
// shipped CMS attribute-placement registry, so a new placement rule is a row.
|
|
98
|
+
var CONTROL_PLACEMENT = {};
|
|
99
|
+
CONTROL_PLACEMENT[OID_RESPONSE_BODY] = "pkiResponse"; // sec. 2.6: never in a PKIData
|
|
100
|
+
CONTROL_PLACEMENT[OID_RA_IDENTITY_WITNESS] = "pkiData"; // sec. 2.5: never in a PKIResponse
|
|
101
|
+
|
|
102
|
+
// ---- leaves ----------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
// A BodyPartID read + bounded + narrowed atomically. `guard.range.int` takes
|
|
105
|
+
// BigInt bounds and returns a Number only when the value is inside them, so an
|
|
106
|
+
// out-of-range identifier can never reach the caller as a silently-truncated int.
|
|
107
|
+
function readBodyPartId(node, label) {
|
|
108
|
+
var v = asn1.read.integer(node);
|
|
109
|
+
return guard.range.int(v, 0n, BODY_PART_MAX, E, "cmc/bad-body-part-id", label || "a BodyPartID");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// The identity of an element, where 0 is reserved (RFC 5272 sec. 3.2.2: "The
|
|
113
|
+
// bodyPartID value of 0 is reserved for use as the reference to the current
|
|
114
|
+
// PKIData object"). A REFERENCE may be 0; an identity may not.
|
|
115
|
+
function readBodyPartIdentity(node) {
|
|
116
|
+
var id = readBodyPartId(node, "a body part identity");
|
|
117
|
+
if (id === 0) {
|
|
118
|
+
throw E("cmc/reserved-body-part-id",
|
|
119
|
+
"bodyPartID 0 is reserved as the reference to the current PKIData and cannot identify a body part (RFC 5272 sec. 3.2.2)");
|
|
120
|
+
}
|
|
121
|
+
return id;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ---- controls --------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
// TaggedAttribute ::= SEQUENCE { bodyPartID BodyPartID, attrType OBJECT
|
|
127
|
+
// IDENTIFIER, attrValues SET OF AttributeValue } -- RFC 5272 sec. 3.2.1.1.
|
|
128
|
+
// attrValues are surfaced RAW: this decoder does not know every control, and
|
|
129
|
+
// PD7's "fail on an unrecognized control" binds the SERVER, not a client reading
|
|
130
|
+
// a response, so an unknown value is data to hand on rather than a fault.
|
|
131
|
+
//
|
|
132
|
+
// The placement rule is NOT checked here: it depends on which message the control
|
|
133
|
+
// sits in, which the enclosing walk knows and this shape does not.
|
|
134
|
+
var TAGGED_ATTRIBUTE = schema.seq([
|
|
135
|
+
schema.field("bodyPartID", schema.decode(readBodyPartIdentity)),
|
|
136
|
+
schema.field("attrType", schema.oidLeaf()),
|
|
137
|
+
schema.field("attrValues", schema.setOf(schema.any(),
|
|
138
|
+
{ code: "cmc/bad-control", what: "TaggedAttribute attrValues", build: rawList })),
|
|
139
|
+
], {
|
|
140
|
+
assert: "sequence", arity: { exact: 3 }, code: "cmc/bad-control", what: "a TaggedAttribute",
|
|
141
|
+
build: function (m) {
|
|
142
|
+
var attrType = m.fields.attrType.value;
|
|
143
|
+
return {
|
|
144
|
+
bodyPartID: m.fields.bodyPartID.value,
|
|
145
|
+
attrType: attrType,
|
|
146
|
+
attrName: oid.name(attrType) || null,
|
|
147
|
+
values: m.fields.attrValues.value.result,
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// RFC 6402 sec. 2.5 / 2.6 give two MUSTs about WHERE a control may appear, and
|
|
153
|
+
// both are one-directional. Applied against the enclosing message kind.
|
|
154
|
+
function assertControlPlacement(control, where) {
|
|
155
|
+
var placement = CONTROL_PLACEMENT[control.attrType];
|
|
156
|
+
if (placement && placement !== where) {
|
|
157
|
+
throw E("cmc/control-misplaced",
|
|
158
|
+
"the control " + (control.attrName || control.attrType) + " may appear only in the control sequence of a " +
|
|
159
|
+
placement + " (RFC 6402 sec. 2.5 / 2.6)");
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---- status controls -------------------------------------------------
|
|
164
|
+
|
|
165
|
+
// BodyPartReference ::= CHOICE { bodyPartID BodyPartID, bodyPartPath BodyPartPath }
|
|
166
|
+
// -- an INTEGER and a SEQUENCE, so the arms are tag-disjoint. BodyPartPath is
|
|
167
|
+
// SEQUENCE SIZE(1..MAX), so an empty one is malformed rather than "no path".
|
|
168
|
+
function parseBodyPartReference(node) {
|
|
169
|
+
if (node.tagClass === "universal" && node.tagNumber === TAGS.INTEGER) {
|
|
170
|
+
return { bodyPartID: readBodyPartId(node), bodyPartPath: null };
|
|
171
|
+
}
|
|
172
|
+
if (node.tagClass === "universal" && node.tagNumber === TAGS.SEQUENCE && node.constructed) {
|
|
173
|
+
if (node.children.length < 1) {
|
|
174
|
+
throw E("cmc/bad-body-part-path", "a BodyPartPath is SEQUENCE SIZE(1..MAX) and cannot be empty (RFC 5272 sec. 3.2.2)");
|
|
175
|
+
}
|
|
176
|
+
return { bodyPartID: null, bodyPartPath: node.children.map(function (c) { return readBodyPartId(c); }) };
|
|
177
|
+
}
|
|
178
|
+
throw E("cmc/bad-status-info", "a BodyPartReference must be a BodyPartID INTEGER or a BodyPartPath SEQUENCE");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* OtherStatusInfo ::= CHOICE { failInfo CMCFailInfo, pendInfo PendInfo,
|
|
183
|
+
* extendedFailInfo SEQUENCE { ... } }
|
|
184
|
+
*
|
|
185
|
+
* The load-bearing ambiguity. RFC 5272 App A and RFC 6402 App A.1 (the 1988
|
|
186
|
+
* module) leave `extendedFailInfo` UNTAGGED, so it collides with `pendInfo` --
|
|
187
|
+
* both are a bare SEQUENCE. RFC 6402 App A.2 (the 2008 module) tags it `[1]`.
|
|
188
|
+
* Both encodings occur on the wire, so both are decoded:
|
|
189
|
+
*
|
|
190
|
+
* universal INTEGER -> failInfo
|
|
191
|
+
* context [1] -> extendedFailInfo (2008 module)
|
|
192
|
+
* universal SEQUENCE -> disambiguate on the FIRST child's tag:
|
|
193
|
+
* OCTET STRING -> pendInfo (PendInfo ::= SEQUENCE
|
|
194
|
+
* { pendToken OCTET STRING, pendTime GeneralizedTime })
|
|
195
|
+
* OBJECT IDENTIFIER -> extendedFailInfo
|
|
196
|
+
* anything else -> refuse
|
|
197
|
+
*
|
|
198
|
+
* The final arm is the point: a shape that matches neither rule is REFUSED, not
|
|
199
|
+
* guessed at. Guessing here would attach a failure reason, or a pending token,
|
|
200
|
+
* that the responder never sent.
|
|
201
|
+
*/
|
|
202
|
+
function parseOtherStatusInfo(node, version) {
|
|
203
|
+
// The extendedFailInfo arm belongs to CMCStatusInfoV2 alone. RFC 5272 sec. 6.1.1
|
|
204
|
+
// gives the v1 otherInfo exactly two arms -- `CHOICE { failInfo CMCFailInfo,
|
|
205
|
+
// pendInfo PendInfo }` -- so accepting the third here would read a failure
|
|
206
|
+
// verdict out of a v1 control that cannot legally express one, and the
|
|
207
|
+
// disambiguation below would be answering a question v1 never asks.
|
|
208
|
+
var extendedAllowed = version !== 1;
|
|
209
|
+
if (node.tagClass === "universal" && node.tagNumber === TAGS.INTEGER) {
|
|
210
|
+
var v = asn1.read.integer(node);
|
|
211
|
+
var n = guard.range.int(v, 0n, 0xFFFFFFFFn, E, "cmc/bad-status-info", "a CMCFailInfo");
|
|
212
|
+
return { failInfo: n, failInfoName: FAIL_INFO_BY_VALUE[n] || null, pendInfo: null, extendedFailInfo: null };
|
|
213
|
+
}
|
|
214
|
+
if (node.tagClass === "context" && node.tagNumber === 1) {
|
|
215
|
+
if (!extendedAllowed) {
|
|
216
|
+
throw E("cmc/bad-status-info",
|
|
217
|
+
"a v1 CMCStatusInfo otherInfo is failInfo or pendInfo only; the [1] extendedFailInfo arm is CMCStatusInfoV2's (RFC 5272 sec. 6.1.1)");
|
|
218
|
+
}
|
|
219
|
+
// IMPLICIT, so the [1] REPLACES the SEQUENCE tag and its children are the
|
|
220
|
+
// failInfoOID / failInfoValue directly -- there is no inner SEQUENCE to
|
|
221
|
+
// unwrap. Both RFC 6402 modules are IMPLICIT: A.1 (rfc6402.txt:805) and A.2
|
|
222
|
+
// (rfc6402.txt:1314) each open `DEFINITIONS IMPLICIT TAGS ::=`, and the
|
|
223
|
+
// EXPLICIT-override rule does not apply here because the tagged type is a
|
|
224
|
+
// plain SEQUENCE rather than a CHOICE or an open type. Reading this arm as
|
|
225
|
+
// EXPLICIT would reject every conforming 2008-module response.
|
|
226
|
+
return { failInfo: null, failInfoName: null, pendInfo: null, extendedFailInfo: readExtendedFailInfo(node.children, "[1] extendedFailInfo") };
|
|
227
|
+
}
|
|
228
|
+
if (node.tagClass === "universal" && node.tagNumber === TAGS.SEQUENCE && node.constructed) {
|
|
229
|
+
var first = node.children[0];
|
|
230
|
+
if (!first) throw E("cmc/ambiguous-status-info", "an empty OtherStatusInfo SEQUENCE matches neither pendInfo nor extendedFailInfo");
|
|
231
|
+
if (first.tagClass === "universal" && first.tagNumber === TAGS.OCTET_STRING) {
|
|
232
|
+
if (node.children.length !== 2) throw E("cmc/bad-status-info", "PendInfo must be { pendToken, pendTime }");
|
|
233
|
+
// pendTime is a GeneralizedTime specifically, so assert the tag rather than
|
|
234
|
+
// accept either time form -- a UTCTime here is a different encoding, and
|
|
235
|
+
// reading it anyway would let a two-digit year through the retry clock.
|
|
236
|
+
var timeNode = node.children[1];
|
|
237
|
+
if (timeNode.tagClass !== "universal" || timeNode.tagNumber !== TAGS.GENERALIZED_TIME) {
|
|
238
|
+
throw E("cmc/bad-status-info", "PendInfo pendTime must be a GeneralizedTime (RFC 5272 sec. 6.1.1)");
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
failInfo: null, failInfoName: null, extendedFailInfo: null,
|
|
242
|
+
pendInfo: { pendToken: asn1.read.octetString(first), pendTime: asn1.read.time(timeNode) },
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
if (first.tagClass === "universal" && first.tagNumber === TAGS.OBJECT_IDENTIFIER) {
|
|
246
|
+
if (!extendedAllowed) {
|
|
247
|
+
throw E("cmc/bad-status-info",
|
|
248
|
+
"a v1 CMCStatusInfo otherInfo SEQUENCE is pendInfo; this one leads with an OBJECT IDENTIFIER, which is the extendedFailInfo shape CMCStatusInfoV2 introduced (RFC 5272 sec. 6.1.1)");
|
|
249
|
+
}
|
|
250
|
+
return { failInfo: null, failInfoName: null, pendInfo: null, extendedFailInfo: readExtendedFailInfo(node.children, "extendedFailInfo") };
|
|
251
|
+
}
|
|
252
|
+
throw E("cmc/ambiguous-status-info",
|
|
253
|
+
"an untagged OtherStatusInfo SEQUENCE is pendInfo or extendedFailInfo depending on its first element, and this one is neither an OCTET STRING nor an OBJECT IDENTIFIER (RFC 5272 App A vs RFC 6402 App A.1/A.2)");
|
|
254
|
+
}
|
|
255
|
+
throw E("cmc/bad-status-info", "OtherStatusInfo must be a CMCFailInfo INTEGER, a PendInfo SEQUENCE, or an extendedFailInfo");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ExtendedFailInfo ::= SEQUENCE { failInfoOID OBJECT IDENTIFIER, failInfoValue ANY }
|
|
259
|
+
// The OID is surfaced raw and NEVER mapped onto a CMCFailInfo: RFC 5272 sec.
|
|
260
|
+
// 6.1.1 makes internalCAError only a MAY for an unrecognized extended error, and
|
|
261
|
+
// synthesizing it would report a reason the responder did not send.
|
|
262
|
+
function readExtendedFailInfo(children, what) {
|
|
263
|
+
if (!children || children.length !== 2) {
|
|
264
|
+
throw E("cmc/bad-status-info", what + " must be SEQUENCE { failInfoOID, failInfoValue }");
|
|
265
|
+
}
|
|
266
|
+
return { failInfoOID: asn1.read.oid(children[0]), failInfoValueBytes: children[1].bytes };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// CMCStatusInfoV2 ::= SEQUENCE { cMCStatus, bodyList SEQUENCE SIZE(1..MAX) OF
|
|
270
|
+
// BodyPartReference, statusString UTF8String OPTIONAL, otherInfo OPTIONAL }
|
|
271
|
+
// The v1 CMCStatusInfo is the same shape with a SEQUENCE OF BodyPartID bodyList
|
|
272
|
+
// and no extendedFailInfo arm; both are decoded into one surface so a caller
|
|
273
|
+
// reads one ordered list of verdicts (RFC 5272 sec. 6.1: a client MUST cope with
|
|
274
|
+
// several status controls).
|
|
275
|
+
function parseStatusInfo(valueDer, version, bodyPartID) {
|
|
276
|
+
var root = asn1.decode(valueDer);
|
|
277
|
+
if (root.tagClass !== "universal" || root.tagNumber !== TAGS.SEQUENCE || !root.constructed) {
|
|
278
|
+
throw E("cmc/bad-status-info", "a CMCStatusInfo value must be a SEQUENCE");
|
|
279
|
+
}
|
|
280
|
+
var kids = root.children;
|
|
281
|
+
if (kids.length < 2) throw E("cmc/bad-status-info", "a CMCStatusInfo needs at least cMCStatus and bodyList");
|
|
282
|
+
|
|
283
|
+
var statusValue = asn1.read.integer(kids[0]);
|
|
284
|
+
var statusNum = guard.range.int(statusValue, 0n, 0xFFFFFFFFn, E, "cmc/bad-status", "a CMCStatus");
|
|
285
|
+
var statusName = STATUS_BY_VALUE[statusNum];
|
|
286
|
+
if (!statusName) {
|
|
287
|
+
// 1 is reserved and everything above 7 is unassigned. An unknown status is
|
|
288
|
+
// not a verdict in either direction, so it is refused rather than mapped.
|
|
289
|
+
throw E("cmc/bad-status", "unknown CMCStatus " + statusNum + " (1 is reserved; RFC 5272 sec. 6.1.3)");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
var listNode = kids[1];
|
|
293
|
+
if (listNode.tagClass !== "universal" || listNode.tagNumber !== TAGS.SEQUENCE || !listNode.constructed) {
|
|
294
|
+
throw E("cmc/bad-status-info", "the CMCStatusInfo bodyList must be a SEQUENCE");
|
|
295
|
+
}
|
|
296
|
+
if (listNode.children.length < 1) {
|
|
297
|
+
throw E("cmc/bad-status-info", "the CMCStatusInfo bodyList is SEQUENCE SIZE(1..MAX) and cannot be empty (RFC 5272 sec. 6.1.1)");
|
|
298
|
+
}
|
|
299
|
+
var bodyList = listNode.children.map(function (c) {
|
|
300
|
+
return version === 2 ? parseBodyPartReference(c) : { bodyPartID: readBodyPartId(c), bodyPartPath: null };
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// The two OPTIONAL tail fields are ORDERED by the ASN.1: statusString precedes
|
|
304
|
+
// otherInfo. Tracking only "have I seen a statusString yet" would accept them
|
|
305
|
+
// reversed -- a second encoding of a structure DER admits exactly one encoding
|
|
306
|
+
// of -- so the walk refuses anything once otherInfo has been consumed.
|
|
307
|
+
var statusString = null, other = null, sawOther = false;
|
|
308
|
+
for (var i = 2; i < kids.length; i++) {
|
|
309
|
+
var k = kids[i];
|
|
310
|
+
if (k.tagClass === "universal" && k.tagNumber === TAGS.UTF8_STRING && statusString === null && !sawOther) {
|
|
311
|
+
// read.string, not a per-type reader: the codec exposes one strict text reader
|
|
312
|
+
// for the DirectoryString family and there is no `read.utf8`. The tag is
|
|
313
|
+
// already asserted UTF8String above, so this decodes exactly that.
|
|
314
|
+
statusString = asn1.read.string(k);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (sawOther) {
|
|
318
|
+
throw E("cmc/bad-status-info",
|
|
319
|
+
"a CMCStatusInfo carries at most one OtherStatusInfo, and the OPTIONAL statusString precedes it (RFC 5272 sec. 6.1.1)");
|
|
320
|
+
}
|
|
321
|
+
other = parseOtherStatusInfo(k, version);
|
|
322
|
+
sawOther = true;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
var out = {
|
|
326
|
+
version: version, bodyPartID: bodyPartID, status: statusName, statusValue: statusNum,
|
|
327
|
+
bodyList: bodyList, statusString: statusString,
|
|
328
|
+
failInfo: other ? other.failInfo : null,
|
|
329
|
+
failInfoName: other ? other.failInfoName : null,
|
|
330
|
+
pendInfo: other ? other.pendInfo : null,
|
|
331
|
+
extendedFailInfo: other ? other.extendedFailInfo : null,
|
|
332
|
+
};
|
|
333
|
+
_assertStatusCoherent(out);
|
|
334
|
+
return out;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// The cross-field coupling (RFC 5272 sec. 6.1.1 / 6.1.2): failInfo and
|
|
338
|
+
// extendedFailInfo are "present only if cMCStatus contains the value failed",
|
|
339
|
+
// and pendInfo "MUST be populated for a cMCStatus value of pending or partial".
|
|
340
|
+
// A message whose status and detail disagree is refused rather than reconciled --
|
|
341
|
+
// the two halves would otherwise let a caller read a verdict neither field states.
|
|
342
|
+
function _assertStatusCoherent(s) {
|
|
343
|
+
var isFailed = s.status === "failed";
|
|
344
|
+
var isPending = s.status === "pending" || s.status === "partial";
|
|
345
|
+
if ((s.failInfo !== null || s.extendedFailInfo !== null) && !isFailed) {
|
|
346
|
+
throw E("cmc/status-info-mismatch",
|
|
347
|
+
"a failInfo / extendedFailInfo is present only when cMCStatus is failed, got " + s.status + " (RFC 5272 sec. 6.1.1)");
|
|
348
|
+
}
|
|
349
|
+
if (s.pendInfo !== null && !isPending) {
|
|
350
|
+
throw E("cmc/status-info-mismatch",
|
|
351
|
+
"a pendInfo is present only when cMCStatus is pending or partial, got " + s.status + " (RFC 5272 sec. 6.1.1)");
|
|
352
|
+
}
|
|
353
|
+
if (isPending && s.pendInfo === null) {
|
|
354
|
+
throw E("cmc/status-info-mismatch",
|
|
355
|
+
"the pendInfo field MUST be populated for a cMCStatus of " + s.status + " (RFC 5272 sec. 6.1.2)");
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ---- tagged requests -------------------------------------------------
|
|
360
|
+
|
|
361
|
+
// TaggedRequest ::= CHOICE { tcr [0] TaggedCertificationRequest,
|
|
362
|
+
// crm [1] CertReqMsg,
|
|
363
|
+
// orm [2] SEQUENCE { bodyPartID, requestMessageType,
|
|
364
|
+
// requestMessageValue } }
|
|
365
|
+
// The CMC module is IMPLICIT TAGS, so each context tag REPLACES the arm's own
|
|
366
|
+
// universal tag -- the [0] node's direct children are the TaggedCertificationRequest
|
|
367
|
+
// fields, NOT a nested SEQUENCE. An EXPLICIT encoding is therefore a different
|
|
368
|
+
// (and invalid) message, which is what C5 pins.
|
|
369
|
+
// An IMPLICIT context-tagged constructed node re-encoded under the universal
|
|
370
|
+
// SEQUENCE tag it stands in for. IMPLICIT tagging REPLACES the tag and leaves the
|
|
371
|
+
// content untouched, so restoring it is exactly a re-headering of the same bytes.
|
|
372
|
+
function _asUniversalSequence(node) {
|
|
373
|
+
var headerLen = node.header.end - node.header.start;
|
|
374
|
+
return asn1.encode(0x00, true, TAGS.SEQUENCE, node.bytes.subarray(headerLen));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function parseTaggedRequest(node) {
|
|
378
|
+
if (node.tagClass !== "context" || !node.constructed) {
|
|
379
|
+
throw E("cmc/bad-tagged-request", "a TaggedRequest must be a context-tagged constructed CHOICE arm (RFC 5272 sec. 3.2.1.2)");
|
|
380
|
+
}
|
|
381
|
+
if (node.tagNumber === 0) {
|
|
382
|
+
// TaggedCertificationRequest ::= SEQUENCE { bodyPartID, certificationRequest }
|
|
383
|
+
if (node.children.length !== 2) {
|
|
384
|
+
throw E("cmc/bad-tagged-request", "a tcr arm is [0] IMPLICIT { bodyPartID, certificationRequest }; the module is IMPLICIT TAGS, so an EXPLICIT wrapper is not this arm");
|
|
385
|
+
}
|
|
386
|
+
var csrNode = node.children[1];
|
|
387
|
+
if (csrNode.tagClass !== "universal" || csrNode.tagNumber !== TAGS.SEQUENCE || !csrNode.constructed) {
|
|
388
|
+
throw E("cmc/bad-tagged-request", "the tcr certificationRequest must be a CertificationRequest SEQUENCE");
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
arm: "tcr", bodyPartID: readBodyPartIdentity(node.children[0]),
|
|
392
|
+
certificationRequestBytes: csrNode.bytes, certReqMsgBytes: null,
|
|
393
|
+
requestMessageType: null, requestMessageValueBytes: null,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
if (node.tagNumber === 1) {
|
|
397
|
+
// crm [1] IMPLICIT CertReqMsg.
|
|
398
|
+
//
|
|
399
|
+
// This arm still has a body part IDENTITY, and it is not a separate field:
|
|
400
|
+
// RFC 5272 sec. 3.2.2 says "Body part identifiers are encoded in the
|
|
401
|
+
// certReqIds field for CertReqMsg objects (in a TaggedRequest) or in the
|
|
402
|
+
// bodyPartID field of the other objects", and sec. 6.5.2 repeats it
|
|
403
|
+
// ("... either the bodyPartID of a TaggedCertificationRequest (PKCS #10) or
|
|
404
|
+
// the certReqId of the CertRequest within a CertReqMsg (CRMF)"). So the
|
|
405
|
+
// certReqId is read here and carried into the SAME whole-message uniqueness
|
|
406
|
+
// check every other body part is bound by -- returning null would silently
|
|
407
|
+
// exempt this arm from it.
|
|
408
|
+
//
|
|
409
|
+
// CertReqMsg ::= SEQUENCE { certReq CertRequest, popo OPTIONAL, regInfo OPTIONAL }
|
|
410
|
+
// CertRequest ::= SEQUENCE { certReqId INTEGER, certTemplate CertTemplate, ... }
|
|
411
|
+
//
|
|
412
|
+
// Only the identity is decoded here; the CertReqMsg's own conformance is
|
|
413
|
+
// pki.schema.crmf's to enforce, and duplicating it would fork the rules.
|
|
414
|
+
var certReq = node.children[0];
|
|
415
|
+
if (!certReq || certReq.tagClass !== "universal" || certReq.tagNumber !== TAGS.SEQUENCE || !certReq.constructed) {
|
|
416
|
+
throw E("cmc/bad-tagged-request", "a crm arm is [1] IMPLICIT CertReqMsg, whose first element is a CertRequest SEQUENCE");
|
|
417
|
+
}
|
|
418
|
+
if (!certReq.children.length) {
|
|
419
|
+
throw E("cmc/bad-tagged-request", "a CertRequest must lead with its certReqId INTEGER (RFC 4211 sec. 5)");
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
arm: "crm", bodyPartID: readBodyPartIdentity(certReq.children[0]),
|
|
423
|
+
certificationRequestBytes: null,
|
|
424
|
+
// Re-tagged from the IMPLICIT [1] back to the universal SEQUENCE the CRMF
|
|
425
|
+
// parser expects: handing back the context-tagged bytes would produce a
|
|
426
|
+
// structure no CertReqMsg reader accepts.
|
|
427
|
+
certReqMsgBytes: _asUniversalSequence(node),
|
|
428
|
+
requestMessageType: null, requestMessageValueBytes: null,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
if (node.tagNumber === 2) {
|
|
432
|
+
if (node.children.length !== 3) {
|
|
433
|
+
throw E("cmc/bad-tagged-request", "an orm arm is [2] IMPLICIT { bodyPartID, requestMessageType, requestMessageValue }");
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
arm: "orm", bodyPartID: readBodyPartIdentity(node.children[0]),
|
|
437
|
+
certificationRequestBytes: null, certReqMsgBytes: null,
|
|
438
|
+
requestMessageType: asn1.read.oid(node.children[1]),
|
|
439
|
+
requestMessageValueBytes: node.children[2].bytes,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
throw E("cmc/bad-tagged-request", "unknown TaggedRequest alternative [" + node.tagNumber + "] (RFC 5272 sec. 3.2.1.2)");
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ---- body-part identity ----------------------------------------------
|
|
446
|
+
|
|
447
|
+
// RFC 5272 sec. 3.2.2: "The body part identifier MUST be unique within a single
|
|
448
|
+
// PKIData or PKIResponse." The scope is the WHOLE message -- a checker that ran
|
|
449
|
+
// per-sequence would accept a control and a request that collide, which is
|
|
450
|
+
// exactly the shape B2 pins.
|
|
451
|
+
function assertUniqueBodyPartIds(ids) {
|
|
452
|
+
var seen = Object.create(null);
|
|
453
|
+
for (var i = 0; i < ids.length; i++) {
|
|
454
|
+
if (ids[i] === null) continue;
|
|
455
|
+
if (seen[ids[i]]) {
|
|
456
|
+
throw E("cmc/duplicate-body-part-id",
|
|
457
|
+
"bodyPartID " + ids[i] + " appears more than once; it MUST be unique within a single PKIData or PKIResponse (RFC 5272 sec. 3.2.2)");
|
|
458
|
+
}
|
|
459
|
+
seen[ids[i]] = true;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ---- the two message bodies ------------------------------------------
|
|
464
|
+
|
|
465
|
+
// The two message bodies as engine schemas. Declaring them means the engine owns
|
|
466
|
+
// the positional reads, the field arity and the SET-OF ordering -- the bug class
|
|
467
|
+
// a hand-rolled cursor keeps reintroducing -- while the rules that genuinely span
|
|
468
|
+
// fields (whole-message body-part uniqueness, control placement, the status
|
|
469
|
+
// cross-checks) run in the build / the caller, which is where they can see
|
|
470
|
+
// everything they need.
|
|
471
|
+
// A repeat's walk result is the match object `{ node, items: [{ node, value }] }`,
|
|
472
|
+
// so each of these carries a `build` that flattens it to the plain list the
|
|
473
|
+
// enclosing build wants. The three differ by what an item's `value` IS, which is
|
|
474
|
+
// decided by the item schema:
|
|
475
|
+
// - a `seq` item walks to its own MATCH, so its build output is at .value.result
|
|
476
|
+
// - a `decode` leaf walks straight to whatever its function returned
|
|
477
|
+
// - an `any` item walks to the node, whose .bytes is the raw TLV
|
|
478
|
+
function builtList(match) { return match.items.map(function (i) { return i.value.result; }); }
|
|
479
|
+
function decodedList(match) { return match.items.map(function (i) { return i.value; }); }
|
|
480
|
+
function rawList(match) { return match.items.map(function (i) { return i.node.bytes; }); }
|
|
481
|
+
|
|
482
|
+
var CONTROL_SEQUENCE = schema.seqOf(TAGGED_ATTRIBUTE,
|
|
483
|
+
{ code: "cmc/bad-control", what: "a control sequence", build: builtList });
|
|
484
|
+
var REQ_SEQUENCE = schema.seqOf(schema.decode(parseTaggedRequest),
|
|
485
|
+
{ code: "cmc/bad-tagged-request", what: "a reqSequence", build: decodedList });
|
|
486
|
+
|
|
487
|
+
// TaggedContentInfo ::= SEQUENCE { bodyPartID, contentInfo } (RFC 5272 sec.
|
|
488
|
+
// 3.2.1.3) and OtherMsg ::= SEQUENCE { bodyPartID, otherMsgType, otherMsgValue }
|
|
489
|
+
// (sec. 3.2.1.4). Neither payload is interpreted here -- the ContentInfo is a
|
|
490
|
+
// CMS object the caller decodes, the OtherMsg value is by definition arbitrary --
|
|
491
|
+
// but BOTH carry a body part identity, and sec. 3.2.2's uniqueness rule covers
|
|
492
|
+
// "each element of a PKIData or PKIResponse". Decoding just the identifier is
|
|
493
|
+
// what lets that rule cover them without this layer claiming to understand the
|
|
494
|
+
// payload.
|
|
495
|
+
var TAGGED_CONTENT_INFO = schema.seq([
|
|
496
|
+
schema.field("bodyPartID", schema.decode(readBodyPartIdentity)),
|
|
497
|
+
schema.field("contentInfo", schema.any()),
|
|
498
|
+
], {
|
|
499
|
+
assert: "sequence", arity: { exact: 2 }, code: "cmc/bad-cms-sequence", what: "a TaggedContentInfo",
|
|
500
|
+
build: function (m) {
|
|
501
|
+
// The payload stays RAW -- this layer does not decode a content type it may
|
|
502
|
+
// not know -- but it must still BE a ContentInfo: SEQUENCE { contentType
|
|
503
|
+
// OBJECT IDENTIFIER, content [0] EXPLICIT ANY OPTIONAL } (RFC 5652 sec. 3).
|
|
504
|
+
// Surfacing an INTEGER, or anything else that cannot be one, as a content
|
|
505
|
+
// info would hand the caller a "CMS message" no CMS reader will accept, which
|
|
506
|
+
// is not the same thing as a content type this decoder happens not to handle.
|
|
507
|
+
var ci = m.fields.contentInfo.value;
|
|
508
|
+
var kids = ci.children || [];
|
|
509
|
+
var lead = kids[0];
|
|
510
|
+
if (!(ci.tagClass === "universal" && ci.tagNumber === TAGS.SEQUENCE && ci.constructed) || !lead) {
|
|
511
|
+
throw E("cmc/bad-cms-sequence",
|
|
512
|
+
"a TaggedContentInfo carries a CMS ContentInfo, which leads with its contentType OBJECT IDENTIFIER (RFC 5652 sec. 3)");
|
|
513
|
+
}
|
|
514
|
+
// READ the contentType rather than inspecting its tag. An OBJECT IDENTIFIER
|
|
515
|
+
// TLV can carry content no OID reader will accept -- empty, or a subidentifier
|
|
516
|
+
// padded to a non-minimal encoding -- and a tag check waves both through. The
|
|
517
|
+
// toolkit's own CMS parser refuses them, so accepting them here would let the
|
|
518
|
+
// builder sign a "CMS message" pki.schema.cms.parse will not read back. The
|
|
519
|
+
// VALUE is still not interpreted: this layer only establishes that there is a
|
|
520
|
+
// content type to interpret.
|
|
521
|
+
try { asn1.read.oid(lead); }
|
|
522
|
+
catch (e) {
|
|
523
|
+
throw E("cmc/bad-cms-sequence",
|
|
524
|
+
"a TaggedContentInfo carries a CMS ContentInfo, whose contentType must be a readable OBJECT IDENTIFIER (RFC 5652 sec. 3)", e);
|
|
525
|
+
}
|
|
526
|
+
// The WHOLE envelope, not just its first field: content is `[0] EXPLICIT ANY
|
|
527
|
+
// DEFINED BY contentType OPTIONAL`, so at most one more element and it is that
|
|
528
|
+
// tag. An INTEGER sitting where the content belongs, or a third field, is not
|
|
529
|
+
// a ContentInfo any CMS reader will take -- and stopping at the leading OID
|
|
530
|
+
// would surface it as one.
|
|
531
|
+
if (kids.length > 2) {
|
|
532
|
+
throw E("cmc/bad-cms-sequence",
|
|
533
|
+
"a CMS ContentInfo is { contentType, [0] EXPLICIT content OPTIONAL }; this carries " +
|
|
534
|
+
kids.length + " fields (RFC 5652 sec. 3)");
|
|
535
|
+
}
|
|
536
|
+
if (kids.length === 2) {
|
|
537
|
+
var content = kids[1];
|
|
538
|
+
if (content.tagClass !== "context" || content.tagNumber !== 0 || !content.constructed) {
|
|
539
|
+
throw E("cmc/bad-cms-sequence",
|
|
540
|
+
"a CMS ContentInfo's second field is the [0] EXPLICIT content (RFC 5652 sec. 3)");
|
|
541
|
+
}
|
|
542
|
+
// EXPLICIT wraps EXACTLY ONE value (X.690 sec. 8.14). Checking the tag alone
|
|
543
|
+
// would let an EMPTY wrapper, or one holding several values, read as a content
|
|
544
|
+
// info -- shapes pki.schema.cms.parse refuses as cms/not-a-content-info, so the
|
|
545
|
+
// builder's readback would pass a message its own CMS reader will not accept.
|
|
546
|
+
if (!content.children || content.children.length !== 1) {
|
|
547
|
+
throw E("cmc/bad-cms-sequence",
|
|
548
|
+
"a ContentInfo's [0] EXPLICIT content wraps exactly one value, got " +
|
|
549
|
+
((content.children && content.children.length) || 0) + " (X.690 sec. 8.14)");
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return { bodyPartID: m.fields.bodyPartID.value, contentInfoBytes: ci.bytes };
|
|
553
|
+
},
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
var OTHER_MSG = schema.seq([
|
|
557
|
+
schema.field("bodyPartID", schema.decode(readBodyPartIdentity)),
|
|
558
|
+
schema.field("otherMsgType", schema.oidLeaf()),
|
|
559
|
+
schema.field("otherMsgValue", schema.any()),
|
|
560
|
+
], {
|
|
561
|
+
assert: "sequence", arity: { exact: 3 }, code: "cmc/bad-other-msg", what: "an OtherMsg",
|
|
562
|
+
build: function (m) {
|
|
563
|
+
return {
|
|
564
|
+
bodyPartID: m.fields.bodyPartID.value,
|
|
565
|
+
otherMsgType: m.fields.otherMsgType.value,
|
|
566
|
+
otherMsgValueBytes: m.fields.otherMsgValue.value.bytes,
|
|
567
|
+
};
|
|
568
|
+
},
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
// PKIData ::= SEQUENCE { controlSequence, reqSequence, cmsSequence,
|
|
572
|
+
// otherMsgSequence } -- RFC 5272 sec. 3.2.1. All FOUR are mandatory though each
|
|
573
|
+
// may be empty, so `arity: { exact: 4 }` is the rule, not a convenience.
|
|
574
|
+
var PKI_DATA = schema.seq([
|
|
575
|
+
schema.field("controlSequence", CONTROL_SEQUENCE),
|
|
576
|
+
schema.field("reqSequence", REQ_SEQUENCE),
|
|
577
|
+
schema.field("cmsSequence", schema.seqOf(TAGGED_CONTENT_INFO,
|
|
578
|
+
{ code: "cmc/bad-cms-sequence", what: "the PKIData cmsSequence", build: builtList })),
|
|
579
|
+
schema.field("otherMsgSequence", schema.seqOf(OTHER_MSG,
|
|
580
|
+
{ code: "cmc/bad-other-msg", what: "the PKIData otherMsgSequence", build: builtList })),
|
|
581
|
+
], {
|
|
582
|
+
assert: "sequence", arity: { exact: 4 }, code: "cmc/bad-pkidata",
|
|
583
|
+
what: "a PKIData (SEQUENCE { controlSequence, reqSequence, cmsSequence, otherMsgSequence }, all four mandatory though each may be empty -- RFC 5272 sec. 3.2.1)",
|
|
584
|
+
build: function (m) {
|
|
585
|
+
return {
|
|
586
|
+
kind: "pkiData",
|
|
587
|
+
controls: m.fields.controlSequence.value.result,
|
|
588
|
+
requests: m.fields.reqSequence.value.result,
|
|
589
|
+
cmsSequence: m.fields.cmsSequence.value.result,
|
|
590
|
+
otherMsgs: m.fields.otherMsgSequence.value.result,
|
|
591
|
+
// The raw reqSequence TLV, tag and length included: the Identity Proof V2
|
|
592
|
+
// witness is computed over exactly these bytes "encoded exactly as it
|
|
593
|
+
// appears in the Full PKI Request" (RFC 5272 sec. 6.2.1), so it is taken
|
|
594
|
+
// off the matched NODE rather than re-encoded from the decoded requests.
|
|
595
|
+
reqSequenceBytes: m.fields.reqSequence.node.bytes,
|
|
596
|
+
};
|
|
597
|
+
},
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
// PKIResponse ::= SEQUENCE { controlSequence, cmsSequence, otherMsgSequence } --
|
|
601
|
+
// RFC 5272 sec. 4.2.1. THREE fields: there is no reqSequence in a response.
|
|
602
|
+
var PKI_RESPONSE = schema.seq([
|
|
603
|
+
schema.field("controlSequence", CONTROL_SEQUENCE),
|
|
604
|
+
schema.field("cmsSequence", schema.seqOf(TAGGED_CONTENT_INFO,
|
|
605
|
+
{ code: "cmc/bad-cms-sequence", what: "the PKIResponse cmsSequence", build: builtList })),
|
|
606
|
+
schema.field("otherMsgSequence", schema.seqOf(OTHER_MSG,
|
|
607
|
+
{ code: "cmc/bad-other-msg", what: "the PKIResponse otherMsgSequence", build: builtList })),
|
|
608
|
+
], {
|
|
609
|
+
assert: "sequence", arity: { exact: 3 }, code: "cmc/bad-pkiresponse",
|
|
610
|
+
what: "a PKIResponse (SEQUENCE { controlSequence, cmsSequence, otherMsgSequence } -- three fields, all mandatory though each may be empty -- RFC 5272 sec. 4.2.1)",
|
|
611
|
+
build: function (m) {
|
|
612
|
+
return {
|
|
613
|
+
kind: "pkiResponse",
|
|
614
|
+
controls: m.fields.controlSequence.value.result,
|
|
615
|
+
requests: [],
|
|
616
|
+
cmsSequence: m.fields.cmsSequence.value.result,
|
|
617
|
+
otherMsgs: m.fields.otherMsgSequence.value.result,
|
|
618
|
+
reqSequenceBytes: null,
|
|
619
|
+
};
|
|
620
|
+
},
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// Collect the status controls out of an already-parsed control list, in wire
|
|
624
|
+
// order. Absence is NOT an error: RFC 5272 sec. 6.1.2 says "If no status exists
|
|
625
|
+
// for a Simple or Full PKI Request, then the value of success is assumed", so a
|
|
626
|
+
// response with no status control is a successful one.
|
|
627
|
+
function _collectStatuses(controls) {
|
|
628
|
+
var out = [];
|
|
629
|
+
for (var i = 0; i < controls.length; i++) {
|
|
630
|
+
var c = controls[i];
|
|
631
|
+
var version = c.attrType === OID_STATUS_INFO_V2 ? 2 : (c.attrType === OID_STATUS_INFO ? 1 : 0);
|
|
632
|
+
if (!version) continue;
|
|
633
|
+
if (c.values.length !== 1) {
|
|
634
|
+
throw E("cmc/bad-status-info", "a CMC status control carries exactly one AttributeValue, got " + c.values.length);
|
|
635
|
+
}
|
|
636
|
+
out.push(parseStatusInfo(c.values[0], version, c.bodyPartID));
|
|
637
|
+
}
|
|
638
|
+
return out;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// The cross-cutting rules that a per-field walk cannot see: where each control is
|
|
642
|
+
// allowed to appear, which of them are status verdicts, which are unrecognized
|
|
643
|
+
// (surfaced, never a fault), and the whole-message body-part uniqueness.
|
|
644
|
+
function _finishBody(body, where) {
|
|
645
|
+
for (var i = 0; i < body.controls.length; i++) assertControlPlacement(body.controls[i], where);
|
|
646
|
+
body.statuses = _collectStatuses(body.controls);
|
|
647
|
+
body.unhandled = body.controls.filter(function (c) {
|
|
648
|
+
return c.attrType !== OID_STATUS_INFO && c.attrType !== OID_STATUS_INFO_V2 && c.attrName === null;
|
|
649
|
+
});
|
|
650
|
+
// RFC 5272 sec. 3.2.2 scopes uniqueness to "each element of a PKIData or
|
|
651
|
+
// PKIResponse", and FOUR element kinds carry an identity. Enumerated here in
|
|
652
|
+
// one place so the rule cannot hold for some of them and quietly lapse for the
|
|
653
|
+
// rest: controls (sec. 3.2.1.1), requests (sec. 3.2.1.2 -- the tcr/orm
|
|
654
|
+
// bodyPartID and the crm arm's certReqId), content infos (sec. 3.2.1.3) and
|
|
655
|
+
// other messages (sec. 3.2.1.4).
|
|
656
|
+
var ids = [];
|
|
657
|
+
function collect(list) { for (var j = 0; j < list.length; j++) ids.push(list[j].bodyPartID); }
|
|
658
|
+
collect(body.controls);
|
|
659
|
+
collect(body.requests);
|
|
660
|
+
collect(body.cmsSequence);
|
|
661
|
+
collect(body.otherMsgs);
|
|
662
|
+
assertUniqueBodyPartIds(ids);
|
|
663
|
+
return body;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* @primitive pki.schema.cmc.parsePkiData
|
|
668
|
+
* @signature pki.schema.cmc.parsePkiData(der) -> parsed
|
|
669
|
+
* @since 0.4.16
|
|
670
|
+
* @status experimental
|
|
671
|
+
* @spec RFC 5272 sec. 3.2.1
|
|
672
|
+
* @related pki.schema.cmc.parse, pki.schema.cmc.parsePkiResponse
|
|
673
|
+
*
|
|
674
|
+
* Decode a bare `PKIData` body (the encapsulated content of a Full PKI Request),
|
|
675
|
+
* without the CMS layer. All FOUR sequences are mandatory though each may be
|
|
676
|
+
* empty, so a message that simply omits a trailing empty one is malformed rather
|
|
677
|
+
* than shorthand. `reqSequenceBytes` is the raw `reqSequence` TLV, tag and length
|
|
678
|
+
* included, because the Identity Proof V2 witness is computed over exactly those
|
|
679
|
+
* bytes (RFC 5272 sec. 6.2.1).
|
|
680
|
+
*
|
|
681
|
+
* @example
|
|
682
|
+
* var b = pki.asn1.build;
|
|
683
|
+
* var der = b.sequence([b.sequence([]), b.sequence([]), b.sequence([]), b.sequence([])]);
|
|
684
|
+
* var d = pki.schema.cmc.parsePkiData(der);
|
|
685
|
+
* d.controls.length; // 0 -- present and empty, which is legal
|
|
686
|
+
*/
|
|
687
|
+
function parsePkiData(input) {
|
|
688
|
+
var root = asn1.decode(Buffer.isBuffer(input) ? input : Buffer.from(input));
|
|
689
|
+
var body = schema.walk(PKI_DATA, root, NS).result;
|
|
690
|
+
body.bytes = root.bytes;
|
|
691
|
+
return _finishBody(body, "pkiData");
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* @primitive pki.schema.cmc.parsePkiResponse
|
|
696
|
+
* @signature pki.schema.cmc.parsePkiResponse(der) -> parsed
|
|
697
|
+
* @since 0.4.16
|
|
698
|
+
* @status experimental
|
|
699
|
+
* @spec RFC 5272 sec. 4.2.1
|
|
700
|
+
* @related pki.schema.cmc.parse, pki.schema.cmc.parsePkiData
|
|
701
|
+
*
|
|
702
|
+
* Decode a bare `PKIResponse` body. A PKIResponse has THREE sequences -- there is
|
|
703
|
+
* no `reqSequence` -- and all three are mandatory though each may be empty. The
|
|
704
|
+
* status controls are collected in wire order; a response may legitimately carry
|
|
705
|
+
* several, and carrying NONE means success (RFC 5272 sec. 6.1.2).
|
|
706
|
+
*
|
|
707
|
+
* @example
|
|
708
|
+
* var b = pki.asn1.build;
|
|
709
|
+
* var der = b.sequence([b.sequence([]), b.sequence([]), b.sequence([])]);
|
|
710
|
+
* var r = pki.schema.cmc.parsePkiResponse(der);
|
|
711
|
+
* r.statuses.length; // 0 -- no status control means success is assumed
|
|
712
|
+
*/
|
|
713
|
+
function parsePkiResponse(input) {
|
|
714
|
+
var root = asn1.decode(Buffer.isBuffer(input) ? input : Buffer.from(input));
|
|
715
|
+
var body = schema.walk(PKI_RESPONSE, root, NS).result;
|
|
716
|
+
body.bytes = root.bytes;
|
|
717
|
+
return _finishBody(body, "pkiResponse");
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* @primitive pki.schema.cmc.parse
|
|
722
|
+
* @signature pki.schema.cmc.parse(input) -> parsed
|
|
723
|
+
* @since 0.4.16
|
|
724
|
+
* @status experimental
|
|
725
|
+
* @spec RFC 5272, RFC 6402
|
|
726
|
+
* @defends cmc-status-confusion (CWE-20)
|
|
727
|
+
* @related pki.schema.cmc.parsePkiData, pki.schema.cmc.parsePkiResponse, pki.cms.verify
|
|
728
|
+
*
|
|
729
|
+
* Decode a CMC Full PKI Request or Full PKI Response from its CMS carrier. `input`
|
|
730
|
+
* is DER, a PEM `CMS` block, or an already-parsed `pki.schema.cms` object. The CMS
|
|
731
|
+
* layer is peeled, the encapsulated content type selects the body
|
|
732
|
+
* (`id-cct-PKIData` -> a request, `id-cct-PKIResponse` -> a response), and any
|
|
733
|
+
* other content type is refused as not-CMC rather than guessed at.
|
|
734
|
+
*
|
|
735
|
+
* The parsed CMS is returned on `cms` so a caller can verify the signature -- this
|
|
736
|
+
* decoder never does: reading a message and trusting it are separate steps, and
|
|
737
|
+
* RFC 5272 sec. 3.2.1.3.4 makes the signature check the caller's obligation.
|
|
738
|
+
*
|
|
739
|
+
* @example
|
|
740
|
+
* var b = pki.asn1.build;
|
|
741
|
+
* var body = b.sequence([b.sequence([]), b.sequence([]), b.sequence([]), b.sequence([])]);
|
|
742
|
+
* var encap = b.sequence([b.oid(pki.oid.byName("id-cct-PKIData")), b.explicit(0, b.octetString(body))]);
|
|
743
|
+
* var sd = b.sequence([b.integer(3n), b.set([]), encap, b.set([])]);
|
|
744
|
+
* var der = b.sequence([b.oid("1.2.840.113549.1.7.2"), b.explicit(0, sd)]);
|
|
745
|
+
* pki.schema.cmc.parse(der).kind; // "pkiData"
|
|
746
|
+
*/
|
|
747
|
+
function parse(input) {
|
|
748
|
+
// Bytes or an already-parsed CMS? Decided by a POSITIVE marker of the parsed
|
|
749
|
+
// shape, never by "it is an object" -- a Uint8Array is an object too, so the
|
|
750
|
+
// looser test routes perfectly good DER to the parsed branch, where it has no
|
|
751
|
+
// content type and is refused as not-CMC while the identical Buffer parses.
|
|
752
|
+
// encapContentInfo is the field this function goes on to read, so requiring it
|
|
753
|
+
// is the same question the code below already asks.
|
|
754
|
+
var parsedCms = (input && typeof input === "object" && !ArrayBuffer.isView(input) &&
|
|
755
|
+
!(input instanceof ArrayBuffer) && input.encapContentInfo != null)
|
|
756
|
+
? input : cms.parse(input);
|
|
757
|
+
// The CARRIER is part of the identity, not just the encapsulated content type.
|
|
758
|
+
// RFC 5272 sec. 3.2 / 4.2 admit a SignedData or an AuthenticatedData; other CMS
|
|
759
|
+
// types also expose an encapContentInfo (CompressedData does), and dispatching
|
|
760
|
+
// on the inner OID alone would accept an unsigned, unauthenticated wrapper as a
|
|
761
|
+
// Full PKI message.
|
|
762
|
+
if (CMC_CARRIERS[parsedCms.contentTypeName] !== 1) {
|
|
763
|
+
throw E("cmc/not-cmc",
|
|
764
|
+
"a Full PKI Request / Response is carried in a CMS SignedData or AuthenticatedData, got " +
|
|
765
|
+
(parsedCms.contentTypeName || "an unnamed content type") + " (RFC 5272 sec. 3.2 / 4.2)");
|
|
766
|
+
}
|
|
767
|
+
var encap = parsedCms.encapContentInfo;
|
|
768
|
+
if (!encap) throw E("cmc/not-cmc", "a CMC message is carried in a CMS SignedData or AuthenticatedData");
|
|
769
|
+
var eContentType = encap.eContentType;
|
|
770
|
+
if (eContentType !== OID_PKI_DATA && eContentType !== OID_PKI_RESPONSE) {
|
|
771
|
+
throw E("cmc/not-cmc",
|
|
772
|
+
"the encapsulated content type is " + (oid.name(eContentType) || eContentType) +
|
|
773
|
+
", not id-cct-PKIData or id-cct-PKIResponse (RFC 5272 sec. 3.2 / 4.2)");
|
|
774
|
+
}
|
|
775
|
+
if (encap.eContent == null) {
|
|
776
|
+
throw E("cmc/no-content",
|
|
777
|
+
"a Full PKI Request / Response carries its body as the encapsulated content; this SignedData is detached (RFC 5272 sec. 3.2)");
|
|
778
|
+
}
|
|
779
|
+
var body = eContentType === OID_PKI_DATA ? parsePkiData(encap.eContent) : parsePkiResponse(encap.eContent);
|
|
780
|
+
body.cms = parsedCms;
|
|
781
|
+
body.eContentType = eContentType;
|
|
782
|
+
return body;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
module.exports = {
|
|
786
|
+
parse: parse,
|
|
787
|
+
parsePkiData: parsePkiData,
|
|
788
|
+
parsePkiResponse: parsePkiResponse,
|
|
789
|
+
STATUS_BY_VALUE: STATUS_BY_VALUE,
|
|
790
|
+
FAIL_INFO_BY_VALUE: FAIL_INFO_BY_VALUE,
|
|
791
|
+
};
|