@blamejs/pki 0.1.16 → 0.1.18
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 +39 -0
- package/README.md +9 -7
- package/lib/asn1-der.js +122 -22
- package/lib/constants.js +16 -0
- package/lib/framework-error.js +13 -0
- package/lib/oid.js +42 -3
- package/lib/path-validate.js +10 -2
- package/lib/schema-all.js +41 -7
- package/lib/schema-cms.js +22 -3
- package/lib/schema-crl.js +2 -2
- package/lib/schema-crmf.js +487 -0
- package/lib/schema-csr.js +3 -3
- package/lib/schema-engine.js +216 -18
- package/lib/schema-ocsp.js +5 -7
- package/lib/schema-pkcs12.js +615 -0
- package/lib/schema-pkcs8.js +14 -4
- package/lib/schema-pkix.js +71 -29
- package/lib/schema-tsp.js +1 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @module pki.schema.crmf
|
|
6
|
+
* @nav Schema
|
|
7
|
+
* @title CRMF
|
|
8
|
+
* @order 180
|
|
9
|
+
* @slug crmf
|
|
10
|
+
*
|
|
11
|
+
* @intro
|
|
12
|
+
* Certificate Request Message Format handling per RFC 4211. `parse` decodes a
|
|
13
|
+
* `CertReqMessages` — the request body CMP and EST enrollment carry — into an
|
|
14
|
+
* array of messages, each with its `CertRequest` (certReqId, a `CertTemplate`
|
|
15
|
+
* of the requested certificate fields, and any registration controls), an
|
|
16
|
+
* optional proof-of-possession, and optional registration info.
|
|
17
|
+
*
|
|
18
|
+
* RFC 4211 Appendix B is an IMPLICIT TAGS module, so the whole `CertTemplate`
|
|
19
|
+
* body is one ascending run of IMPLICIT context tags `[0]`..`[9]`, every field
|
|
20
|
+
* OPTIONAL. Two fields break IMPLICIT because their base type is a CHOICE and
|
|
21
|
+
* X.680 forces EXPLICIT: `issuer [3]` / `subject [5]` are `Name`, dual-accepted
|
|
22
|
+
* here (the standards-compliant EXPLICIT encoding and the dominant IMPLICIT one
|
|
23
|
+
* real tooling emits); and the `OptionalValidity` times are EXPLICIT. version,
|
|
24
|
+
* when supplied, MUST be 2 (RFC 4211 §5); certReqId is an unbounded signed
|
|
25
|
+
* INTEGER (the RFC 9483 `-1` sentinel is legal). The `CertRequest` byte range
|
|
26
|
+
* the proof-of-possession signature covers, and each `poposkInput`, are surfaced
|
|
27
|
+
* RAW for a downstream verifier; registration controls / info values and the
|
|
28
|
+
* keyEncipherment / keyAgreement POP arms are surfaced RAW rather than recursed.
|
|
29
|
+
* DER-only, fail-closed.
|
|
30
|
+
*
|
|
31
|
+
* @card
|
|
32
|
+
* Parse DER / PEM RFC 4211 CertReqMessages into requested-certificate templates,
|
|
33
|
+
* proof-of-possession, and registration controls — dual-accepted names, raw
|
|
34
|
+
* verifier inputs, fail-closed.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
var asn1 = require("./asn1-der");
|
|
38
|
+
var schema = require("./schema-engine");
|
|
39
|
+
var pkix = require("./schema-pkix");
|
|
40
|
+
var cms = require("./schema-cms");
|
|
41
|
+
var oid = require("./oid");
|
|
42
|
+
var frameworkError = require("./framework-error");
|
|
43
|
+
|
|
44
|
+
var CrmfError = frameworkError.CrmfError;
|
|
45
|
+
var PemError = frameworkError.PemError;
|
|
46
|
+
|
|
47
|
+
var NS = pkix.makeNS("crmf", CrmfError, oid);
|
|
48
|
+
|
|
49
|
+
var TAGS = asn1.TAGS;
|
|
50
|
+
|
|
51
|
+
// ---- dual-accept Name (issuer [3] / subject [5]) ---------------------
|
|
52
|
+
// RFC 4211 Appendix B is IMPLICIT TAGS, but Name is a CHOICE so X.680 §31.2.7
|
|
53
|
+
// forces EXPLICIT tagging — the standards-compliant wire is a context [tag]
|
|
54
|
+
// EXPLICIT wrapper around a universal RDNSequence. The dominant tooling
|
|
55
|
+
// (pyasn1-modules, BouncyCastle) instead encodes IMPLICIT (the [tag] REPLACES the
|
|
56
|
+
// RDNSequence 0x30, so the children ARE the RDN SETs). Both occur in real CMP /
|
|
57
|
+
// EST, so accept either, disambiguated by the first inner element's tag: a
|
|
58
|
+
// universal SET leads the IMPLICIT form; a single universal SEQUENCE is the
|
|
59
|
+
// EXPLICIT wrapper. Anything else fails closed.
|
|
60
|
+
function crmfName(tag) {
|
|
61
|
+
var NAME = pkix.name(NS); // EXPLICIT arm walks a universal RDNSequence
|
|
62
|
+
var INAME = pkix.name(NS, { implicitTag: tag }); // IMPLICIT arm: [tag] children ARE the RDN SETs
|
|
63
|
+
return schema.decode(function (n, ctx) {
|
|
64
|
+
if (n.tagClass !== "context" || n.tagNumber !== tag || !n.children) {
|
|
65
|
+
throw ctx.E("crmf/bad-name", "issuer/subject [" + tag + "] must be a Name (RFC 4211 §5)");
|
|
66
|
+
}
|
|
67
|
+
if (n.children.length === 0) return schema.walk(INAME, n, ctx).result; // IMPLICIT empty RDNSequence
|
|
68
|
+
var c0 = n.children[0];
|
|
69
|
+
if (c0.tagClass === "universal" && c0.tagNumber === TAGS.SET) {
|
|
70
|
+
return schema.walk(INAME, n, ctx).result; // IMPLICIT: children ARE RDN SETs
|
|
71
|
+
}
|
|
72
|
+
if (n.children.length === 1 && c0.tagClass === "universal" && c0.tagNumber === TAGS.SEQUENCE) {
|
|
73
|
+
return schema.walk(NAME, c0, ctx).result; // EXPLICIT: [tag] wraps RDNSequence
|
|
74
|
+
}
|
|
75
|
+
throw ctx.E("crmf/bad-name", "issuer/subject [" + tag + "] Name must be an IMPLICIT RDNSequence (SET-led) or an EXPLICIT-wrapped RDNSequence");
|
|
76
|
+
}, function (value) {
|
|
77
|
+
// encode the IMPLICIT form (the dominant wire encoding): the [tag] whose children
|
|
78
|
+
// ARE the RDN SETs, built from the decoded `rdns` via the shared Name schema.
|
|
79
|
+
return schema.encode(INAME, value.rdns, NS);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---- OptionalValidity (§5) -------------------------------------------
|
|
84
|
+
// validity [4] IMPLICIT SEQUENCE { notBefore [0] Time OPTIONAL, notAfter [1] Time
|
|
85
|
+
// OPTIONAL }. Time is a CHOICE of UTCTime / GeneralizedTime, so [0]/[1] are
|
|
86
|
+
// EXPLICIT (an IMPLICIT tag would erase the UTCTime-vs-GeneralizedTime
|
|
87
|
+
// discriminator). RFC 4211 §5 requires at least one of the two to be present.
|
|
88
|
+
var OPTIONAL_VALIDITY = schema.seq([
|
|
89
|
+
schema.trailing([
|
|
90
|
+
{ tag: 0, name: "notBefore", schema: schema.time(NS), explicit: true, emptyCode: "crmf/bad-validity" },
|
|
91
|
+
{ tag: 1, name: "notAfter", schema: schema.time(NS), explicit: true, emptyCode: "crmf/bad-validity" },
|
|
92
|
+
], { minTag: 0, maxTag: 1, unexpectedCode: "crmf/bad-validity", orderCode: "crmf/bad-validity" }),
|
|
93
|
+
], {
|
|
94
|
+
assert: "implicit", implicitTag: 4, code: "crmf/bad-validity", what: "OptionalValidity",
|
|
95
|
+
build: function (m, ctx) {
|
|
96
|
+
var f = m.fields;
|
|
97
|
+
if (!f.notBefore.present && !f.notAfter.present) {
|
|
98
|
+
throw ctx.E("crmf/bad-validity", "OptionalValidity must contain notBefore or notAfter (RFC 4211 §5)");
|
|
99
|
+
}
|
|
100
|
+
return { notBefore: f.notBefore.present ? f.notBefore.value : null, notAfter: f.notAfter.present ? f.notAfter.value : null };
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ---- ProofOfPossession (§4) ------------------------------------------
|
|
105
|
+
// raVerified [0] NULL — the RA has already verified POP out of band.
|
|
106
|
+
var POPO_RAVERIFIED = schema.decode(function (n, ctx) {
|
|
107
|
+
try { asn1.read.nullImplicit(n, 0); }
|
|
108
|
+
catch (e) { throw ctx.E("crmf/bad-popo", "raVerified [0] must be an IMPLICIT NULL", e); }
|
|
109
|
+
return { type: "raVerified" };
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// When poposkInput is present the POP signature is over the DER of the
|
|
113
|
+
// POPOSigningKeyInput VALUE (RFC 4211 §4.1), which is a SEQUENCE — but on the wire
|
|
114
|
+
// poposkInput is an IMPLICIT [0] field, so its bytes lead with the context tag
|
|
115
|
+
// (0xA0) rather than the SEQUENCE tag a verifier hashes. `asn1.sequenceTlv` recovers
|
|
116
|
+
// the SEQUENCE-tagged signed region from the field's content; `signedBytes` is that
|
|
117
|
+
// region and `bytes` keeps the raw wire [0] TLV, so a verifier can use either
|
|
118
|
+
// encoding its peer produced.
|
|
119
|
+
|
|
120
|
+
// POPOSigningKeyInput ::= SEQUENCE { authInfo CHOICE { sender [0] GeneralName,
|
|
121
|
+
// publicKeyMAC PKMACValue }, publicKey SubjectPublicKeyInfo } (§4.1). Structurally
|
|
122
|
+
// validated (not deferred raw), so an empty or malformed poposkInput fails closed
|
|
123
|
+
// rather than surfacing a signed region that was never a well-formed POP input.
|
|
124
|
+
// PKMACValue ::= SEQUENCE { algId AlgorithmIdentifier, value BIT STRING }.
|
|
125
|
+
var PKMAC_VALUE = schema.seq([
|
|
126
|
+
schema.field("algId", pkix.algorithmIdentifier(NS)),
|
|
127
|
+
schema.field("value", schema.bitString()),
|
|
128
|
+
], { assert: "sequence", arity: { exact: 2 }, code: "crmf/bad-popo", what: "PKMACValue" });
|
|
129
|
+
|
|
130
|
+
// authInfo CHOICE: sender [0] GeneralName is EXPLICIT (GeneralName is itself a
|
|
131
|
+
// CHOICE); publicKeyMAC is a bare PKMACValue SEQUENCE — disjoint by tag class.
|
|
132
|
+
var POPOSK_AUTH_INFO = schema.choice([
|
|
133
|
+
{ when: { tagClass: "context", tagNumber: 0 }, schema: schema.explicit(0, pkix.generalName(NS, { code: "crmf/bad-popo" }), { code: "crmf/bad-popo" }) },
|
|
134
|
+
{ when: { tagClass: "universal", tagNumber: TAGS.SEQUENCE }, schema: PKMAC_VALUE },
|
|
135
|
+
], { code: "crmf/bad-popo" });
|
|
136
|
+
|
|
137
|
+
var POPOSK_INPUT = schema.seq([
|
|
138
|
+
schema.field("authInfo", POPOSK_AUTH_INFO),
|
|
139
|
+
schema.field("publicKey", pkix.spki(NS)),
|
|
140
|
+
], { assert: "implicit", implicitTag: 0, arity: { exact: 2 }, code: "crmf/bad-popo", what: "POPOSigningKeyInput" });
|
|
141
|
+
|
|
142
|
+
// POPOSigningKey ::= SEQUENCE { poposkInput [0] POPOSigningKeyInput OPTIONAL,
|
|
143
|
+
// algorithmIdentifier AlgorithmIdentifier, signature BIT STRING } — IMPLICIT [1].
|
|
144
|
+
var POPO_SIGNING_KEY = schema.seq([
|
|
145
|
+
schema.optional("poposkInput", schema.decode(function (n, ctx) {
|
|
146
|
+
// Validate the full POPOSigningKeyInput structure fail-closed, then surface the
|
|
147
|
+
// raw [0] node plus its decoded publicKey (for the §4.1 template-match check).
|
|
148
|
+
var m = schema.walk(POPOSK_INPUT, n, ctx);
|
|
149
|
+
return { node: n, publicKey: m.fields.publicKey.value.result.bytes };
|
|
150
|
+
}), { tag: 0 }),
|
|
151
|
+
schema.field("algorithmIdentifier", pkix.algorithmIdentifier(NS)),
|
|
152
|
+
schema.field("signature", schema.bitString()),
|
|
153
|
+
], {
|
|
154
|
+
assert: "implicit", implicitTag: 1, code: "crmf/bad-popo", what: "POPOSigningKey",
|
|
155
|
+
build: function (m) {
|
|
156
|
+
var pin = m.fields.poposkInput;
|
|
157
|
+
return {
|
|
158
|
+
type: "signature",
|
|
159
|
+
poposkInput: pin.present ? { bytes: pin.node.bytes, signedBytes: asn1.sequenceTlv(pin.node), publicKey: pin.value.publicKey } : null,
|
|
160
|
+
algorithmIdentifier: m.fields.algorithmIdentifier.value.result,
|
|
161
|
+
signature: { unusedBits: m.fields.signature.value.unusedBits, bytes: m.fields.signature.value.bytes },
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// keyEncipherment [2] / keyAgreement [3] POPOPrivKey (§4). POPOPrivKey is a
|
|
167
|
+
// 5-arm CHOICE { thisMessage [0], subsequentMessage [1], dhMAC [2], agreeMAC [3],
|
|
168
|
+
// encryptedKey [4] }, so X.680 §31.2.7 forces the outer [2]/[3] tag to be EXPLICIT
|
|
169
|
+
// (a CHOICE has no single tag to replace). Validate that shell — a constructed
|
|
170
|
+
// wrapper around exactly one inner context [0]..[4] alternative — before surfacing
|
|
171
|
+
// the arm RAW, so a primitive / empty / mis-tagged node is rejected rather than
|
|
172
|
+
// reported to a verifier as a well-formed POP. The inner alternative's own value
|
|
173
|
+
// (incl. the encryptedKey [4] EnvelopedData) stays deferred to the verify layer.
|
|
174
|
+
// POPOPrivKey ::= CHOICE { thisMessage [0] BIT STRING (deprecated), subsequentMessage
|
|
175
|
+
// [1] SubsequentMessage, dhMAC [2] BIT STRING (deprecated), agreeMAC [3] PKMACValue,
|
|
176
|
+
// encryptedKey [4] EnvelopedData } (RFC 4211 §4.2). The deprecated arms are still
|
|
177
|
+
// parsed. The inner VALUE stays deferred raw (the EnvelopedData / MAC decode is a
|
|
178
|
+
// verify-layer concern), but the alternative's shape is validated fail-closed.
|
|
179
|
+
var POPOPRIVKEY_METHODS = { 0: "thisMessage", 1: "subsequentMessage", 2: "dhMAC", 3: "agreeMAC", 4: "encryptedKey" };
|
|
180
|
+
function popoPrivKey(type) {
|
|
181
|
+
return schema.decode(function (n, ctx) {
|
|
182
|
+
if (!n.children || n.children.length !== 1) {
|
|
183
|
+
throw ctx.E("crmf/bad-popo", type + " [" + n.tagNumber + "] POPOPrivKey must be an EXPLICIT wrapper around one alternative (RFC 4211 §4)");
|
|
184
|
+
}
|
|
185
|
+
var inner = n.children[0];
|
|
186
|
+
if (inner.tagClass !== "context" || inner.tagNumber < 0 || inner.tagNumber > 4) {
|
|
187
|
+
throw ctx.E("crmf/bad-popo", type + " POPOPrivKey alternative must be a context [0]..[4] CHOICE element (RFC 4211 §4)");
|
|
188
|
+
}
|
|
189
|
+
// RFC 4211 §4.2/§4.3 — the MAC alternatives dhMAC [2] / agreeMAC [3] are the
|
|
190
|
+
// key-agreement "fourth method"; key-encipherment POP uses only the
|
|
191
|
+
// thisMessage / subsequentMessage / encryptedKey methods, so a MAC arm under
|
|
192
|
+
// keyEncipherment is non-conforming.
|
|
193
|
+
if (type === "keyEncipherment" && (inner.tagNumber === 2 || inner.tagNumber === 3)) {
|
|
194
|
+
throw ctx.E("crmf/bad-popo", "keyEncipherment POP cannot use the MAC alternative " + POPOPRIVKEY_METHODS[inner.tagNumber] + " (RFC 4211 §4.2)");
|
|
195
|
+
}
|
|
196
|
+
// FORM: thisMessage [0] / subsequentMessage [1] / dhMAC [2] are primitive (BIT
|
|
197
|
+
// STRING / INTEGER / BIT STRING); agreeMAC [3] PKMACValue and encryptedKey [4]
|
|
198
|
+
// EnvelopedData are constructed (SEQUENCE). A wrong-form node is malformed.
|
|
199
|
+
var mustBeConstructed = inner.tagNumber === 3 || inner.tagNumber === 4;
|
|
200
|
+
if (mustBeConstructed !== !!inner.children) {
|
|
201
|
+
throw ctx.E("crmf/bad-popo", type + " POPOPrivKey [" + inner.tagNumber + "] has the wrong primitive/constructed form (RFC 4211 §4)");
|
|
202
|
+
}
|
|
203
|
+
// Each alternative's PAYLOAD is decoded fail-closed, not just its form:
|
|
204
|
+
// thisMessage [0] / dhMAC [2] are BIT STRINGs; subsequentMessage [1] an INTEGER
|
|
205
|
+
// in {0,1}; agreeMAC [3] a PKMACValue SEQUENCE; encryptedKey [4] an EnvelopedData.
|
|
206
|
+
if (inner.tagNumber === 0 || inner.tagNumber === 2) {
|
|
207
|
+
try { asn1.read.bitStringImplicit(inner, inner.tagNumber); }
|
|
208
|
+
catch (e) { throw ctx.E("crmf/bad-popo", POPOPRIVKEY_METHODS[inner.tagNumber] + " [" + inner.tagNumber + "] must be a BIT STRING (RFC 4211 §4.2)", e); }
|
|
209
|
+
}
|
|
210
|
+
if (inner.tagNumber === 1) {
|
|
211
|
+
var v;
|
|
212
|
+
try { v = asn1.read.integerImplicit(inner, 1); }
|
|
213
|
+
catch (e) { throw ctx.E("crmf/bad-popo", "subsequentMessage must be an INTEGER", e); }
|
|
214
|
+
if (v !== 0n && v !== 1n) {
|
|
215
|
+
throw ctx.E("crmf/bad-popo", "SubsequentMessage must be encrCert(0) or challengeResp(1) (RFC 4211 §4.2)");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (inner.tagNumber === 3) {
|
|
219
|
+
try { schema.embeddedDer(PKMAC_VALUE, asn1.sequenceTlv(inner), NS, { code: "crmf/bad-popo", what: "agreeMAC [3] PKMACValue" }); }
|
|
220
|
+
catch (e) { throw ctx.E("crmf/bad-popo", "agreeMAC [3] must be a PKMACValue SEQUENCE { algId, BIT STRING } (RFC 4211 §4.2)", e); }
|
|
221
|
+
}
|
|
222
|
+
// encryptedKey [4] EnvelopedData — validate the CMS structure fail-closed by
|
|
223
|
+
// composing the shared EnvelopedData decoder (retag the IMPLICIT [4] to the
|
|
224
|
+
// universal SEQUENCE it decodes as). The EncKeyWithID identity check lives inside
|
|
225
|
+
// the ENCRYPTED content, so it stays a verify-layer concern.
|
|
226
|
+
if (inner.tagNumber === 4) {
|
|
227
|
+
var env;
|
|
228
|
+
try { env = cms.walkEnvelopedData(asn1.decode(asn1.sequenceTlv(inner))); }
|
|
229
|
+
catch (e) { throw ctx.E("crmf/bad-popo", "encryptedKey [4] must be a well-formed EnvelopedData (RFC 4211 §4.2, RFC 5652 §6.1)", e); }
|
|
230
|
+
// RFC 4211 §4.2 — the enveloped content type MUST be id-ct-encKeyWithID (the
|
|
231
|
+
// ContentType OID is in the clear even though the content itself is encrypted).
|
|
232
|
+
if (env.encryptedContentInfo.contentType !== oid.byName("encKeyWithID")) {
|
|
233
|
+
throw ctx.E("crmf/bad-popo", "encryptedKey [4] EnvelopedData content type MUST be id-ct-encKeyWithID (RFC 4211 §4.2)");
|
|
234
|
+
}
|
|
235
|
+
// The encrypted key material MUST be present — CMS allows a detached
|
|
236
|
+
// EnvelopedData (encryptedContent OPTIONAL), but a POP with no key to verify
|
|
237
|
+
// or archive is meaningless.
|
|
238
|
+
if (env.encryptedContentInfo.encryptedContent == null) {
|
|
239
|
+
throw ctx.E("crmf/bad-popo", "encryptedKey [4] EnvelopedData MUST carry the encrypted key in encryptedContent (RFC 4211 §4.2)");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return { type: type, method: POPOPRIVKEY_METHODS[inner.tagNumber], bytes: n.bytes };
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ProofOfPossession ::= CHOICE { raVerified [0] NULL, signature [1] POPOSigningKey,
|
|
247
|
+
// keyEncipherment [2] POPOPrivKey, keyAgreement [3] POPOPrivKey }. Each arm yields
|
|
248
|
+
// the final object so the CertReqMsg build reads one uniform popo shape.
|
|
249
|
+
var PROOF_OF_POSSESSION = schema.choice([
|
|
250
|
+
{ when: { tagClass: "context", tagNumber: 0 }, schema: POPO_RAVERIFIED },
|
|
251
|
+
{ when: { tagClass: "context", tagNumber: 1 }, schema: schema.decode(function (n, ctx) { return schema.walk(POPO_SIGNING_KEY, n, ctx).result; }) },
|
|
252
|
+
{ when: { tagClass: "context", tagNumber: 2 }, schema: popoPrivKey("keyEncipherment") },
|
|
253
|
+
{ when: { tagClass: "context", tagNumber: 3 }, schema: popoPrivKey("keyAgreement") },
|
|
254
|
+
], { code: "crmf/bad-popo" });
|
|
255
|
+
|
|
256
|
+
// ---- Controls / regInfo (§5, §6, §7) ---------------------------------
|
|
257
|
+
// AttributeTypeAndValue ::= SEQUENCE { type OBJECT IDENTIFIER, value ANY DEFINED BY
|
|
258
|
+
// type }. The list is decoded; each value stays RAW (the per-OID value semantics
|
|
259
|
+
// are deferred). Shared shape for both controls and regInfo.
|
|
260
|
+
var CONTROL = schema.seq([
|
|
261
|
+
schema.field("type", schema.oidLeaf()),
|
|
262
|
+
schema.field("value", schema.any()),
|
|
263
|
+
], {
|
|
264
|
+
assert: "sequence", arity: { exact: 2 }, code: "crmf/bad-control", what: "AttributeTypeAndValue",
|
|
265
|
+
build: function (m, ctx) {
|
|
266
|
+
var t = m.fields.type.value;
|
|
267
|
+
return { type: t, name: ctx.oid.name(t) || null, value: m.fields.value.node.bytes };
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
function mapControls(m) { return m.items.map(function (it) { return it.value.result; }); }
|
|
271
|
+
// Controls ::= SEQUENCE SIZE(1..MAX) OF AttributeTypeAndValue (§5).
|
|
272
|
+
var CONTROLS = schema.seqOf(CONTROL, { assert: "sequence", min: 1, code: "crmf/bad-controls", what: "Controls", build: mapControls });
|
|
273
|
+
// regInfo ::= SEQUENCE SIZE(1..MAX) OF AttributeTypeAndValue (§7).
|
|
274
|
+
var REG_INFO = schema.seqOf(CONTROL, { assert: "sequence", min: 1, code: "crmf/bad-reg-info", what: "regInfo", build: mapControls });
|
|
275
|
+
|
|
276
|
+
// ---- CertTemplate (§5) — the IMPLICIT-TAGS core ----------------------
|
|
277
|
+
// CertTemplate ::= SEQUENCE { version [0], serialNumber [1], signingAlg [2],
|
|
278
|
+
// issuer [3] Name, validity [4] OptionalValidity, subject [5] Name, publicKey [6]
|
|
279
|
+
// SubjectPublicKeyInfo, issuerUID [7], subjectUID [8], extensions [9] } — every
|
|
280
|
+
// field IMPLICIT and OPTIONAL, so the whole body is one ascending [0..9] trailing
|
|
281
|
+
// run. issuer/subject Name and the [4] validity times are the CHOICE exceptions.
|
|
282
|
+
var CERT_TEMPLATE = schema.seq([
|
|
283
|
+
schema.trailing([
|
|
284
|
+
{ tag: 0, name: "version", schema: schema.implicitInteger(0) },
|
|
285
|
+
{ tag: 1, name: "serialNumber", schema: schema.implicitInteger(1) },
|
|
286
|
+
{ tag: 2, name: "signingAlg", schema: pkix.algorithmIdentifier(NS, { implicitTag: 2 }) },
|
|
287
|
+
{ tag: 3, name: "issuer", schema: crmfName(3) },
|
|
288
|
+
{ tag: 4, name: "validity", schema: OPTIONAL_VALIDITY },
|
|
289
|
+
{ tag: 5, name: "subject", schema: crmfName(5) },
|
|
290
|
+
{ tag: 6, name: "publicKey", schema: pkix.spki(NS, { implicitTag: 6 }) },
|
|
291
|
+
{ tag: 7, name: "issuerUID", schema: schema.implicitBitString(7) },
|
|
292
|
+
{ tag: 8, name: "subjectUID", schema: schema.implicitBitString(8) },
|
|
293
|
+
{ tag: 9, name: "extensions", schema: pkix.extensions(NS, { implicitTag: 9 }) },
|
|
294
|
+
], { minTag: 0, maxTag: 9, unexpectedCode: "crmf/bad-cert-template", orderCode: "crmf/bad-cert-template" }),
|
|
295
|
+
], {
|
|
296
|
+
assert: "sequence", code: "crmf/bad-cert-template", what: "CertTemplate",
|
|
297
|
+
build: function (m, ctx) {
|
|
298
|
+
var f = m.fields;
|
|
299
|
+
// RFC 4211 §5 — serialNumber, signingAlg, issuerUID, and subjectUID MUST be
|
|
300
|
+
// omitted from a CertTemplate: serialNumber and signingAlg are assigned by the
|
|
301
|
+
// CA, and the UID pair is deprecated. A requester must not dictate a
|
|
302
|
+
// CA-assigned value (a requester-chosen serialNumber is a real hazard), so a
|
|
303
|
+
// template that sets any of them is rejected fail-closed, not surfaced as
|
|
304
|
+
// acceptable. They stay in the field map (parsed, then rejected) so the
|
|
305
|
+
// diagnostic names the offending field rather than a bare tag number.
|
|
306
|
+
var caAssigned = ["serialNumber", "signingAlg", "issuerUID", "subjectUID"];
|
|
307
|
+
for (var i = 0; i < caAssigned.length; i++) {
|
|
308
|
+
if (f[caAssigned[i]].present) {
|
|
309
|
+
throw ctx.E("crmf/bad-cert-template", "CertTemplate " + caAssigned[i] + " MUST be omitted — it is CA-assigned or deprecated (RFC 4211 §5)");
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// version MUST be 2 (v3) when supplied; SHOULD be omitted (RFC 4211 §5).
|
|
313
|
+
if (f.version.present && f.version.value !== 2n) {
|
|
314
|
+
throw ctx.E("crmf/bad-version", "CertTemplate version MUST be 2 (v3) if supplied (RFC 4211 §5)");
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
version: f.version.present ? f.version.value : null,
|
|
318
|
+
issuer: f.issuer.present ? f.issuer.value : null,
|
|
319
|
+
validity: f.validity.present ? f.validity.value.result : null,
|
|
320
|
+
subject: f.subject.present ? f.subject.value : null,
|
|
321
|
+
publicKey: f.publicKey.present ? f.publicKey.value.result : null,
|
|
322
|
+
extensions: f.extensions.present ? f.extensions.value.result : null,
|
|
323
|
+
};
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// ---- CertRequest / CertReqMsg / CertReqMessages (§3, §5) --------------
|
|
328
|
+
// CertRequest ::= SEQUENCE { certReqId INTEGER, certTemplate CertTemplate,
|
|
329
|
+
// controls Controls OPTIONAL }. certReqId is a SIGNED INTEGER with no value
|
|
330
|
+
// constraint — a negative value (the RFC 9483 -1 sentinel) is legal.
|
|
331
|
+
var CERT_REQUEST = schema.seq([
|
|
332
|
+
schema.field("certReqId", schema.integerLeaf()),
|
|
333
|
+
schema.field("certTemplate", CERT_TEMPLATE),
|
|
334
|
+
schema.optional("controls", CONTROLS, { whenUniversal: [TAGS.SEQUENCE] }),
|
|
335
|
+
], {
|
|
336
|
+
assert: "sequence", code: "crmf/bad-cert-request", what: "CertRequest",
|
|
337
|
+
build: function (m) {
|
|
338
|
+
return {
|
|
339
|
+
certReqId: m.fields.certReqId.value,
|
|
340
|
+
certReqIdHex: m.fields.certReqId.node.content.toString("hex"),
|
|
341
|
+
certTemplate: m.fields.certTemplate.value.result,
|
|
342
|
+
controls: m.fields.controls.present ? m.fields.controls.value.result : null,
|
|
343
|
+
certReqBytes: m.node.bytes, // the exact CertRequest TLV the POP signature covers (§4.1)
|
|
344
|
+
};
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// CertReqMsg ::= SEQUENCE { certReq CertRequest, popo ProofOfPossession OPTIONAL,
|
|
349
|
+
// regInfo SEQUENCE OF AttributeTypeAndValue OPTIONAL }. popo is a CHOICE of context
|
|
350
|
+
// [0]..[3]; regInfo is a bare universal SEQUENCE, so the two OPTIONAL positions are
|
|
351
|
+
// disambiguated by tag class.
|
|
352
|
+
var CERT_REQ_MSG = schema.seq([
|
|
353
|
+
schema.field("certReq", CERT_REQUEST),
|
|
354
|
+
schema.optional("popo", PROOF_OF_POSSESSION, { tags: [0, 1, 2, 3] }),
|
|
355
|
+
schema.optional("regInfo", REG_INFO, { whenUniversal: [TAGS.SEQUENCE] }),
|
|
356
|
+
], {
|
|
357
|
+
assert: "sequence", code: "crmf/bad-cert-req-msg", what: "CertReqMsg",
|
|
358
|
+
build: function (m, ctx) {
|
|
359
|
+
var certReq = m.fields.certReq.value.result;
|
|
360
|
+
var popo = m.fields.popo.present ? m.fields.popo.value : null;
|
|
361
|
+
// A POP whose signature/MAC covers the certReq requires the CertTemplate to
|
|
362
|
+
// contain both subject and publicKey — otherwise there is no complete request
|
|
363
|
+
// to sign / MAC over, and a verifier would be bound to the wrong identity/key.
|
|
364
|
+
var complete = certReq.certTemplate.subject !== null && certReq.certTemplate.publicKey !== null;
|
|
365
|
+
// RFC 4211 §4.1 — for a signature POP, poposkInput's presence is fixed by the
|
|
366
|
+
// CertTemplate: it MUST be omitted when the template carries BOTH subject and
|
|
367
|
+
// publicKey (the signature is then over the DER of the CertRequest), and MUST be
|
|
368
|
+
// present otherwise.
|
|
369
|
+
if (popo && popo.type === "signature") {
|
|
370
|
+
if (complete && popo.poposkInput !== null) {
|
|
371
|
+
throw ctx.E("crmf/bad-popo", "poposkInput MUST be omitted when the CertTemplate contains both subject and publicKey (RFC 4211 §4.1)");
|
|
372
|
+
}
|
|
373
|
+
if (!complete && popo.poposkInput === null) {
|
|
374
|
+
throw ctx.E("crmf/bad-popo", "poposkInput MUST be present when the CertTemplate lacks subject or publicKey (RFC 4211 §4.1)");
|
|
375
|
+
}
|
|
376
|
+
// RFC 4211 §4.1 — the POPOSigningKeyInput publicKey MUST be exactly the template
|
|
377
|
+
// publicKey (both surfaced as canonical SubjectPublicKeyInfo DER), so a request
|
|
378
|
+
// cannot sign one key while asking for a certificate over a different key.
|
|
379
|
+
if (popo.poposkInput !== null && certReq.certTemplate.publicKey !== null &&
|
|
380
|
+
!popo.poposkInput.publicKey.equals(certReq.certTemplate.publicKey.bytes)) {
|
|
381
|
+
throw ctx.E("crmf/bad-popo", "POPOSigningKeyInput publicKey MUST equal the CertTemplate publicKey (RFC 4211 §4.1)");
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// RFC 4211 §4.2/§4.3 — a MAC-based POPOPrivKey (dhMAC / agreeMAC) proves
|
|
385
|
+
// possession by MACing the certReq, which MUST include both subject and publicKey.
|
|
386
|
+
if (popo && (popo.type === "keyEncipherment" || popo.type === "keyAgreement") &&
|
|
387
|
+
(popo.method === "dhMAC" || popo.method === "agreeMAC") && !complete) {
|
|
388
|
+
throw ctx.E("crmf/bad-popo", "a MAC-based key-agreement/encipherment POP (dhMAC/agreeMAC) requires the CertTemplate to contain both subject and publicKey (RFC 4211 §4.2/§4.3)");
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
certReq: certReq,
|
|
392
|
+
popo: popo,
|
|
393
|
+
regInfo: m.fields.regInfo.present ? m.fields.regInfo.value.result : null,
|
|
394
|
+
};
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// CertReqMessages ::= SEQUENCE SIZE(1..MAX) OF CertReqMsg (§3) — an empty sequence
|
|
399
|
+
// is malformed.
|
|
400
|
+
var CERT_REQ_MESSAGES = schema.seqOf(CERT_REQ_MSG, {
|
|
401
|
+
assert: "sequence", min: 1, code: "crmf/bad-cert-req-messages", what: "CertReqMessages",
|
|
402
|
+
build: function (m) { return { messages: m.items.map(function (it) { return it.value.result; }) }; },
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// ---- parse -----------------------------------------------------------
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* @primitive pki.schema.crmf.parse
|
|
409
|
+
* @signature pki.schema.crmf.parse(input) -> certReqMessages
|
|
410
|
+
* @since 0.1.17
|
|
411
|
+
* @status experimental
|
|
412
|
+
* @spec RFC 4211
|
|
413
|
+
* @related pki.schema.parse, pki.schema.csr.parse
|
|
414
|
+
*
|
|
415
|
+
* Parse a DER `Buffer` or a PEM string/Buffer into a structured `CertReqMessages`:
|
|
416
|
+
* `{ messages: [ { certReq, popo, regInfo } ] }`. Each `certReq` is
|
|
417
|
+
* `{ certReqId, certReqIdHex, certTemplate, controls, certReqBytes }`, and
|
|
418
|
+
* `certTemplate` carries the requestable certificate fields (`version`, `issuer`,
|
|
419
|
+
* `validity`, `subject`, `publicKey`, `extensions` — each `null` when absent). The
|
|
420
|
+
* CA-assigned / deprecated fields RFC 4211 §5 requires a request to omit
|
|
421
|
+
* (`serialNumber`, `signingAlg`, `issuerUID`, `subjectUID`) are rejected, not
|
|
422
|
+
* surfaced. `popo` is
|
|
423
|
+
* `null`, `{ type: "raVerified" }`, `{ type: "signature", poposkInput,
|
|
424
|
+
* algorithmIdentifier, signature }`, or `{ type: "keyEncipherment" |
|
|
425
|
+
* "keyAgreement", method, bytes }` (where `method` is the POPOPrivKey alternative —
|
|
426
|
+
* `thisMessage` / `subsequentMessage` / `dhMAC` / `agreeMAC` / `encryptedKey` — each
|
|
427
|
+
* structurally validated, the `encryptedKey` EnvelopedData included). When present, `poposkInput` is `{ bytes, signedBytes }`
|
|
428
|
+
* — `bytes` is the raw wire `[0]` TLV and `signedBytes` is the `POPOSigningKeyInput`
|
|
429
|
+
* re-tagged to the `SEQUENCE` DER the signature actually covers (RFC 4211 §4.1).
|
|
430
|
+
* `certReqBytes` is the exact `CertRequest` byte range a proof-of-possession
|
|
431
|
+
* verifier hashes when `poposkInput` is absent.
|
|
432
|
+
*
|
|
433
|
+
* Throws `CrmfError` when the bytes are not a well-formed `CertReqMessages`, and
|
|
434
|
+
* `Asn1Error` when the underlying DER is malformed.
|
|
435
|
+
*
|
|
436
|
+
* @example
|
|
437
|
+
* var m = pki.schema.crmf.parse(der);
|
|
438
|
+
* m.messages[0].certReq.certTemplate.subject.dn; // "CN=req.example"
|
|
439
|
+
*/
|
|
440
|
+
var parse = pkix.makeParser({
|
|
441
|
+
pemLabel: null, PemError: PemError, ErrorClass: CrmfError, prefix: "crmf",
|
|
442
|
+
what: "certificate request message", topSchema: CERT_REQ_MESSAGES, ns: NS,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* @primitive pki.schema.crmf.pemDecode
|
|
447
|
+
* @signature pki.schema.crmf.pemDecode(text, label?) -> Buffer
|
|
448
|
+
* @since 0.1.17
|
|
449
|
+
* @status experimental
|
|
450
|
+
* @spec RFC 7468, RFC 4211
|
|
451
|
+
* @related pki.schema.crmf.parse
|
|
452
|
+
*
|
|
453
|
+
* Extract the DER bytes from a PEM block (RFC 4211 registers no RFC 7468 label, so
|
|
454
|
+
* the first block is taken unless `label` is given — CRMF rides inside CMP / EST as
|
|
455
|
+
* DER in practice). Throws `PemError` on a missing envelope or a non-base64 body.
|
|
456
|
+
*
|
|
457
|
+
* @example
|
|
458
|
+
* var der = pki.schema.crmf.pemDecode(pemText);
|
|
459
|
+
*/
|
|
460
|
+
function pemDecode(text, label) { return pkix.pemDecode(text, label || null, PemError); }
|
|
461
|
+
|
|
462
|
+
// matches(root): a CertReqMessages is a universal SEQUENCE whose first CertReqMsg
|
|
463
|
+
// is a SEQUENCE whose first CertRequest is a SEQUENCE leading with a universal
|
|
464
|
+
// INTEGER (certReqId) then a universal SEQUENCE (certTemplate). This 3-level probe
|
|
465
|
+
// is disjoint from every registered root PROVIDED crmf is checked before
|
|
466
|
+
// ocsp-request: an OCSPRequest's tbsRequest never leads with the INTEGER-then-
|
|
467
|
+
// SEQUENCE pair (its children[0] is a context [0] version or a requestList).
|
|
468
|
+
function matches(root) {
|
|
469
|
+
if (!root || root.tagClass !== "universal" || root.tagNumber !== TAGS.SEQUENCE || !root.children || root.children.length < 1) return false;
|
|
470
|
+
var msg = root.children[0];
|
|
471
|
+
if (!msg.children || msg.tagClass !== "universal" || msg.tagNumber !== TAGS.SEQUENCE || msg.children.length < 1) return false;
|
|
472
|
+
var certReq = msg.children[0];
|
|
473
|
+
if (!certReq.children || certReq.tagClass !== "universal" || certReq.tagNumber !== TAGS.SEQUENCE || certReq.children.length < 2) return false;
|
|
474
|
+
var id = certReq.children[0], tpl = certReq.children[1];
|
|
475
|
+
return id.tagClass === "universal" && id.tagNumber === TAGS.INTEGER &&
|
|
476
|
+
tpl.tagClass === "universal" && tpl.tagNumber === TAGS.SEQUENCE && !!tpl.children;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
module.exports = {
|
|
480
|
+
parse: parse,
|
|
481
|
+
pemDecode: pemDecode,
|
|
482
|
+
matches: matches,
|
|
483
|
+
// The top schema, exposed (not on the curated pki.schema.crmf surface) so a
|
|
484
|
+
// round-trip test can prove schema.encode(schema, v) -> decode -> parse recovers
|
|
485
|
+
// v — the invariant that structurally validates the IMPLICIT/EXPLICIT tag handling.
|
|
486
|
+
certReqMessagesSchema: CERT_REQ_MESSAGES,
|
|
487
|
+
};
|
package/lib/schema-csr.js
CHANGED
|
@@ -106,7 +106,7 @@ var CERTIFICATION_REQUEST = pkix.signedEnvelope(NS, CERTIFICATION_REQUEST_INFO,
|
|
|
106
106
|
* @primitive pki.schema.csr.parse
|
|
107
107
|
* @signature pki.schema.csr.parse(input) -> csr
|
|
108
108
|
* @since 0.1.8
|
|
109
|
-
* @status
|
|
109
|
+
* @status stable
|
|
110
110
|
* @spec RFC 2986
|
|
111
111
|
* @related pki.schema.x509.parse, pki.schema.parse
|
|
112
112
|
*
|
|
@@ -129,7 +129,7 @@ var parse = pkix.makeParser({ pemLabel: "CERTIFICATE REQUEST", PemError: PemErro
|
|
|
129
129
|
* @primitive pki.schema.csr.pemDecode
|
|
130
130
|
* @signature pki.schema.csr.pemDecode(text, label?) -> Buffer
|
|
131
131
|
* @since 0.1.8
|
|
132
|
-
* @status
|
|
132
|
+
* @status stable
|
|
133
133
|
* @spec RFC 7468, RFC 2986
|
|
134
134
|
* @related pki.schema.csr.parse
|
|
135
135
|
*
|
|
@@ -146,7 +146,7 @@ function pemDecode(text, label) { return pkix.pemDecode(text, label || "CERTIFIC
|
|
|
146
146
|
* @primitive pki.schema.csr.pemEncode
|
|
147
147
|
* @signature pki.schema.csr.pemEncode(der, label?) -> string
|
|
148
148
|
* @since 0.1.8
|
|
149
|
-
* @status
|
|
149
|
+
* @status stable
|
|
150
150
|
* @spec RFC 7468
|
|
151
151
|
* @related pki.schema.csr.pemDecode
|
|
152
152
|
*
|