@blamejs/pki 0.1.17 → 0.1.19
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 +45 -1
- package/README.md +9 -7
- package/lib/asn1-der.js +109 -22
- package/lib/constants.js +16 -0
- package/lib/framework-error.js +14 -0
- package/lib/oid.js +48 -2
- package/lib/path-validate.js +14 -2
- package/lib/schema-all.js +45 -8
- package/lib/schema-attrcert.js +6 -6
- package/lib/schema-cmp.js +928 -0
- package/lib/schema-cms.js +20 -13
- package/lib/schema-crl.js +2 -2
- package/lib/schema-crmf.js +63 -29
- package/lib/schema-csr.js +4 -4
- package/lib/schema-engine.js +114 -8
- package/lib/schema-ocsp.js +10 -14
- package/lib/schema-pkcs12.js +614 -0
- package/lib/schema-pkcs8.js +16 -7
- package/lib/schema-pkix.js +40 -15
- package/lib/schema-tsp.js +11 -18
- package/lib/schema-x509.js +3 -3
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -0,0 +1,928 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @module pki.schema.cmp
|
|
6
|
+
* @nav Schema
|
|
7
|
+
* @title CMP
|
|
8
|
+
* @order 200
|
|
9
|
+
* @slug cmp
|
|
10
|
+
*
|
|
11
|
+
* @intro
|
|
12
|
+
* Certificate Management Protocol handling per RFC 9810 (which obsoletes
|
|
13
|
+
* RFC 4210 and RFC 9480). `parse` decodes a `PKIMessage` — the protected
|
|
14
|
+
* transport envelope CMP enrollment, revocation, confirmation, and support
|
|
15
|
+
* exchanges ride — into its header (version, sender / recipient
|
|
16
|
+
* GeneralNames including the NULL-DN anonymous form, nonces, transaction
|
|
17
|
+
* id, free text, general info), its body, its protection bits, and its
|
|
18
|
+
* extra certificates.
|
|
19
|
+
*
|
|
20
|
+
* The body is a 27-arm tagged CHOICE: certificate-request arms (`ir`,
|
|
21
|
+
* `cr`, `kur`, `krr`, `ccr`) are RFC 4211 CertReqMessages decoded by the
|
|
22
|
+
* CRMF parser; response, revocation, confirmation, error, general-message,
|
|
23
|
+
* and polling arms decode structurally (an encrypted certificate's
|
|
24
|
+
* EnvelopedData decodes via the CMS parser); every other defined arm is
|
|
25
|
+
* recognized and surfaced raw — `nested` messages are never auto-recursed.
|
|
26
|
+
* Protection is surfaced, not verified: the exact `headerBytes` and
|
|
27
|
+
* `bodyBytes` wire slices are exposed so an external verifier reconstructs
|
|
28
|
+
* the virtual ProtectedPart `SEQUENCE { header, body }` and checks the MAC
|
|
29
|
+
* or signature itself. DER-only, fail-closed.
|
|
30
|
+
*
|
|
31
|
+
* @card
|
|
32
|
+
* Parse DER / PEM RFC 9810 CMP PKIMessages — header, 27-arm body (requests
|
|
33
|
+
* via CRMF, encrypted certs via CMS, the rest structural or raw),
|
|
34
|
+
* protection inputs surfaced byte-exact for external verification,
|
|
35
|
+
* fail-closed.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
var asn1 = require("./asn1-der.js");
|
|
39
|
+
var oid = require("./oid.js");
|
|
40
|
+
var schema = require("./schema-engine.js");
|
|
41
|
+
var pkix = require("./schema-pkix.js");
|
|
42
|
+
var cms = require("./schema-cms.js");
|
|
43
|
+
var crmf = require("./schema-crmf.js");
|
|
44
|
+
var frameworkError = require("./framework-error.js");
|
|
45
|
+
|
|
46
|
+
var CmpError = frameworkError.CmpError;
|
|
47
|
+
var PemError = frameworkError.PemError;
|
|
48
|
+
var NS = pkix.makeNS("cmp", CmpError, oid);
|
|
49
|
+
var TAGS = asn1.TAGS;
|
|
50
|
+
|
|
51
|
+
var OID_IMPLICIT_CONFIRM = oid.byName("implicitConfirm");
|
|
52
|
+
var OID_CONFIRM_WAIT_TIME = oid.byName("confirmWaitTime");
|
|
53
|
+
var OID_CERT_PROFILE = oid.byName("certProfile");
|
|
54
|
+
var OID_PKI_ARCHIVE_OPTIONS = oid.byName("pkiArchiveOptions");
|
|
55
|
+
|
|
56
|
+
// ---- leaves -----------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
// CMP times are GeneralizedTime only (RFC 9810 §5.1.1).
|
|
59
|
+
var GENERALIZED_TIME = schema.decode(function (n, ctx) {
|
|
60
|
+
if (n.tagClass !== "universal" || n.tagNumber !== TAGS.GENERALIZED_TIME) {
|
|
61
|
+
throw ctx.E("cmp/bad-time", "CMP times must be GeneralizedTime (RFC 9810 §5.1.1)");
|
|
62
|
+
}
|
|
63
|
+
return asn1.read.time(n);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// A PKIFreeText element MUST be a UTF8String (§5.1.1).
|
|
67
|
+
var UTF8_TEXT = schema.decode(function (n, ctx) {
|
|
68
|
+
if (n.tagClass !== "universal" || n.tagNumber !== TAGS.UTF8_STRING) {
|
|
69
|
+
throw ctx.E("cmp/bad-freetext", "PKIFreeText elements must be UTF8String (RFC 9810 §5.1.1)");
|
|
70
|
+
}
|
|
71
|
+
return asn1.read.string(n);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// PKIFreeText ::= SEQUENCE SIZE (1..MAX) OF UTF8String.
|
|
75
|
+
var PKI_FREE_TEXT = schema.seqOf(UTF8_TEXT, {
|
|
76
|
+
min: 1, code: "cmp/bad-freetext", what: "PKIFreeText",
|
|
77
|
+
build: function (m) { return m.items.map(function (it) { return it.value; }); },
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// PKIStatus ::= INTEGER {0..6} (§5.2.3) — a value whitelist, surfaced named.
|
|
81
|
+
var PKI_STATUS_NAMES = ["accepted", "grantedWithMods", "rejection", "waiting",
|
|
82
|
+
"revocationWarning", "revocationNotification", "keyUpdateWarning"];
|
|
83
|
+
var PKI_STATUS = schema.decode(function (n, ctx) {
|
|
84
|
+
var v = asn1.read.integer(n);
|
|
85
|
+
if (v < 0n || v > 6n) throw ctx.E("cmp/bad-status", "undefined PKIStatus " + v.toString() + " (RFC 9810 §5.2.3)");
|
|
86
|
+
var code = Number(v);
|
|
87
|
+
return { code: code, name: PKI_STATUS_NAMES[code] };
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// PKIFailureInfo ::= BIT STRING named bits 0..26 (§5.2.3). X.690 §11.2.2:
|
|
91
|
+
// a named-bit BIT STRING drops trailing zero bits, so the final carried bit
|
|
92
|
+
// must be 1. Bits past 26 are extensible and surface as numeric indexes.
|
|
93
|
+
var FAIL_INFO_NAMES = ["badAlg", "badMessageCheck", "badRequest", "badTime", "badCertId",
|
|
94
|
+
"badDataFormat", "wrongAuthority", "incorrectData", "missingTimeStamp", "badPOP",
|
|
95
|
+
"certRevoked", "certConfirmed", "wrongIntegrity", "badRecipientNonce", "timeNotAvailable",
|
|
96
|
+
"unacceptedPolicy", "unacceptedExtension", "addInfoNotAvailable", "badSenderNonce",
|
|
97
|
+
"badCertTemplate", "signerNotTrusted", "transactionIdInUse", "unsupportedVersion",
|
|
98
|
+
"notAuthorized", "systemUnavail", "systemFailure", "duplicateCertReq"];
|
|
99
|
+
var PKI_FAILURE_INFO = schema.decode(function (n, ctx) {
|
|
100
|
+
var bs = asn1.read.bitString(n);
|
|
101
|
+
var total = bs.bytes.length * 8 - bs.unusedBits;
|
|
102
|
+
schema.assertMinimalNamedBits(bs.unusedBits, bs.bytes, function (m) { throw ctx.E("cmp/bad-fail-info", m); });
|
|
103
|
+
var bits = [];
|
|
104
|
+
for (var i = 0; i < total; i++) {
|
|
105
|
+
if ((bs.bytes[i >> 3] >> (7 - (i % 8))) & 1) bits.push(i < FAIL_INFO_NAMES.length ? FAIL_INFO_NAMES[i] : i);
|
|
106
|
+
}
|
|
107
|
+
return { bits: bits, raw: bs };
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// PollRep checkAfter is a delay in seconds — negative poisons scheduling, and
|
|
111
|
+
// the value surfaces as an exact number or not at all.
|
|
112
|
+
var CHECK_AFTER = schema.decode(function (n, ctx) {
|
|
113
|
+
var v = asn1.read.integer(n);
|
|
114
|
+
if (v < 0n || v > 2147483647n) throw ctx.E("cmp/bad-poll-rep", "checkAfter must be a non-negative delay in seconds (RFC 9810 §5.3.22)");
|
|
115
|
+
return Number(v);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// A raw certificate / CRL / publication-info element — surfaced byte-exact for
|
|
119
|
+
// downstream parsing, but structurally a universal SEQUENCE: a CMPCertificate
|
|
120
|
+
// is a Certificate, and CertificateList / PKIPublicationInfo are SEQUENCEs, so
|
|
121
|
+
// a primitive (an INTEGER) or wrong-tag element is not a valid structure and
|
|
122
|
+
// rejects rather than handing arbitrary bytes to certificate processing. The
|
|
123
|
+
// `code` names the containing structure (a shared leaf cannot know its context).
|
|
124
|
+
function rawSequence(code) {
|
|
125
|
+
return schema.decode(function (n, ctx) {
|
|
126
|
+
// A Certificate, CertificateList, and PKIPublicationInfo are each a
|
|
127
|
+
// NON-EMPTY universal SEQUENCE — an empty SEQUENCE (0x30 0x00) has the
|
|
128
|
+
// right tag but is none of them, so require at least one child rather than
|
|
129
|
+
// surface a degenerate structure as certificate/CRL bytes.
|
|
130
|
+
if (!(n.tagClass === "universal" && n.tagNumber === TAGS.SEQUENCE && n.children && n.children.length >= 1)) {
|
|
131
|
+
throw ctx.E(code, "expected a non-empty universal SEQUENCE (Certificate / CertificateList / PKIPublicationInfo)");
|
|
132
|
+
}
|
|
133
|
+
return n.bytes;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
var GENERAL_NAME = pkix.generalName(NS, { decodeValue: true });
|
|
138
|
+
var ALG_ID = pkix.algorithmIdentifier(NS);
|
|
139
|
+
var EXTENSIONS = pkix.extensions(NS);
|
|
140
|
+
|
|
141
|
+
// ---- header ------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
// InfoTypeAndValue ::= SEQUENCE { infoType OID, infoValue ANY OPTIONAL }
|
|
144
|
+
// (§5.1.1, §5.3.19). Recognized id-it types with a fixed value syntax are
|
|
145
|
+
// validated (the CMS signed-attr value-syntax posture); all others surface
|
|
146
|
+
// raw with the registry name resolving.
|
|
147
|
+
var INFO_TYPE_AND_VALUE = schema.seq([
|
|
148
|
+
schema.field("infoType", schema.oidLeaf()),
|
|
149
|
+
schema.optional("infoValue", schema.any(), { whenAny: true }),
|
|
150
|
+
], {
|
|
151
|
+
assert: "sequence", code: "cmp/bad-info-type-and-value", what: "InfoTypeAndValue",
|
|
152
|
+
build: function (m, ctx) {
|
|
153
|
+
var t = m.fields.infoType.value;
|
|
154
|
+
var valueNode = m.fields.infoValue.present ? m.fields.infoValue.value : null;
|
|
155
|
+
// A recognized id-it type with a FIXED value syntax (RFC 9810 §5.1.1) MUST
|
|
156
|
+
// carry its infoValue — implicitConfirm a NULL, confirmWaitTime a
|
|
157
|
+
// GeneralizedTime, certProfile a SEQUENCE OF UTF8String. A missing value is
|
|
158
|
+
// accepted only for the open / query id-it types (a bare OID in a genm body),
|
|
159
|
+
// never for a type whose value syntax the RFC defines.
|
|
160
|
+
if ((t === OID_IMPLICIT_CONFIRM || t === OID_CONFIRM_WAIT_TIME || t === OID_CERT_PROFILE) && valueNode === null) {
|
|
161
|
+
throw ctx.E("cmp/bad-info-value", "a recognized fixed-syntax id-it (implicitConfirm / confirmWaitTime / certProfile) must carry its infoValue (RFC 9810 §5.1.1)");
|
|
162
|
+
}
|
|
163
|
+
if (valueNode !== null) {
|
|
164
|
+
// A recognized id-it value is validated by CONTENT, not just tag: run the
|
|
165
|
+
// strict typed reader so a well-tagged but malformed payload (a non-empty
|
|
166
|
+
// NULL, a GeneralizedTime carrying garbage, an invalid-UTF-8 string)
|
|
167
|
+
// rejects rather than being surfaced as valid CMP.
|
|
168
|
+
if (t === OID_IMPLICIT_CONFIRM) {
|
|
169
|
+
if (!(valueNode.tagClass === "universal" && valueNode.tagNumber === TAGS.NULL)) {
|
|
170
|
+
throw ctx.E("cmp/bad-info-value", "an implicitConfirm value must be NULL (RFC 9810 §5.1.1.1)");
|
|
171
|
+
}
|
|
172
|
+
try { asn1.read.nullValue(valueNode); }
|
|
173
|
+
catch (e) { throw ctx.E("cmp/bad-info-value", "an implicitConfirm value must be an empty NULL (RFC 9810 §5.1.1.1)", e); }
|
|
174
|
+
}
|
|
175
|
+
if (t === OID_CONFIRM_WAIT_TIME) {
|
|
176
|
+
if (!(valueNode.tagClass === "universal" && valueNode.tagNumber === TAGS.GENERALIZED_TIME)) {
|
|
177
|
+
throw ctx.E("cmp/bad-info-value", "a confirmWaitTime value must be a GeneralizedTime (RFC 9810 §5.1.1.2)");
|
|
178
|
+
}
|
|
179
|
+
try { asn1.read.time(valueNode); }
|
|
180
|
+
catch (e) { throw ctx.E("cmp/bad-info-value", "a confirmWaitTime GeneralizedTime is malformed (RFC 9810 §5.1.1.2)", e); }
|
|
181
|
+
}
|
|
182
|
+
if (t === OID_CERT_PROFILE) {
|
|
183
|
+
if (!(valueNode.tagClass === "universal" && valueNode.tagNumber === TAGS.SEQUENCE &&
|
|
184
|
+
valueNode.children && valueNode.children.length >= 1)) {
|
|
185
|
+
throw ctx.E("cmp/bad-info-value", "a certProfile value must be a non-empty SEQUENCE OF UTF8String (RFC 9810 §5.1.1.4)");
|
|
186
|
+
}
|
|
187
|
+
for (var i = 0; i < valueNode.children.length; i++) {
|
|
188
|
+
var el = valueNode.children[i];
|
|
189
|
+
if (!(el.tagClass === "universal" && el.tagNumber === TAGS.UTF8_STRING)) {
|
|
190
|
+
throw ctx.E("cmp/bad-info-value", "a certProfile element must be a UTF8String (RFC 9810 §5.1.1.4)");
|
|
191
|
+
}
|
|
192
|
+
try { asn1.read.string(el); }
|
|
193
|
+
catch (e) { throw ctx.E("cmp/bad-info-value", "a certProfile element must be a valid UTF8String (RFC 9810 §5.1.1.4)", e); }
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return { type: t, name: ctx.oid.name(t) || null, value: valueNode === null ? null : valueNode.bytes };
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
var GENERAL_INFO = schema.seqOf(INFO_TYPE_AND_VALUE, {
|
|
202
|
+
min: 1, code: "cmp/bad-general-info", what: "generalInfo",
|
|
203
|
+
build: function (m) { return m.items.map(function (it) { return it.value.result; }); },
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// PKIHeader ::= SEQUENCE { pvno, sender, recipient, then EXPLICIT [0]..[8]
|
|
207
|
+
// optionals, ascending, at-most-once } (§5.1.1).
|
|
208
|
+
var PKI_HEADER = schema.seq([
|
|
209
|
+
schema.field("pvno", pkix.versionReader(NS, { "1": 1, "2": 2, "3": 3 })),
|
|
210
|
+
schema.field("sender", GENERAL_NAME),
|
|
211
|
+
schema.field("recipient", GENERAL_NAME),
|
|
212
|
+
schema.trailing([
|
|
213
|
+
{ tag: 0, name: "messageTime", schema: GENERALIZED_TIME, explicit: true, emptyCode: "cmp/bad-header" },
|
|
214
|
+
{ tag: 1, name: "protectionAlg", schema: ALG_ID, explicit: true, emptyCode: "cmp/bad-header" },
|
|
215
|
+
{ tag: 2, name: "senderKID", schema: schema.octetString(), explicit: true, emptyCode: "cmp/bad-header" },
|
|
216
|
+
{ tag: 3, name: "recipKID", schema: schema.octetString(), explicit: true, emptyCode: "cmp/bad-header" },
|
|
217
|
+
{ tag: 4, name: "transactionID", schema: schema.octetString(), explicit: true, emptyCode: "cmp/bad-header" },
|
|
218
|
+
{ tag: 5, name: "senderNonce", schema: schema.octetString(), explicit: true, emptyCode: "cmp/bad-header" },
|
|
219
|
+
{ tag: 6, name: "recipNonce", schema: schema.octetString(), explicit: true, emptyCode: "cmp/bad-header" },
|
|
220
|
+
{ tag: 7, name: "freeText", schema: PKI_FREE_TEXT, explicit: true, emptyCode: "cmp/bad-header" },
|
|
221
|
+
{ tag: 8, name: "generalInfo", schema: GENERAL_INFO, explicit: true, emptyCode: "cmp/bad-header" },
|
|
222
|
+
], { minTag: 0, maxTag: 8, unexpectedCode: "cmp/bad-header", orderCode: "cmp/bad-header" }),
|
|
223
|
+
], {
|
|
224
|
+
assert: "sequence", code: "cmp/bad-header", what: "PKIHeader",
|
|
225
|
+
build: function (m) {
|
|
226
|
+
var f = m.fields;
|
|
227
|
+
return {
|
|
228
|
+
pvno: f.pvno.value,
|
|
229
|
+
sender: f.sender.value,
|
|
230
|
+
recipient: f.recipient.value,
|
|
231
|
+
messageTime: f.messageTime.present ? f.messageTime.value : null,
|
|
232
|
+
protectionAlg: f.protectionAlg.present ? f.protectionAlg.value.result : null,
|
|
233
|
+
senderKID: f.senderKID.present ? f.senderKID.value : null,
|
|
234
|
+
recipKID: f.recipKID.present ? f.recipKID.value : null,
|
|
235
|
+
transactionID: f.transactionID.present ? f.transactionID.value : null,
|
|
236
|
+
senderNonce: f.senderNonce.present ? f.senderNonce.value : null,
|
|
237
|
+
recipNonce: f.recipNonce.present ? f.recipNonce.value : null,
|
|
238
|
+
freeText: f.freeText.present ? f.freeText.value.result : null,
|
|
239
|
+
generalInfo: f.generalInfo.present ? f.generalInfo.value.result : null,
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// ---- decoded body arms ---------------------------------------------------
|
|
245
|
+
|
|
246
|
+
// PKIStatusInfo ::= SEQUENCE { status, statusString OPTIONAL, failInfo OPTIONAL } (§5.2.3).
|
|
247
|
+
var PKI_STATUS_INFO = schema.seq([
|
|
248
|
+
schema.field("status", PKI_STATUS),
|
|
249
|
+
schema.optional("statusString", PKI_FREE_TEXT, { whenUniversal: [TAGS.SEQUENCE] }),
|
|
250
|
+
schema.optional("failInfo", PKI_FAILURE_INFO, { whenUniversal: [TAGS.BIT_STRING] }),
|
|
251
|
+
], {
|
|
252
|
+
assert: "sequence", code: "cmp/bad-status-info", what: "PKIStatusInfo",
|
|
253
|
+
build: function (m) {
|
|
254
|
+
return {
|
|
255
|
+
status: m.fields.status.value,
|
|
256
|
+
statusString: m.fields.statusString.present ? m.fields.statusString.value.result : null,
|
|
257
|
+
failInfo: m.fields.failInfo.present ? m.fields.failInfo.value : null,
|
|
258
|
+
};
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// ErrorMsgContent ::= SEQUENCE { pKIStatusInfo, errorCode INTEGER OPTIONAL,
|
|
263
|
+
// errorDetails PKIFreeText OPTIONAL } (§5.3.21).
|
|
264
|
+
var ERROR_MSG_CONTENT = schema.seq([
|
|
265
|
+
schema.field("pKIStatusInfo", PKI_STATUS_INFO),
|
|
266
|
+
schema.optional("errorCode", schema.integerLeaf(), { whenUniversal: [TAGS.INTEGER] }),
|
|
267
|
+
schema.optional("errorDetails", PKI_FREE_TEXT, { whenUniversal: [TAGS.SEQUENCE] }),
|
|
268
|
+
], {
|
|
269
|
+
assert: "sequence", code: "cmp/bad-error", what: "ErrorMsgContent",
|
|
270
|
+
build: function (m) {
|
|
271
|
+
return {
|
|
272
|
+
pKIStatusInfo: m.fields.pKIStatusInfo.value.result,
|
|
273
|
+
errorCode: m.fields.errorCode.present ? m.fields.errorCode.value : null,
|
|
274
|
+
errorDetails: m.fields.errorDetails.present ? m.fields.errorDetails.value.result : null,
|
|
275
|
+
};
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// CertStatus ::= SEQUENCE { certHash, certReqId, statusInfo OPTIONAL,
|
|
280
|
+
// hashAlg [0] OPTIONAL } (§5.3.18). certReqId accepts the -1 sentinel.
|
|
281
|
+
var CERT_STATUS = schema.seq([
|
|
282
|
+
schema.field("certHash", schema.octetString()),
|
|
283
|
+
schema.field("certReqId", schema.integerLeaf()),
|
|
284
|
+
schema.optional("statusInfo", PKI_STATUS_INFO, { whenUniversal: [TAGS.SEQUENCE] }),
|
|
285
|
+
schema.trailing([
|
|
286
|
+
{ tag: 0, name: "hashAlg", schema: ALG_ID, explicit: true, emptyCode: "cmp/bad-cert-status" },
|
|
287
|
+
], { minTag: 0, maxTag: 0, unexpectedCode: "cmp/bad-cert-status", orderCode: "cmp/bad-cert-status" }),
|
|
288
|
+
], {
|
|
289
|
+
assert: "sequence", code: "cmp/bad-cert-status", what: "CertStatus",
|
|
290
|
+
build: function (m) {
|
|
291
|
+
return {
|
|
292
|
+
certHash: m.fields.certHash.value,
|
|
293
|
+
certReqId: m.fields.certReqId.value,
|
|
294
|
+
statusInfo: m.fields.statusInfo.present ? m.fields.statusInfo.value.result : null,
|
|
295
|
+
hashAlg: m.fields.hashAlg.present ? m.fields.hashAlg.value.result : null,
|
|
296
|
+
};
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// CertConfirmContent ::= SEQUENCE OF CertStatus — an EMPTY sequence is legal
|
|
301
|
+
// (reject-all, §5.3.18), so no SIZE floor.
|
|
302
|
+
var CERT_CONFIRM_CONTENT = schema.seqOf(CERT_STATUS, {
|
|
303
|
+
code: "cmp/bad-cert-status", what: "CertConfirmContent",
|
|
304
|
+
build: function (m) { return m.items.map(function (it) { return it.value.result; }); },
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// GenMsgContent / GenRepContent ::= SEQUENCE OF InfoTypeAndValue — no SIZE
|
|
308
|
+
// bound, empty legal (§5.3.19/.20).
|
|
309
|
+
var GEN_MSG_CONTENT = schema.seqOf(INFO_TYPE_AND_VALUE, {
|
|
310
|
+
code: "cmp/bad-info-type-and-value", what: "GenMsgContent",
|
|
311
|
+
build: function (m) { return m.items.map(function (it) { return it.value.result; }); },
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// RevDetails ::= SEQUENCE { certDetails CertTemplate, crlEntryDetails
|
|
315
|
+
// Extensions OPTIONAL } (§5.3.9) — the CertTemplate interior (an IMPLICIT
|
|
316
|
+
// TAGS module) is owned by the CRMF parser and walked NS-bound there.
|
|
317
|
+
var CERT_TEMPLATE_LEAF = schema.decode(function (n) { return crmf.walkCertTemplate(n); });
|
|
318
|
+
var REV_DETAILS = schema.seq([
|
|
319
|
+
schema.field("certDetails", CERT_TEMPLATE_LEAF),
|
|
320
|
+
schema.optional("crlEntryDetails", EXTENSIONS, { whenUniversal: [TAGS.SEQUENCE] }),
|
|
321
|
+
], {
|
|
322
|
+
assert: "sequence", code: "cmp/bad-rev-req", what: "RevDetails",
|
|
323
|
+
build: function (m) {
|
|
324
|
+
return {
|
|
325
|
+
certDetails: m.fields.certDetails.value,
|
|
326
|
+
crlEntryDetails: m.fields.crlEntryDetails.present ? m.fields.crlEntryDetails.value.result : null,
|
|
327
|
+
};
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
var REV_REQ_CONTENT = schema.seqOf(REV_DETAILS, {
|
|
331
|
+
code: "cmp/bad-rev-req", what: "RevReqContent",
|
|
332
|
+
build: function (m) { return m.items.map(function (it) { return it.value.result; }); },
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// CertId ::= SEQUENCE { issuer GeneralName, serialNumber INTEGER } (RFC 4211).
|
|
336
|
+
var CERT_ID = schema.seq([
|
|
337
|
+
schema.field("issuer", GENERAL_NAME),
|
|
338
|
+
schema.field("serialNumber", schema.integerLeaf()),
|
|
339
|
+
], {
|
|
340
|
+
assert: "sequence", code: "cmp/bad-rev-rep", what: "CertId",
|
|
341
|
+
build: function (m) {
|
|
342
|
+
return { issuer: m.fields.issuer.value, serialNumber: m.fields.serialNumber.value };
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// RevRepContent ::= SEQUENCE { status SIZE(1..MAX), revCerts [0] OPTIONAL,
|
|
347
|
+
// crls [1] OPTIONAL } (§5.3.10).
|
|
348
|
+
var REV_REP_CONTENT = schema.seq([
|
|
349
|
+
schema.field("status", schema.seqOf(PKI_STATUS_INFO, {
|
|
350
|
+
min: 1, code: "cmp/bad-rev-rep", what: "RevRep status",
|
|
351
|
+
build: function (m) { return m.items.map(function (it) { return it.value.result; }); },
|
|
352
|
+
})),
|
|
353
|
+
schema.trailing([
|
|
354
|
+
{ tag: 0, name: "revCerts", schema: schema.seqOf(CERT_ID, {
|
|
355
|
+
min: 1, code: "cmp/bad-rev-rep", what: "revCerts",
|
|
356
|
+
build: function (m) { return m.items.map(function (it) { return it.value.result; }); },
|
|
357
|
+
}), explicit: true, emptyCode: "cmp/bad-rev-rep" },
|
|
358
|
+
{ tag: 1, name: "crls", schema: schema.seqOf(rawSequence("cmp/bad-rev-rep"), {
|
|
359
|
+
min: 1, code: "cmp/bad-rev-rep", what: "crls",
|
|
360
|
+
build: function (m) { return m.items.map(function (it) { return it.value; }); },
|
|
361
|
+
}), explicit: true, emptyCode: "cmp/bad-rev-rep" },
|
|
362
|
+
], { minTag: 0, maxTag: 1, unexpectedCode: "cmp/bad-rev-rep", orderCode: "cmp/bad-rev-rep" }),
|
|
363
|
+
], {
|
|
364
|
+
assert: "sequence", code: "cmp/bad-rev-rep", what: "RevRepContent",
|
|
365
|
+
build: function (m) {
|
|
366
|
+
return {
|
|
367
|
+
status: m.fields.status.value.result,
|
|
368
|
+
revCerts: m.fields.revCerts.present ? m.fields.revCerts.value.result : null,
|
|
369
|
+
crls: m.fields.crls.present ? m.fields.crls.value.result : null,
|
|
370
|
+
};
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// EncryptedKey ::= CHOICE { encryptedValue EncryptedValue (deprecated),
|
|
375
|
+
// envelopedData [0] EnvelopedData } (§5.2.2). EncryptedKey is imported from
|
|
376
|
+
// the RFC 4211 CRMF module, which is IMPLICIT TAGS, so `envelopedData [0]` is
|
|
377
|
+
// an IMPLICIT context tag REPLACING the EnvelopedData SEQUENCE tag — the [0]
|
|
378
|
+
// node's children ARE the EnvelopedData fields, no inner wrapper. Retag it to
|
|
379
|
+
// the universal SEQUENCE the CMS walker expects, exactly as the sibling CRMF
|
|
380
|
+
// `encryptedKey [4] EnvelopedData` POP arm does (schema-crmf.js). The
|
|
381
|
+
// deprecated encryptedValue arm surfaces RAW — never field-walked, so nothing
|
|
382
|
+
// dereferences its optional interior (absent algorithm parameters crash
|
|
383
|
+
// consumers that walk it blindly).
|
|
384
|
+
var ENCRYPTED_KEY = schema.choice([
|
|
385
|
+
{ when: { tagClass: "universal", tagNumber: TAGS.SEQUENCE },
|
|
386
|
+
schema: schema.decode(function (n) { return { encryptedValue: n.bytes }; }) },
|
|
387
|
+
{ when: { tagClass: "context", tagNumber: 0 },
|
|
388
|
+
schema: schema.decode(function (n, ctx) {
|
|
389
|
+
var env;
|
|
390
|
+
// Wrap every EnvelopedData failure — a non-decodable retarget OR a CmsError
|
|
391
|
+
// from the composed walker — as the CMP domain error (with the original as
|
|
392
|
+
// the cause), so cmp.parse never escapes with a cms/* error for a malformed
|
|
393
|
+
// PKIMessage (the same wrapping the sibling CRMF encryptedKey POP arm does).
|
|
394
|
+
try { env = cms.walkEnvelopedData(asn1.decode(asn1.sequenceTlv(n))); }
|
|
395
|
+
catch (e) {
|
|
396
|
+
throw ctx.E("cmp/bad-cert-response", "envelopedData [0] must be a well-formed IMPLICIT EnvelopedData (RFC 9810 §5.2.2)", e);
|
|
397
|
+
}
|
|
398
|
+
// RFC 9810 §5.2.2 — a CMP EncryptedKey EnvelopedData is addressed to
|
|
399
|
+
// exactly one recipient (the end entity for a delivered cert / key). CMS
|
|
400
|
+
// permits RecipientInfos SET SIZE 1..MAX, so the single-recipient bound is
|
|
401
|
+
// CMP's: more than one recipient is a malformed EncryptedKey.
|
|
402
|
+
if (env.recipientInfos.length !== 1) {
|
|
403
|
+
throw ctx.E("cmp/bad-cert-response", "an EncryptedKey EnvelopedData must contain exactly one RecipientInfo (RFC 9810 §5.2.2)");
|
|
404
|
+
}
|
|
405
|
+
// The ciphertext IS the encrypted certificate / private key here, so an
|
|
406
|
+
// EnvelopedData with no ciphertext to decrypt — absent (detached, CMS
|
|
407
|
+
// permits it) OR present-but-empty — must reject (RFC 9810 §5.2.2/§5.3.4;
|
|
408
|
+
// the same rule the CRMF encryptedKey POP arm enforces).
|
|
409
|
+
var ct = env.encryptedContentInfo.encryptedContent;
|
|
410
|
+
if (ct === null || ct.length === 0) {
|
|
411
|
+
throw ctx.E("cmp/bad-cert-response", "an EncryptedKey envelopedData must carry non-empty attached ciphertext — its encryptedContent is the certificate/key payload (RFC 9810 §5.2.2)");
|
|
412
|
+
}
|
|
413
|
+
return { envelopedData: env };
|
|
414
|
+
}) },
|
|
415
|
+
], { code: "cmp/bad-cert-response", what: "EncryptedKey" });
|
|
416
|
+
|
|
417
|
+
// CertOrEncCert ::= CHOICE { certificate [0], encryptedCert [1] } (§5.3.4).
|
|
418
|
+
var CERT_OR_ENC_CERT = schema.choice([
|
|
419
|
+
{ when: { tagClass: "context", tagNumber: 0 },
|
|
420
|
+
schema: schema.explicit(0, schema.decode(function (n, ctx) {
|
|
421
|
+
if (!(n.tagClass === "universal" && n.tagNumber === TAGS.SEQUENCE && n.children && n.children.length >= 1)) {
|
|
422
|
+
throw ctx.E("cmp/bad-cert-response", "a certificate [0] must be a CMPCertificate (a non-empty universal SEQUENCE)");
|
|
423
|
+
}
|
|
424
|
+
return { certificate: n.bytes };
|
|
425
|
+
}), { code: "cmp/bad-cert-response" }) },
|
|
426
|
+
{ when: { tagClass: "context", tagNumber: 1 },
|
|
427
|
+
schema: schema.explicit(1, ENCRYPTED_KEY, { code: "cmp/bad-cert-response" }) },
|
|
428
|
+
], { code: "cmp/bad-cert-response", what: "CertOrEncCert" });
|
|
429
|
+
|
|
430
|
+
// CertifiedKeyPair ::= SEQUENCE { certOrEncCert, privateKey [0] OPTIONAL,
|
|
431
|
+
// publicationInfo [1] OPTIONAL } (§5.3.4).
|
|
432
|
+
var CERTIFIED_KEY_PAIR = schema.seq([
|
|
433
|
+
schema.field("certOrEncCert", CERT_OR_ENC_CERT),
|
|
434
|
+
schema.trailing([
|
|
435
|
+
{ tag: 0, name: "privateKey", schema: ENCRYPTED_KEY, explicit: true, emptyCode: "cmp/bad-cert-response" },
|
|
436
|
+
{ tag: 1, name: "publicationInfo", schema: rawSequence("cmp/bad-cert-response"), explicit: true, emptyCode: "cmp/bad-cert-response" },
|
|
437
|
+
], { minTag: 0, maxTag: 1, unexpectedCode: "cmp/bad-cert-response", orderCode: "cmp/bad-cert-response" }),
|
|
438
|
+
], {
|
|
439
|
+
assert: "sequence", code: "cmp/bad-cert-response", what: "CertifiedKeyPair",
|
|
440
|
+
build: function (m) {
|
|
441
|
+
var out = m.fields.certOrEncCert.value.certificate !== undefined
|
|
442
|
+
? { certificate: m.fields.certOrEncCert.value.certificate }
|
|
443
|
+
: { encryptedCert: m.fields.certOrEncCert.value };
|
|
444
|
+
out.privateKey = m.fields.privateKey.present ? m.fields.privateKey.value : null;
|
|
445
|
+
out.publicationInfo = m.fields.publicationInfo.present ? m.fields.publicationInfo.value : null;
|
|
446
|
+
return out;
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// CertResponse ::= SEQUENCE { certReqId INTEGER (the -1 sentinel is legal),
|
|
451
|
+
// status PKIStatusInfo, certifiedKeyPair OPTIONAL, rspInfo OPTIONAL } (§5.3.4).
|
|
452
|
+
var CERT_RESPONSE = schema.seq([
|
|
453
|
+
schema.field("certReqId", schema.integerLeaf()),
|
|
454
|
+
schema.field("status", PKI_STATUS_INFO),
|
|
455
|
+
schema.optional("certifiedKeyPair", CERTIFIED_KEY_PAIR, { whenUniversal: [TAGS.SEQUENCE] }),
|
|
456
|
+
schema.optional("rspInfo", schema.octetString(), { whenUniversal: [TAGS.OCTET_STRING] }),
|
|
457
|
+
], {
|
|
458
|
+
assert: "sequence", code: "cmp/bad-cert-response", what: "CertResponse",
|
|
459
|
+
build: function (m, ctx) {
|
|
460
|
+
var status = m.fields.status.value.result;
|
|
461
|
+
var certifiedKeyPair = m.fields.certifiedKeyPair.present ? m.fields.certifiedKeyPair.value.result : null;
|
|
462
|
+
// RFC 9810 §5.3.4 — only one of the failInfo (in PKIStatusInfo) and
|
|
463
|
+
// certifiedKeyPair fields can be present in a CertResponse: a rejection
|
|
464
|
+
// carries no certificate. Accepting both lets a caller keying off
|
|
465
|
+
// certifiedKeyPair process a certificate from a failed response.
|
|
466
|
+
if (status.failInfo !== null && certifiedKeyPair !== null) {
|
|
467
|
+
throw ctx.E("cmp/bad-cert-response",
|
|
468
|
+
"a CertResponse must not carry both failInfo and certifiedKeyPair (RFC 9810 §5.3.4)");
|
|
469
|
+
}
|
|
470
|
+
// A certifiedKeyPair is present ONLY when the status grants the certificate:
|
|
471
|
+
// accepted (0) or grantedWithMods (1). Any other status — rejection, waiting,
|
|
472
|
+
// the revocation / keyUpdate warnings — denies or defers the request, so a
|
|
473
|
+
// certificate under it is a malformed response even when no explicit failInfo
|
|
474
|
+
// bit is set (a rejection is commonly signalled by status alone). Keying the
|
|
475
|
+
// rule off failInfo presence alone would let a bare-rejection status ship a
|
|
476
|
+
// certificate (RFC 9810 §5.3.4).
|
|
477
|
+
if (certifiedKeyPair !== null && status.status.code !== 0 && status.status.code !== 1) {
|
|
478
|
+
throw ctx.E("cmp/bad-cert-response",
|
|
479
|
+
"a CertResponse certifiedKeyPair is allowed only under a granting status (accepted or grantedWithMods) (RFC 9810 §5.3.4)");
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
certReqId: m.fields.certReqId.value,
|
|
483
|
+
status: status,
|
|
484
|
+
certifiedKeyPair: certifiedKeyPair,
|
|
485
|
+
rspInfo: m.fields.rspInfo.present ? m.fields.rspInfo.value : null,
|
|
486
|
+
};
|
|
487
|
+
},
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
// CertRepMessage ::= SEQUENCE { caPubs [1] OPTIONAL, response SEQUENCE OF
|
|
491
|
+
// CertResponse } (§5.3.4) — the optional precedes the required field (the
|
|
492
|
+
// x509-version shape). caPubs surface raw: the parser confers NO trust.
|
|
493
|
+
var CERT_REP_MESSAGE = schema.seq([
|
|
494
|
+
schema.optional("caPubs", schema.seqOf(rawSequence("cmp/bad-cert-rep"), {
|
|
495
|
+
min: 1, code: "cmp/bad-cert-rep", what: "caPubs",
|
|
496
|
+
build: function (m) { return m.items.map(function (it) { return it.value; }); },
|
|
497
|
+
}), { tag: 1, explicit: true, emptyCode: "cmp/bad-cert-rep" }),
|
|
498
|
+
schema.field("response", schema.seqOf(CERT_RESPONSE, {
|
|
499
|
+
code: "cmp/bad-cert-rep", what: "response",
|
|
500
|
+
build: function (m) { return m.items.map(function (it) { return it.value.result; }); },
|
|
501
|
+
})),
|
|
502
|
+
], {
|
|
503
|
+
assert: "sequence", code: "cmp/bad-cert-rep", what: "CertRepMessage",
|
|
504
|
+
build: function (m) {
|
|
505
|
+
return {
|
|
506
|
+
caPubs: m.fields.caPubs.present ? m.fields.caPubs.value.result : null,
|
|
507
|
+
response: m.fields.response.value.result,
|
|
508
|
+
};
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// KeyRecRepContent ::= SEQUENCE { status PKIStatusInfo, newSigCert [0]
|
|
513
|
+
// Certificate OPTIONAL, caCerts [1] SEQUENCE OF Certificate OPTIONAL,
|
|
514
|
+
// keyPairHist [2] SEQUENCE OF CertifiedKeyPair OPTIONAL } (§5.3.8). The
|
|
515
|
+
// keyPairHist CertifiedKeyPairs can carry the same EnvelopedData form as the
|
|
516
|
+
// enrollment responses, so krp is decoded (not surfaced raw) — the operator sees
|
|
517
|
+
// the status / key history and the cross-field EnvelopedData version check runs.
|
|
518
|
+
var KEY_REC_REP_CONTENT = schema.seq([
|
|
519
|
+
schema.field("status", PKI_STATUS_INFO),
|
|
520
|
+
schema.trailing([
|
|
521
|
+
{ tag: 0, name: "newSigCert", schema: rawSequence("cmp/bad-key-rec-rep"), explicit: true, emptyCode: "cmp/bad-key-rec-rep" },
|
|
522
|
+
{ tag: 1, name: "caCerts", schema: schema.seqOf(rawSequence("cmp/bad-key-rec-rep"), {
|
|
523
|
+
min: 1, code: "cmp/bad-key-rec-rep", what: "caCerts",
|
|
524
|
+
build: function (m) { return m.items.map(function (it) { return it.value; }); },
|
|
525
|
+
}), explicit: true, emptyCode: "cmp/bad-key-rec-rep" },
|
|
526
|
+
{ tag: 2, name: "keyPairHist", schema: schema.seqOf(CERTIFIED_KEY_PAIR, {
|
|
527
|
+
min: 1, code: "cmp/bad-key-rec-rep", what: "keyPairHist",
|
|
528
|
+
build: function (m) { return m.items.map(function (it) { return it.value.result; }); },
|
|
529
|
+
}), explicit: true, emptyCode: "cmp/bad-key-rec-rep" },
|
|
530
|
+
], { minTag: 0, maxTag: 2, unexpectedCode: "cmp/bad-key-rec-rep", orderCode: "cmp/bad-key-rec-rep" }),
|
|
531
|
+
], {
|
|
532
|
+
assert: "sequence", code: "cmp/bad-key-rec-rep", what: "KeyRecRepContent",
|
|
533
|
+
build: function (m) {
|
|
534
|
+
return {
|
|
535
|
+
status: m.fields.status.value.result,
|
|
536
|
+
newSigCert: m.fields.newSigCert.present ? m.fields.newSigCert.value : null,
|
|
537
|
+
caCerts: m.fields.caCerts.present ? m.fields.caCerts.value.result : null,
|
|
538
|
+
keyPairHist: m.fields.keyPairHist.present ? m.fields.keyPairHist.value.result : null,
|
|
539
|
+
};
|
|
540
|
+
},
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
// PollReqContent / PollRepContent (§5.3.22) — certReqId may be -1.
|
|
544
|
+
var POLL_REQ_ENTRY = schema.seq([
|
|
545
|
+
schema.field("certReqId", schema.integerLeaf()),
|
|
546
|
+
], {
|
|
547
|
+
assert: "sequence", code: "cmp/bad-poll-req", what: "PollReq entry",
|
|
548
|
+
build: function (m) { return { certReqId: m.fields.certReqId.value }; },
|
|
549
|
+
});
|
|
550
|
+
var POLL_REQ_CONTENT = schema.seqOf(POLL_REQ_ENTRY, {
|
|
551
|
+
code: "cmp/bad-poll-req", what: "PollReqContent",
|
|
552
|
+
build: function (m) { return m.items.map(function (it) { return it.value.result; }); },
|
|
553
|
+
});
|
|
554
|
+
var POLL_REP_ENTRY = schema.seq([
|
|
555
|
+
schema.field("certReqId", schema.integerLeaf()),
|
|
556
|
+
schema.field("checkAfter", CHECK_AFTER),
|
|
557
|
+
schema.optional("reason", PKI_FREE_TEXT, { whenUniversal: [TAGS.SEQUENCE] }),
|
|
558
|
+
], {
|
|
559
|
+
assert: "sequence", code: "cmp/bad-poll-rep", what: "PollRep entry",
|
|
560
|
+
build: function (m) {
|
|
561
|
+
return {
|
|
562
|
+
certReqId: m.fields.certReqId.value,
|
|
563
|
+
checkAfter: m.fields.checkAfter.value,
|
|
564
|
+
reason: m.fields.reason.present ? m.fields.reason.value.result : null,
|
|
565
|
+
};
|
|
566
|
+
},
|
|
567
|
+
});
|
|
568
|
+
var POLL_REP_CONTENT = schema.seqOf(POLL_REP_ENTRY, {
|
|
569
|
+
code: "cmp/bad-poll-rep", what: "PollRepContent",
|
|
570
|
+
build: function (m) { return m.items.map(function (it) { return it.value.result; }); },
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// ---- the 27-arm body dispatch ----------------------------------------------
|
|
574
|
+
|
|
575
|
+
// Tag -> arm name, verbatim from the PKIXCMP-2023 module (§5.1.2). Every
|
|
576
|
+
// defined arm is at minimum recognized; a subset decodes structurally.
|
|
577
|
+
var BODY_ARMS = ["ir", "ip", "cr", "cp", "p10cr", "popdecc", "popdecr", "kur", "kup",
|
|
578
|
+
"krr", "krp", "rr", "rp", "ccr", "ccp", "ckuann", "cann", "rann", "crlann",
|
|
579
|
+
"pkiconf", "nested", "genm", "genp", "error", "certConf", "pollReq", "pollRep"];
|
|
580
|
+
// Arms carrying RFC 4211 CertReqMessages — walked by the CRMF parser.
|
|
581
|
+
var CRMF_ARMS = { 0: true, 2: true, 7: true, 9: true, 13: true };
|
|
582
|
+
// Arms with a structural schema here.
|
|
583
|
+
var DECODED_ARMS = {
|
|
584
|
+
1: CERT_REP_MESSAGE, 3: CERT_REP_MESSAGE, 8: CERT_REP_MESSAGE, 14: CERT_REP_MESSAGE,
|
|
585
|
+
10: KEY_REC_REP_CONTENT,
|
|
586
|
+
11: REV_REQ_CONTENT, 12: REV_REP_CONTENT,
|
|
587
|
+
21: GEN_MSG_CONTENT, 22: GEN_MSG_CONTENT,
|
|
588
|
+
23: ERROR_MSG_CONTENT, 24: CERT_CONFIRM_CONTENT,
|
|
589
|
+
25: POLL_REQ_CONTENT, 26: POLL_REP_CONTENT,
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
// The open-CHOICE dispatch: tag -> arm name; walk where a schema exists;
|
|
593
|
+
// surface every other defined arm raw as { arm, tag, bytes } — `decoded` is
|
|
594
|
+
// ABSENT (not null) on raw arms, so its presence discriminates decoded-empty
|
|
595
|
+
// (pkiconf decodes to null) from recognized-undecoded. `nested` [20] is never
|
|
596
|
+
// auto-recursed: a self-nesting tower amplifies per-level walk products, so
|
|
597
|
+
// the operator re-feeds its bytes to parse explicitly.
|
|
598
|
+
var BODY = schema.decode(function (n, ctx) {
|
|
599
|
+
if (n.tagClass !== "context" || !n.children) {
|
|
600
|
+
throw ctx.E("cmp/bad-body", "PKIBody must be a constructed context-tagged CHOICE arm (RFC 9810 §5.1.2)");
|
|
601
|
+
}
|
|
602
|
+
if (n.tagNumber < 0 || n.tagNumber > 26) {
|
|
603
|
+
throw ctx.E("cmp/bad-body", "PKIBody tag [" + n.tagNumber + "] is not a defined arm (RFC 9810 §5.1.2)");
|
|
604
|
+
}
|
|
605
|
+
if (n.children.length !== 1) {
|
|
606
|
+
throw ctx.E("cmp/bad-body", "a PKIBody arm wraps exactly one value (X.690 §8.14)");
|
|
607
|
+
}
|
|
608
|
+
var inner = n.children[0];
|
|
609
|
+
var out = { arm: BODY_ARMS[n.tagNumber], tag: n.tagNumber, bytes: inner.bytes };
|
|
610
|
+
if (CRMF_ARMS[n.tagNumber]) {
|
|
611
|
+
// ccr [13] cross-certification: RFC 9810 Appendix D.6 profiles its template
|
|
612
|
+
// with signingAlg present and allows exactly one CertReqMsg (multiple
|
|
613
|
+
// cross-certificates go in separate PKIMessages).
|
|
614
|
+
var isCcr = n.tagNumber === 13;
|
|
615
|
+
out.decoded = crmf.walkCertReqMessages(inner, { allowSigningAlg: isCcr });
|
|
616
|
+
if (isCcr && out.decoded.messages.length !== 1) {
|
|
617
|
+
throw ctx.E("cmp/bad-body", "a cross-certification request (ccr) must contain exactly one CertReqMsg (RFC 9810 Appendix D.6)");
|
|
618
|
+
}
|
|
619
|
+
} else if (DECODED_ARMS[n.tagNumber]) {
|
|
620
|
+
var match = schema.walk(DECODED_ARMS[n.tagNumber], inner, ctx);
|
|
621
|
+
out.decoded = match.result;
|
|
622
|
+
} else if (n.tagNumber === 19) {
|
|
623
|
+
// PKIConfirmContent ::= NULL (§5.3.16).
|
|
624
|
+
if (!(inner.tagClass === "universal" && inner.tagNumber === TAGS.NULL)) {
|
|
625
|
+
throw ctx.E("cmp/bad-pkiconf", "PKIConfirmContent must be NULL (RFC 9810 §5.3.16)");
|
|
626
|
+
}
|
|
627
|
+
// A NULL is well-formed only with empty content (X.690 §8.8.2); the tag check
|
|
628
|
+
// alone would surface a non-empty NULL as a valid confirmation.
|
|
629
|
+
try { asn1.read.nullValue(inner); }
|
|
630
|
+
catch (e) { throw ctx.E("cmp/bad-pkiconf", "PKIConfirmContent must be an empty NULL (RFC 9810 §5.3.16)", e); }
|
|
631
|
+
out.decoded = null;
|
|
632
|
+
}
|
|
633
|
+
return out;
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// ---- the message envelope -----------------------------------------------------
|
|
637
|
+
|
|
638
|
+
// PKIProtection ::= BIT STRING — raw bits for the external verifier.
|
|
639
|
+
var PROTECTION = schema.decode(function (n, ctx) {
|
|
640
|
+
var bs;
|
|
641
|
+
try { bs = asn1.read.bitString(n); }
|
|
642
|
+
catch (e) { throw ctx.E("cmp/bad-protection", "PKIProtection must be a BIT STRING (RFC 9810 §5.1.3)", e); }
|
|
643
|
+
return { unusedBits: bs.unusedBits, bytes: bs.bytes };
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
// PKIMessage ::= SEQUENCE { header, body, protection [0] OPTIONAL,
|
|
647
|
+
// extraCerts [1] OPTIONAL } (§5.1). The build runs the two cross-checks that
|
|
648
|
+
// span structures: protection presence <=> protectionAlg presence (§5.1.1,
|
|
649
|
+
// both directions), and a certConf hashAlg requires pvno cmp2021(3)
|
|
650
|
+
// (§5.3.18). headerBytes / bodyBytes are the exact wire slices protection is
|
|
651
|
+
// computed over (as DER-SEQUENCE(headerBytes || bodyBytes)) — surfaced raw,
|
|
652
|
+
// never re-encoded.
|
|
653
|
+
var PKI_MESSAGE = schema.seq([
|
|
654
|
+
schema.field("header", PKI_HEADER),
|
|
655
|
+
schema.field("body", BODY),
|
|
656
|
+
schema.trailing([
|
|
657
|
+
{ tag: 0, name: "protection", schema: PROTECTION, explicit: true, emptyCode: "cmp/bad-protection" },
|
|
658
|
+
{ tag: 1, name: "extraCerts", schema: schema.seqOf(rawSequence("cmp/bad-extra-certs"), {
|
|
659
|
+
min: 1, code: "cmp/bad-extra-certs", what: "extraCerts",
|
|
660
|
+
build: function (m) { return m.items.map(function (it) { return it.value; }); },
|
|
661
|
+
}), explicit: true, emptyCode: "cmp/bad-extra-certs" },
|
|
662
|
+
], { minTag: 0, maxTag: 1, unexpectedCode: "cmp/not-a-pki-message", orderCode: "cmp/not-a-pki-message" }),
|
|
663
|
+
], {
|
|
664
|
+
assert: "sequence", code: "cmp/not-a-pki-message", what: "PKIMessage",
|
|
665
|
+
build: function (m, ctx) {
|
|
666
|
+
var header = m.fields.header.value.result;
|
|
667
|
+
var body = m.fields.body.value;
|
|
668
|
+
var protection = m.fields.protection.present ? m.fields.protection.value : null;
|
|
669
|
+
var extraCerts = m.fields.extraCerts.present ? m.fields.extraCerts.value.result : null;
|
|
670
|
+
if ((protection !== null) !== (header.protectionAlg !== null)) {
|
|
671
|
+
throw ctx.E("cmp/protection-alg-mismatch",
|
|
672
|
+
"protection bits and the header protectionAlg must be present together or absent together (RFC 9810 §5.1.1)");
|
|
673
|
+
}
|
|
674
|
+
if (body.arm === "certConf") {
|
|
675
|
+
for (var i = 0; i < body.decoded.length; i++) {
|
|
676
|
+
if (body.decoded[i].hashAlg !== null && header.pvno !== 3) {
|
|
677
|
+
throw ctx.E("cmp/bad-cert-status", "a certConf hashAlg requires CMP version cmp2021(3) (RFC 9810 §5.3.18)");
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
// RFC 9810 §5.2.2 / §7 — EnvelopedData is cmp2021(3) syntax: a response
|
|
682
|
+
// carrying it (an encryptedCert or a privateKey in the envelopedData form)
|
|
683
|
+
// under pvno < 3 is a version mismatch, the same version gate the certConf
|
|
684
|
+
// hashAlg rule applies. The deprecated EncryptedValue form is the pre-2021
|
|
685
|
+
// encoding and stays legal at pvno 2. CertifiedKeyPairs arrive both in the
|
|
686
|
+
// CertRepMessage `response` array (ip/cp/kup/ccp) and in the krp keyPairHist.
|
|
687
|
+
var ckps = [];
|
|
688
|
+
if (body.decoded && body.decoded.response) {
|
|
689
|
+
for (var r = 0; r < body.decoded.response.length; r++) {
|
|
690
|
+
if (body.decoded.response[r].certifiedKeyPair) ckps.push(body.decoded.response[r].certifiedKeyPair);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
if (body.decoded && body.decoded.keyPairHist) {
|
|
694
|
+
for (var kh = 0; kh < body.decoded.keyPairHist.length; kh++) ckps.push(body.decoded.keyPairHist[kh]);
|
|
695
|
+
}
|
|
696
|
+
for (var ck = 0; ck < ckps.length; ck++) {
|
|
697
|
+
var usesEnveloped =
|
|
698
|
+
(ckps[ck].encryptedCert && ckps[ck].encryptedCert.envelopedData !== undefined) ||
|
|
699
|
+
(ckps[ck].privateKey && ckps[ck].privateKey.envelopedData !== undefined);
|
|
700
|
+
if (usesEnveloped && header.pvno !== 3) {
|
|
701
|
+
throw ctx.E("cmp/bad-version", "a response carrying EnvelopedData (encryptedCert or privateKey) requires CMP version cmp2021(3) (RFC 9810 §5.2.2, §7)");
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
// RFC 9810 §5.3.12 — a cross-certification response (ccp) reuses the
|
|
705
|
+
// CertRepMessage syntax "with the restriction that no encrypted private key
|
|
706
|
+
// can be sent": cross-certification certifies an existing CA's public key,
|
|
707
|
+
// so there is no key generation and CertifiedKeyPair.privateKey has no
|
|
708
|
+
// meaning. Reject a ccp that carries one rather than surface the key
|
|
709
|
+
// material; the field stays legal in the enrollment responses (ip/cp/kup).
|
|
710
|
+
if (body.arm === "ccp" && body.decoded && body.decoded.response) {
|
|
711
|
+
// RFC 9810 Appendix D.6 — a one-way cross-certification response carries
|
|
712
|
+
// exactly one CertResponse (multiple cross-certificates go in separate
|
|
713
|
+
// PKIMessages), the mirror of the ccr one-CertReqMsg rule.
|
|
714
|
+
if (body.decoded.response.length !== 1) {
|
|
715
|
+
throw ctx.E("cmp/bad-body", "a cross-certification response (ccp) must contain exactly one CertResponse (RFC 9810 Appendix D.6)");
|
|
716
|
+
}
|
|
717
|
+
for (var c = 0; c < body.decoded.response.length; c++) {
|
|
718
|
+
var ckpCcp = body.decoded.response[c].certifiedKeyPair;
|
|
719
|
+
if (ckpCcp && ckpCcp.privateKey !== null) {
|
|
720
|
+
throw ctx.E("cmp/bad-cert-response", "a cross-certification response (ccp) must not carry an encrypted private key (RFC 9810 §5.3.12)");
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
// RFC 9810 §5.3.11 — a cross-certification request (ccr) MUST NOT send the
|
|
725
|
+
// private key to the responding CA (the requesting CA generates and holds
|
|
726
|
+
// it). The private-key-carrying POP choices — encryptedKey (cmp2021) and the
|
|
727
|
+
// deprecated thisMessage (cmp2000) — are therefore forbidden in a ccr; the
|
|
728
|
+
// MAC (dhMAC / agreeMAC) and indirect (subsequentMessage) methods carry no
|
|
729
|
+
// key. Checked before the version gate so a ccr always gets the ccr verdict.
|
|
730
|
+
if (body.arm === "ccr" && body.decoded && body.decoded.messages) {
|
|
731
|
+
for (var cc = 0; cc < body.decoded.messages.length; cc++) {
|
|
732
|
+
var ccrMsg = body.decoded.messages[cc];
|
|
733
|
+
// Via the POP: the encryptedKey / thisMessage POPOPrivKey choices carry
|
|
734
|
+
// the private key.
|
|
735
|
+
var ccrPopo = ccrMsg.popo;
|
|
736
|
+
if (ccrPopo && (ccrPopo.method === "encryptedKey" || ccrPopo.method === "thisMessage")) {
|
|
737
|
+
throw ctx.E("cmp/bad-body", "a cross-certification request (ccr) must not send the private key — the " + ccrPopo.method + " POP choice is forbidden (RFC 9810 §5.3.11)");
|
|
738
|
+
}
|
|
739
|
+
// Via a control: a pkiArchiveOptions control with the encryptedPrivKey [0]
|
|
740
|
+
// arm is the other private-key transport (§5.2.8.3.1), likewise forbidden
|
|
741
|
+
// in a ccr. The control value is the raw PKIArchiveOptions CHOICE; the
|
|
742
|
+
// encryptedPrivKey arm is context tag [0].
|
|
743
|
+
var controls = ccrMsg.certReq.controls;
|
|
744
|
+
if (controls) {
|
|
745
|
+
for (var cn = 0; cn < controls.length; cn++) {
|
|
746
|
+
if (controls[cn].type !== OID_PKI_ARCHIVE_OPTIONS) continue;
|
|
747
|
+
var arch;
|
|
748
|
+
try { arch = asn1.decode(controls[cn].value); }
|
|
749
|
+
catch (e) { throw ctx.E("cmp/bad-body", "a ccr pkiArchiveOptions control did not decode (RFC 9810 §5.3.11)", e); }
|
|
750
|
+
if (arch.tagClass === "context" && arch.tagNumber === 0) {
|
|
751
|
+
throw ctx.E("cmp/bad-body", "a cross-certification request (ccr) must not send the private key — a pkiArchiveOptions encryptedPrivKey control is forbidden (RFC 9810 §5.3.11, §5.2.8.3.1)");
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
// RFC 9810 §5.2.8.3 — the agreeMAC and encryptedKey POPOPrivKey choices are
|
|
758
|
+
// cmp2021(3) syntax; a request (ir/cr/kur/krr/ccr) whose proof-of-possession
|
|
759
|
+
// uses either under pvno < 3 is a version mismatch, the same gate the
|
|
760
|
+
// response EnvelopedData rule applies. The CRMF walker surfaces the chosen
|
|
761
|
+
// POPOPrivKey method on each message's popo.
|
|
762
|
+
if (body.decoded && body.decoded.messages) {
|
|
763
|
+
for (var q = 0; q < body.decoded.messages.length; q++) {
|
|
764
|
+
var popo = body.decoded.messages[q].popo;
|
|
765
|
+
if (popo && (popo.method === "agreeMAC" || popo.method === "encryptedKey") && header.pvno !== 3) {
|
|
766
|
+
throw ctx.E("cmp/bad-version", "a request POP using the agreeMAC or encryptedKey choice requires CMP version cmp2021(3) (RFC 9810 §5.2.8.3)");
|
|
767
|
+
}
|
|
768
|
+
// §5.2.8.3.1/§7 — the same key transport via a pkiArchiveOptions control
|
|
769
|
+
// whose encryptedPrivKey [0] EXPLICIT wraps the envelopedData-form
|
|
770
|
+
// EncryptedKey is cmp2021 syntax too (a ccr rejects the control outright
|
|
771
|
+
// above; the other request arms are version-gated here).
|
|
772
|
+
var archCtrls = body.decoded.messages[q].certReq.controls;
|
|
773
|
+
if (archCtrls && header.pvno !== 3) {
|
|
774
|
+
for (var ac = 0; ac < archCtrls.length; ac++) {
|
|
775
|
+
if (archCtrls[ac].type !== OID_PKI_ARCHIVE_OPTIONS) continue;
|
|
776
|
+
var opt = asn1.decode(archCtrls[ac].value);
|
|
777
|
+
if (opt.tagClass === "context" && opt.tagNumber === 0 && opt.children && opt.children.length >= 1 &&
|
|
778
|
+
opt.children[0].tagClass === "context" && opt.children[0].tagNumber === 0) {
|
|
779
|
+
throw ctx.E("cmp/bad-version", "a pkiArchiveOptions encryptedPrivKey using the EnvelopedData form requires CMP version cmp2021(3) (RFC 9810 §5.2.8.3.1, §7)");
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
// RFC 9810 §5.1.1.4 — the certProfile name count is bounded by the message
|
|
786
|
+
// body: a p10cr carries at most one; an ir/cr/kur no more names than its
|
|
787
|
+
// CertReqMsg count, a genm no more than its GenMsgContent InfoTypeAndValue
|
|
788
|
+
// count (the names map positionally to the body entries). An overlong list
|
|
789
|
+
// makes that mapping ambiguous, so reject it.
|
|
790
|
+
if (header.generalInfo) {
|
|
791
|
+
for (var gi = 0; gi < header.generalInfo.length; gi++) {
|
|
792
|
+
if (header.generalInfo[gi].type !== OID_CERT_PROFILE) continue;
|
|
793
|
+
var profileCount = asn1.decode(header.generalInfo[gi].value).children.length;
|
|
794
|
+
var bodyCount = null;
|
|
795
|
+
if (body.arm === "p10cr") bodyCount = 1;
|
|
796
|
+
else if (body.decoded && body.decoded.messages) bodyCount = body.decoded.messages.length;
|
|
797
|
+
else if (body.arm === "genm" && Array.isArray(body.decoded)) bodyCount = body.decoded.length;
|
|
798
|
+
if (bodyCount !== null && profileCount > bodyCount) {
|
|
799
|
+
throw ctx.E("cmp/bad-info-value", "a certProfile carries " + profileCount + " profile name(s), more than the " + bodyCount + " the " + body.arm + " body permits (RFC 9810 §5.1.1.4)");
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
// RFC 9810 §5.3.13/§7 — the cAKeyUpdAnnV3 [0] RootCaKeyUpdateContent CA key
|
|
804
|
+
// update form is cmp2021(3) syntax; a ckuann using it under pvno < 3 is a
|
|
805
|
+
// version mismatch. ckuann surfaces raw, so inspect its top-level tag: the
|
|
806
|
+
// deprecated cAKeyUpdAnnV2 is a universal SEQUENCE, the v3 form is [0].
|
|
807
|
+
if (body.arm === "ckuann" && header.pvno !== 3) {
|
|
808
|
+
var caKeyUpd = asn1.decode(body.bytes);
|
|
809
|
+
if (caKeyUpd.tagClass === "context" && caKeyUpd.tagNumber === 0) {
|
|
810
|
+
throw ctx.E("cmp/bad-version", "a ckuann using the cAKeyUpdAnnV3 [0] RootCaKeyUpdateContent form requires CMP version cmp2021(3) (RFC 9810 §5.3.13, §7)");
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return {
|
|
814
|
+
header: header,
|
|
815
|
+
headerBytes: m.fields.header.value.node.bytes,
|
|
816
|
+
body: body,
|
|
817
|
+
bodyBytes: m.fields.body.node.bytes,
|
|
818
|
+
protection: protection,
|
|
819
|
+
extraCerts: extraCerts,
|
|
820
|
+
};
|
|
821
|
+
},
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* @primitive pki.schema.cmp.parse
|
|
826
|
+
* @signature pki.schema.cmp.parse(input) -> pkiMessage
|
|
827
|
+
* @since 0.1.19
|
|
828
|
+
* @status experimental
|
|
829
|
+
* @spec RFC 9810, RFC 9483, RFC 9481
|
|
830
|
+
* @related pki.schema.parse, pki.schema.crmf.parse, pki.schema.cms.parse
|
|
831
|
+
*
|
|
832
|
+
* Parse a DER `Buffer` or a PEM string into a structured PKIMessage:
|
|
833
|
+
* `{ header, headerBytes, body, bodyBytes, protection, extraCerts }`.
|
|
834
|
+
* `header` carries `pvno` (1..3), `sender` / `recipient` (validated
|
|
835
|
+
* GeneralNames — the anonymous NULL-DN form included), and the optional
|
|
836
|
+
* `messageTime`, `protectionAlg`, `senderKID` / `recipKID`,
|
|
837
|
+
* `transactionID`, `senderNonce` / `recipNonce`, `freeText`, and
|
|
838
|
+
* `generalInfo` (recognized id-it types value-checked: implicitConfirm is
|
|
839
|
+
* NULL, confirmWaitTime a GeneralizedTime, certProfile a sequence of
|
|
840
|
+
* UTF8Strings). `body` is `{ arm, tag, bytes, decoded? }`: request arms
|
|
841
|
+
* (`ir` / `cr` / `kur` / `krr` / `ccr`) decode via the CRMF parser;
|
|
842
|
+
* `ip` / `cp` / `kup` / `ccp` decode to `{ caPubs, response }` (an encrypted
|
|
843
|
+
* certificate's EnvelopedData decodes via the CMS parser; the deprecated
|
|
844
|
+
* EncryptedValue arm and `caPubs` surface raw — the parser confers no
|
|
845
|
+
* trust); `krp` decodes to `{ status, newSigCert, caCerts, keyPairHist }`;
|
|
846
|
+
* `rr` / `rp`, `genm` / `genp`, `error`, `certConf` (an empty
|
|
847
|
+
* confirmation is the legal reject-all), and `pollReq` / `pollRep` decode
|
|
848
|
+
* structurally; `pkiconf` decodes to `null`; every other defined arm —
|
|
849
|
+
* `p10cr` (feed `body.bytes` to `pki.schema.csr.parse`), the
|
|
850
|
+
* challenge-response and announcement arms, and `nested` (never
|
|
851
|
+
* auto-recursed) — surfaces raw with `decoded` absent. `certReqId` values
|
|
852
|
+
* are BigInt and accept the protocol's -1 sentinel.
|
|
853
|
+
*
|
|
854
|
+
* Protection is surfaced, not verified: `protection` carries the raw BIT
|
|
855
|
+
* STRING, and the MAC or signature is computed over the DER of the virtual
|
|
856
|
+
* `ProtectedPart ::= SEQUENCE { header, body }` — reconstruct it as a DER
|
|
857
|
+
* SEQUENCE wrapping exactly `headerBytes || bodyBytes`. `extraCerts` are
|
|
858
|
+
* raw DER certificates and are NOT covered by protection.
|
|
859
|
+
*
|
|
860
|
+
* Throws `CmpError` when the bytes are not a well-formed PKIMessage, and
|
|
861
|
+
* `Asn1Error` when the underlying DER is malformed.
|
|
862
|
+
*
|
|
863
|
+
* @example
|
|
864
|
+
* var m = pki.schema.cmp.parse(der);
|
|
865
|
+
* m.body.arm; // "ir", "ip", "error", ...
|
|
866
|
+
* m.header.transactionID; // raw Buffer or null
|
|
867
|
+
*/
|
|
868
|
+
var parse = pkix.makeParser({
|
|
869
|
+
pemLabel: "CMP", PemError: PemError, ErrorClass: CmpError,
|
|
870
|
+
prefix: "cmp", what: "PKIMessage", topSchema: PKI_MESSAGE, ns: NS,
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* @primitive pki.schema.cmp.pemDecode
|
|
875
|
+
* @signature pki.schema.cmp.pemDecode(text, label?) -> Buffer
|
|
876
|
+
* @since 0.1.19
|
|
877
|
+
* @status experimental
|
|
878
|
+
* @spec RFC 7468, RFC 9810
|
|
879
|
+
* @related pki.schema.cmp.parse
|
|
880
|
+
*
|
|
881
|
+
* Extract the DER bytes from a PEM block (default label `CMP`). CMP is
|
|
882
|
+
* wire-DER over HTTP (RFC 9811) — the PEM path is a convenience for
|
|
883
|
+
* messages that transit text channels.
|
|
884
|
+
*
|
|
885
|
+
* @example
|
|
886
|
+
* var der = pki.schema.cmp.pemDecode(pemText);
|
|
887
|
+
*/
|
|
888
|
+
function pemDecode(text, label) { return pkix.pemDecode(text, label || "CMP", PemError); }
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* @primitive pki.schema.cmp.pemEncode
|
|
892
|
+
* @signature pki.schema.cmp.pemEncode(der, label?) -> string
|
|
893
|
+
* @since 0.1.19
|
|
894
|
+
* @status experimental
|
|
895
|
+
* @spec RFC 7468
|
|
896
|
+
* @related pki.schema.cmp.pemDecode
|
|
897
|
+
*
|
|
898
|
+
* Wrap DER bytes in a PEM envelope (default label `CMP`).
|
|
899
|
+
*
|
|
900
|
+
* @example
|
|
901
|
+
* var pem = pki.schema.cmp.pemEncode(der);
|
|
902
|
+
*/
|
|
903
|
+
function pemEncode(der, label) { return pkix.pemEncode(der, label || "CMP", PemError); }
|
|
904
|
+
|
|
905
|
+
// A PKIMessage root is a SEQUENCE of 2-4 whose first child (PKIHeader) is a
|
|
906
|
+
// SEQUENCE of >= 3 leading with a bare INTEGER (pvno) and whose second child
|
|
907
|
+
// (PKIBody) is context-class constructed [0..26]. The one overlap in the
|
|
908
|
+
// registry is one-directional: a 2-child PKIMessage with body ir [0] also
|
|
909
|
+
// satisfies the shallow ocsp-request probe, so this detector registers AHEAD
|
|
910
|
+
// of ocsp-request — while every real OCSPRequest fails here (its tbsRequest
|
|
911
|
+
// never leads with a bare INTEGER).
|
|
912
|
+
function matches(root) {
|
|
913
|
+
var k = pkix.rootSequenceChildren(root, 2, 4);
|
|
914
|
+
if (!k) return false;
|
|
915
|
+
var header = k[0];
|
|
916
|
+
if (!(schema.isUniversal(header, TAGS.SEQUENCE) && header.children && header.children.length >= 3)) return false;
|
|
917
|
+
var pvno = header.children[0];
|
|
918
|
+
if (!schema.isUniversal(pvno, TAGS.INTEGER)) return false;
|
|
919
|
+
var body = k[1];
|
|
920
|
+
return schema.isContextInRange(body, 0, 26) && !!body.children;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
module.exports = {
|
|
924
|
+
parse: parse,
|
|
925
|
+
pemDecode: pemDecode,
|
|
926
|
+
pemEncode: pemEncode,
|
|
927
|
+
matches: matches,
|
|
928
|
+
};
|