@blamejs/pki 0.1.22 → 0.1.24
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 +47 -0
- package/README.md +18 -6
- package/index.js +22 -12
- package/lib/asn1-der.js +126 -68
- package/lib/constants.js +46 -21
- package/lib/ct.js +54 -39
- package/lib/est.js +717 -0
- package/lib/framework-error.js +74 -34
- package/lib/oid.js +116 -60
- package/lib/path-validate.js +302 -135
- package/lib/schema-all.js +65 -41
- package/lib/schema-attrcert.js +101 -64
- package/lib/schema-cmp.js +152 -131
- package/lib/schema-cms.js +621 -163
- package/lib/schema-crl.js +43 -21
- package/lib/schema-crmf.js +136 -79
- package/lib/schema-csr.js +78 -15
- package/lib/schema-csrattrs.js +371 -0
- package/lib/schema-engine.js +92 -43
- package/lib/schema-ocsp.js +101 -55
- package/lib/schema-pkcs12.js +80 -64
- package/lib/schema-pkcs8.js +12 -12
- package/lib/schema-pkix.js +295 -105
- package/lib/schema-smime.js +39 -39
- package/lib/schema-tsp.js +108 -59
- package/lib/schema-x509.js +36 -25
- package/lib/webcrypto.js +161 -26
- package/package.json +3 -2
- package/sbom.cdx.json +6 -6
package/lib/schema-csr.js
CHANGED
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
* A CSR is self-signed only in the sense that the requester proves possession of
|
|
22
22
|
* the private key; unlike a certificate or CRL there is no inner signature
|
|
23
23
|
* algorithm to agree with, the subject MAY be empty, and the attribute set has
|
|
24
|
-
* no uniqueness constraint
|
|
24
|
+
* no uniqueness constraint -- the parser deliberately omits those three
|
|
25
25
|
* certificate/CRL guards.
|
|
26
26
|
*
|
|
27
27
|
* @card
|
|
28
|
-
* Parse DER / PEM PKCS#10 CSRs into structured, validated fields
|
|
28
|
+
* Parse DER / PEM PKCS#10 CSRs into structured, validated fields -- subject DN,
|
|
29
29
|
* public key, requested attributes, signature, fail-closed.
|
|
30
30
|
*/
|
|
31
31
|
|
|
@@ -45,23 +45,63 @@ var NS = pkix.makeNS("csr", CsrError, oid);
|
|
|
45
45
|
var NAME = pkix.name(NS);
|
|
46
46
|
var SPKI = pkix.spki(NS);
|
|
47
47
|
|
|
48
|
-
// CertificationRequestInfo version ::= INTEGER { v1(0) } (RFC 2986
|
|
49
|
-
// mandatory INTEGER whose ONLY legal value is 0
|
|
48
|
+
// CertificationRequestInfo version ::= INTEGER { v1(0) } (RFC 2986 sec. 4.1). A BARE
|
|
49
|
+
// mandatory INTEGER whose ONLY legal value is 0 -- the inverse of the cert/CRL
|
|
50
50
|
// readers, which reject 0 as the DER-forbidden default. Surface v1 as 1. A
|
|
51
51
|
// cert-shaped [0] EXPLICIT wrapper at this position makes asn1.read.integer throw
|
|
52
52
|
// asn1/* (fail-closed).
|
|
53
53
|
var CSR_VERSION = pkix.versionReader(NS, { "0": 1 });
|
|
54
54
|
|
|
55
|
-
// Attribute ::= SEQUENCE { type OID, values SET OF ANY }
|
|
55
|
+
// Attribute ::= SEQUENCE { type OID, values SET OF ANY } -- the shared pkix factory
|
|
56
56
|
// under the csr NS (values raw DER, SET SIZE(1..MAX), no uniqueness). The same
|
|
57
57
|
// factory serves the PKCS#8 private-key attributes.
|
|
58
58
|
var ATTRIBUTE = pkix.attribute(NS);
|
|
59
59
|
|
|
60
|
+
// The two RECOGNIZED PKCS#9 request attributes get their value syntax enforced
|
|
61
|
+
// (RFC 2985 sec. 5.4.1 challengePassword / sec. 5.4.2 extensionRequest): both
|
|
62
|
+
// are SINGLE VALUE TRUE, extensionRequest carries one Extensions value
|
|
63
|
+
// (RFC 5280 sec. 4.1, SIZE(1..MAX)) and challengePassword one DirectoryString
|
|
64
|
+
// of 1..255 characters. A recognized type with a malformed, multi-valued, or
|
|
65
|
+
// wrong-syntax value fails closed (csr/bad-attribute-value) -- a CA pipeline
|
|
66
|
+
// reading requested extensions off csr.attributes must never receive garbage
|
|
67
|
+
// under the extensionRequest OID. An UNRECOGNIZED attribute type stays opaque
|
|
68
|
+
// raw DER and never fails the parse, and duplicate attribute TYPES stay legal
|
|
69
|
+
// (RFC 2986 puts no uniqueness on the attributes SET). Registry rows keyed by
|
|
70
|
+
// the stable dotted OID, not a switch; values are still surfaced raw.
|
|
71
|
+
var EXTENSIONS = pkix.extensions(NS);
|
|
72
|
+
var DIRECTORY_STRING_TAGS = [
|
|
73
|
+
asn1.TAGS.UTF8_STRING, asn1.TAGS.PRINTABLE_STRING, asn1.TAGS.TELETEX_STRING,
|
|
74
|
+
asn1.TAGS.UNIVERSAL_STRING, asn1.TAGS.BMP_STRING,
|
|
75
|
+
];
|
|
76
|
+
var RECOGNIZED_ATTRIBUTE_VALUE = {};
|
|
77
|
+
RECOGNIZED_ATTRIBUTE_VALUE[oid.byName("extensionRequest")] = function (node, ctx) {
|
|
78
|
+
// Surface the requested extensions decoded (same {oid,name,critical,value}
|
|
79
|
+
// shape as a certificate's extensions) so a CA pipeline -- and the EST
|
|
80
|
+
// re-enroll guard -- reads them off the attribute instead of re-walking the
|
|
81
|
+
// raw Extensions bytes. Returned as an enrichment merged onto the attribute.
|
|
82
|
+
try { return { extensions: schema.walk(EXTENSIONS, node, ctx).result }; }
|
|
83
|
+
catch (e) {
|
|
84
|
+
throw ctx.E("csr/bad-attribute-value",
|
|
85
|
+
"extensionRequest value must be a well-formed Extensions SEQUENCE (RFC 2985 sec. 5.4.2): " + ((e && e.message) || String(e)), e);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
RECOGNIZED_ATTRIBUTE_VALUE[oid.byName("challengePassword")] = function (node, ctx) {
|
|
89
|
+
if (node.tagClass !== "universal" || DIRECTORY_STRING_TAGS.indexOf(node.tagNumber) === -1) {
|
|
90
|
+
throw ctx.E("csr/bad-attribute-value", "challengePassword must be a DirectoryString (RFC 2985 sec. 5.4.1)");
|
|
91
|
+
}
|
|
92
|
+
var s;
|
|
93
|
+
try { s = asn1.read.string(node); }
|
|
94
|
+
catch (e) { throw ctx.E("csr/bad-attribute-value", "challengePassword must be a well-formed DirectoryString (RFC 2985 sec. 5.4.1)", e); }
|
|
95
|
+
if (s.length < 1 || s.length > 255) {
|
|
96
|
+
throw ctx.E("csr/bad-attribute-value", "challengePassword must be 1..255 characters (RFC 2985 sec. 5.4.1)");
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
60
100
|
// CertificationRequestInfo ::= SEQUENCE { version, subject Name,
|
|
61
101
|
// subjectPKInfo SubjectPublicKeyInfo, attributes [0] IMPLICIT SET OF Attribute }.
|
|
62
102
|
// attributes is a REQUIRED field (a CRI omitting [0] is a missing-required-field
|
|
63
103
|
// fault, not silently accepted) modeled as an IMPLICIT [0] SET OF with min:0 (an
|
|
64
|
-
// empty attributes SET is legal). subject MAY be empty
|
|
104
|
+
// empty attributes SET is legal). subject MAY be empty -- no non-empty-DN guard.
|
|
65
105
|
var CERTIFICATION_REQUEST_INFO = schema.seq([
|
|
66
106
|
schema.field("version", CSR_VERSION),
|
|
67
107
|
schema.field("subject", NAME),
|
|
@@ -69,21 +109,35 @@ var CERTIFICATION_REQUEST_INFO = schema.seq([
|
|
|
69
109
|
schema.field("attributes", schema.implicitSetOf(0, ATTRIBUTE, { min: 0, code: "csr/bad-attributes", what: "attributes" })),
|
|
70
110
|
], {
|
|
71
111
|
assert: "sequence", code: "csr/bad-cri", what: "certificationRequestInfo",
|
|
72
|
-
build: function (m) {
|
|
112
|
+
build: function (m, ctx) {
|
|
113
|
+
var attributes = m.fields.attributes.value.items.map(function (it) {
|
|
114
|
+
var a = it.value.result;
|
|
115
|
+
var checkValue = RECOGNIZED_ATTRIBUTE_VALUE[a.type];
|
|
116
|
+
if (checkValue) {
|
|
117
|
+
var valueItems = it.value.fields.values.value.items;
|
|
118
|
+
// Both recognized PKCS#9 types are SINGLE VALUE TRUE (RFC 2985 sec. 5.4).
|
|
119
|
+
if (valueItems.length !== 1) {
|
|
120
|
+
throw ctx.E("csr/bad-attribute-value", (a.name || a.type) + " is a SINGLE VALUE attribute (RFC 2985 sec. 5.4)");
|
|
121
|
+
}
|
|
122
|
+
var enriched = checkValue(valueItems[0].node, ctx);
|
|
123
|
+
if (enriched) { Object.keys(enriched).forEach(function (k) { a[k] = enriched[k]; }); }
|
|
124
|
+
}
|
|
125
|
+
return a;
|
|
126
|
+
});
|
|
73
127
|
return {
|
|
74
128
|
version: m.fields.version.value,
|
|
75
|
-
subject: m.fields.subject.value.result, // Name is a seqOf
|
|
129
|
+
subject: m.fields.subject.value.result, // Name is a seqOf -> field.value is the match; .result is the {rdns, dn} build
|
|
76
130
|
subjectPublicKeyInfo: m.fields.subjectPKInfo.value.result,
|
|
77
|
-
attributes:
|
|
131
|
+
attributes: attributes,
|
|
78
132
|
};
|
|
79
133
|
},
|
|
80
134
|
});
|
|
81
135
|
|
|
82
136
|
// CertificationRequest ::= SEQUENCE { certificationRequestInfo, signatureAlgorithm,
|
|
83
|
-
// signature BIT STRING }
|
|
137
|
+
// signature BIT STRING } -- the shared SIGNED envelope. DIVERGENCE from the
|
|
84
138
|
// cert/CRL builders: OMIT the outer-vs-inner signatureAlgorithm agreement check
|
|
85
139
|
// (the CRI has no inner signature AlgorithmIdentifier) and the non-empty-subject
|
|
86
|
-
// guard (a CSR subject MAY be empty). The omission is structural
|
|
140
|
+
// guard (a CSR subject MAY be empty). The omission is structural -- this build
|
|
87
141
|
// simply never references the agreement bytes.
|
|
88
142
|
var CERTIFICATION_REQUEST = pkix.signedEnvelope(NS, CERTIFICATION_REQUEST_INFO, {
|
|
89
143
|
code: "csr/not-a-certification-request", what: "CertificationRequest",
|
|
@@ -116,12 +170,15 @@ var CERTIFICATION_REQUEST = pkix.signedEnvelope(NS, CERTIFICATION_REQUEST_INFO,
|
|
|
116
170
|
* Every field is validated on the way in; a malformed CertificationRequest /
|
|
117
171
|
* CertificationRequestInfo throws a typed `CsrError` (`csr/*`) and a leaf-level
|
|
118
172
|
* codec fault surfaces as `asn1/*`. Attribute values are returned as raw DER
|
|
119
|
-
* buffers so an unrecognized attribute type never fails the parse
|
|
173
|
+
* buffers so an unrecognized attribute type never fails the parse; the
|
|
174
|
+
* `extensionRequest` attribute additionally carries its requested extensions
|
|
175
|
+
* decoded on `.extensions` (the `{ oid, name, critical, value }` shape a
|
|
176
|
+
* certificate's extensions use).
|
|
120
177
|
*
|
|
121
178
|
* @example
|
|
122
179
|
* var csr = pki.schema.csr.parse(der);
|
|
123
|
-
* csr.subject.dn; //
|
|
124
|
-
* csr.attributes[0].type; //
|
|
180
|
+
* csr.subject.dn; // -> "CN=req.example"
|
|
181
|
+
* csr.attributes[0].type; // -> "1.2.840.113549.1.9.14"
|
|
125
182
|
*/
|
|
126
183
|
var parse = pkix.makeParser({ pemLabel: "CERTIFICATE REQUEST", PemError: PemError, ErrorClass: CsrError, prefix: "csr", what: "certification request", topSchema: CERTIFICATION_REQUEST, ns: NS });
|
|
127
184
|
|
|
@@ -158,7 +215,7 @@ function pemDecode(text, label) { return pkix.pemDecode(text, label || "CERTIFIC
|
|
|
158
215
|
function pemEncode(der, label) { return pkix.pemEncode(der, label || "CERTIFICATE REQUEST", PemError); }
|
|
159
216
|
|
|
160
217
|
// A CertificationRequest shares the outer SEQUENCE-of-3 shape with a certificate
|
|
161
|
-
// and a CRL; a CSR is distinguished by its CertificationRequestInfo
|
|
218
|
+
// and a CRL; a CSR is distinguished by its CertificationRequestInfo -- a SEQUENCE
|
|
162
219
|
// of EXACTLY four children {version INTEGER, subject SEQUENCE, subjectPKInfo
|
|
163
220
|
// SEQUENCE, attributes [0]}. The mandatory trailing context-[0] attributes
|
|
164
221
|
// element is what a cert (whose tbs leads with [0] EXPLICIT version or an INTEGER
|
|
@@ -182,4 +239,10 @@ module.exports = {
|
|
|
182
239
|
pemDecode: pemDecode,
|
|
183
240
|
pemEncode: pemEncode,
|
|
184
241
|
matches: matches,
|
|
242
|
+
// The top schema, exposed (not on the curated pki.schema.csr surface) so a
|
|
243
|
+
// composer can drive the encode direction -- schema.encode(schema, v) -> a
|
|
244
|
+
// CertificationRequest -- and prove the round-trip. The EST builder uses it
|
|
245
|
+
// to re-encode a CSR with added attributes (the crmf certReqMessagesSchema
|
|
246
|
+
// precedent).
|
|
247
|
+
certificationRequestSchema: CERTIFICATION_REQUEST,
|
|
185
248
|
};
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @module pki.schema.csrattrs
|
|
6
|
+
* @nav Schema
|
|
7
|
+
* @title CSR Attributes
|
|
8
|
+
* @order 185
|
|
9
|
+
* @slug csrattrs
|
|
10
|
+
*
|
|
11
|
+
* @intro
|
|
12
|
+
* The EST CSR Attributes wire format per RFC 8951 sec. 3.5 (the RFC 7030
|
|
13
|
+
* sec. 4.5.2 ASN.1 was syntactically broken, erratum 4384) with the RFC 9908
|
|
14
|
+
* template structures. `parse` turns a DER `Buffer` into
|
|
15
|
+
* `{ items }` -- `CsrAttrs ::= SEQUENCE SIZE (0..MAX) OF AttrOrOID`, where
|
|
16
|
+
* `AttrOrOID ::= CHOICE { oid OBJECT IDENTIFIER, attribute Attribute }`
|
|
17
|
+
* disambiguates on the universal tag. An empty SEQUENCE is a COMPLETE valid
|
|
18
|
+
* document ("no additional information desired"). Each item surfaces its
|
|
19
|
+
* `kind` (`"oid"` / `"attribute"`), the `oid` (dotted) and registry `name`;
|
|
20
|
+
* an attribute keeps its raw `values` and, for the three types RFC 9908
|
|
21
|
+
* gives meaning, a decoded view: `id-ExtensionReq` -> `extensions`, the
|
|
22
|
+
* `ecPublicKey` / `rsaEncryption` key-type conventions -> `curve` / `keySize`,
|
|
23
|
+
* and `id-aa-certificationRequestInfoTemplate` -> a fully-decoded `template`.
|
|
24
|
+
*
|
|
25
|
+
* Structure is strict-DER throughout, but UNKNOWN OIDs and attribute types
|
|
26
|
+
* are TOLERATED -- surfaced raw, never a parse fault ("the client MUST ignore
|
|
27
|
+
* any OID or attribute it does not recognize", RFC 8951 sec. 4.5.2). The
|
|
28
|
+
* RFC 9908 semantic MUSTs that ARE enforced fail closed with a typed
|
|
29
|
+
* `csrattrs/*` code: at most one `id-ExtensionReq` attribute, its values a SET
|
|
30
|
+
* of exactly one `Extensions`, template version v1(0), and a template's inner
|
|
31
|
+
* attributes carrying at most one `id-aa-extensionReqTemplate` and never both
|
|
32
|
+
* extension-request kinds. Template PRIORITY (a client that understands the
|
|
33
|
+
* template ignores the legacy elements, RFC 9908 sec. 4) is a `pki.est` builder
|
|
34
|
+
* rule, not a parse rejection -- parse surfaces everything.
|
|
35
|
+
*
|
|
36
|
+
* There is NO `pemDecode` / `pemEncode`: no RFC 7468 label exists for
|
|
37
|
+
* CsrAttrs; the wire encoding is bare RFC 4648 base64 handled by the EST
|
|
38
|
+
* transfer codec (`pki.est.transferDecode`), not a PEM envelope.
|
|
39
|
+
*
|
|
40
|
+
* @card
|
|
41
|
+
* Parse DER EST CSR Attributes (RFC 8951 / RFC 9908) into ordered items --
|
|
42
|
+
* bare OIDs, attributes with raw values, decoded extension requests / key-type
|
|
43
|
+
* conventions / request-info templates; unknown types tolerated, fail-closed.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
var asn1 = require("./asn1-der");
|
|
47
|
+
var schema = require("./schema-engine");
|
|
48
|
+
var pkix = require("./schema-pkix");
|
|
49
|
+
var oid = require("./oid");
|
|
50
|
+
var frameworkError = require("./framework-error");
|
|
51
|
+
|
|
52
|
+
var CsrattrsError = frameworkError.CsrattrsError;
|
|
53
|
+
|
|
54
|
+
var NS = pkix.makeNS("csrattrs", CsrattrsError, oid);
|
|
55
|
+
var T = asn1.TAGS;
|
|
56
|
+
|
|
57
|
+
// The base Attribute is SET SIZE(1..MAX); ATTRIBUTE_KEYTYPE lowers the floor to 0
|
|
58
|
+
// ONLY for the ecPublicKey / rsaEncryption key-type hints, whose empty values SET
|
|
59
|
+
// means "any key of this type" (RFC 9908 sec. 3.2). Every other attribute -- and
|
|
60
|
+
// every template inner attribute -- keeps the non-empty floor, so an empty
|
|
61
|
+
// challengePassword or unknown attribute still fails closed.
|
|
62
|
+
// ATTRIBUTE keeps the base SET SIZE(1..MAX); ATTRIBUTE_OPTIONAL_VALUES lowers the
|
|
63
|
+
// floor to 0. Only the recognized NON-KEY attributes that require a value
|
|
64
|
+
// (id-ExtensionReq, the request-info template) use the strict form; a key-type
|
|
65
|
+
// hint's empty SET means "any key of this type" (RFC 9908 sec. 3.2), and an
|
|
66
|
+
// UNKNOWN attribute (a key type this build does not know yet, or any other OID) is
|
|
67
|
+
// TOLERATED with an empty SET too -- RFC 8951 ignores what it does not recognize,
|
|
68
|
+
// so an unfamiliar OID must not fail the whole response.
|
|
69
|
+
var ATTRIBUTE = pkix.attribute(NS);
|
|
70
|
+
var ATTRIBUTE_OPTIONAL_VALUES = pkix.attribute(NS, { minValues: 0 });
|
|
71
|
+
var EXTENSIONS = pkix.extensions(NS);
|
|
72
|
+
var ALGORITHM_IDENTIFIER = pkix.algorithmIdentifier(NS);
|
|
73
|
+
|
|
74
|
+
// The RFC 9908 semantically-meaningful attribute OIDs (registry, never literals).
|
|
75
|
+
var OID_EXTENSION_REQUEST = oid.byName("extensionRequest"); // id-ExtensionReq
|
|
76
|
+
var OID_EC_PUBLIC_KEY = oid.byName("ecPublicKey");
|
|
77
|
+
var OID_RSA_ENCRYPTION = oid.byName("rsaEncryption");
|
|
78
|
+
var OID_TEMPLATE = oid.byName("certificationRequestInfoTemplate");
|
|
79
|
+
var OID_EXT_REQ_TEMPLATE = oid.byName("extensionReqTemplate");
|
|
80
|
+
|
|
81
|
+
// The recognized non-key attributes that MUST carry a value (empty SET rejected).
|
|
82
|
+
var STRICT_VALUES_OIDS = new Set([OID_EXTENSION_REQUEST, OID_TEMPLATE]);
|
|
83
|
+
|
|
84
|
+
// The recognized public-key type OIDs -- used to FLAG a parsed attribute isKeyType
|
|
85
|
+
// for the enroll planner (rsaEncryption / ecPublicKey are only the RFC 9908 sec. 3.2
|
|
86
|
+
// examples; a no-parameter key like Ed25519 or an ML-DSA / SLH-DSA / ML-KEM type is
|
|
87
|
+
// equally valid). Built by registered NAME (a typo fails at load), never literals.
|
|
88
|
+
var KEY_TYPE_OIDS = new Set([
|
|
89
|
+
"rsaEncryption", "ecPublicKey", "Ed25519", "Ed448", "X25519", "X448",
|
|
90
|
+
"id-ml-dsa-44", "id-ml-dsa-65", "id-ml-dsa-87",
|
|
91
|
+
"id-ml-kem-512", "id-ml-kem-768", "id-ml-kem-1024",
|
|
92
|
+
"id-slh-dsa-sha2-128s", "id-slh-dsa-sha2-128f", "id-slh-dsa-sha2-192s",
|
|
93
|
+
"id-slh-dsa-sha2-192f", "id-slh-dsa-sha2-256s", "id-slh-dsa-sha2-256f",
|
|
94
|
+
"id-slh-dsa-shake-128s", "id-slh-dsa-shake-128f", "id-slh-dsa-shake-192s",
|
|
95
|
+
"id-slh-dsa-shake-192f", "id-slh-dsa-shake-256s", "id-slh-dsa-shake-256f",
|
|
96
|
+
].map(function (nm) {
|
|
97
|
+
var d = oid.byName(nm);
|
|
98
|
+
if (typeof d !== "string") throw new Error("csrattrs key-type seed " + JSON.stringify(nm) + " is not a registered OID name");
|
|
99
|
+
return d;
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
// ---- RFC 9908 sec. 3.4 CertificationRequestInfoTemplate ------------------
|
|
103
|
+
|
|
104
|
+
// version MUST be v1(0). A dedicated leaf (not pkix.versionReader) so the reject
|
|
105
|
+
// carries the template-specific code the RFC 9908 sec. 3.4 MUST names.
|
|
106
|
+
var TEMPLATE_VERSION = schema.decode(function (n, ctx) {
|
|
107
|
+
var v = asn1.read.integer(n);
|
|
108
|
+
if (v !== 0n) throw ctx.E("csrattrs/bad-template-version", "CertificationRequestInfoTemplate version must be v1(0) (RFC 9908 sec. 3.4)");
|
|
109
|
+
return 0;
|
|
110
|
+
}, function () { return asn1.build.integer(0n); });
|
|
111
|
+
|
|
112
|
+
// SingleAttributeTemplate ::= SEQUENCE { type OID, value ANY OPTIONAL } -- a
|
|
113
|
+
// value-absent element means "the client fills this in" (RFC 9908 sec. 3.4). The
|
|
114
|
+
// trailing OPTIONAL ANY is the algorithmIdentifier-parameters shape.
|
|
115
|
+
var SINGLE_ATTR_TEMPLATE = schema.seq([
|
|
116
|
+
schema.field("type", schema.oidLeaf()),
|
|
117
|
+
schema.optional("value", schema.any(), { whenAny: true }),
|
|
118
|
+
], {
|
|
119
|
+
assert: "sequence", arity: { min: 1, max: 2 }, code: "csrattrs/bad-name-template", what: "SingleAttributeTemplate",
|
|
120
|
+
build: function (m, ctx) {
|
|
121
|
+
return { type: m.fields.type.value, name: ctx.oid.name(m.fields.type.value) || null,
|
|
122
|
+
value: m.fields.value.present ? m.fields.value.node.bytes : null };
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// RelativeDistinguishedNameTemplate ::= SET SIZE (1..MAX) OF SingleAttributeTemplate.
|
|
127
|
+
var RDN_TEMPLATE = schema.setOf(SINGLE_ATTR_TEMPLATE, {
|
|
128
|
+
assert: "set", min: 1, code: "csrattrs/bad-name-template", what: "RelativeDistinguishedNameTemplate",
|
|
129
|
+
build: function (m) { return m.items.map(function (it) { return it.value.result; }); },
|
|
130
|
+
});
|
|
131
|
+
// NameTemplate ::= RDNSequence (a SEQUENCE OF RDN-template SETs) -> array of RDNs,
|
|
132
|
+
// each an array of { type, name, value } (value null = "the client fills it in").
|
|
133
|
+
var NAME_TEMPLATE = schema.seqOf(RDN_TEMPLATE, {
|
|
134
|
+
assert: "sequence", code: "csrattrs/bad-name-template", what: "NameTemplate",
|
|
135
|
+
build: function (m) { return m.items.map(function (it) { return it.value.result; }); },
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// SubjectPublicKeyInfoTemplate ::= SEQUENCE { algorithm AlgorithmIdentifier,
|
|
139
|
+
// subjectPublicKey BIT STRING OPTIONAL } -- the key is OPTIONAL (present ONLY
|
|
140
|
+
// as an RSA modulus-size placeholder), so pkix.spki (key required, arity 2)
|
|
141
|
+
// cannot be reused.
|
|
142
|
+
var SPKI_TEMPLATE = schema.seq([
|
|
143
|
+
schema.field("algorithm", ALGORITHM_IDENTIFIER),
|
|
144
|
+
schema.optional("placeholderKey", schema.bitString(), { whenUniversal: [T.BIT_STRING] }),
|
|
145
|
+
], {
|
|
146
|
+
// subjectPKInfo is [0] IMPLICIT (RFC 9908 sec. 3.4) -- the context tag replaces
|
|
147
|
+
// the universal SEQUENCE tag, so the node is context-[0] constructed and its
|
|
148
|
+
// children are read directly (the pwri keyDerivationAlgorithm [0] precedent).
|
|
149
|
+
assert: "implicit", implicitTag: 0, arity: { min: 1, max: 2 }, code: "csrattrs/bad-spki-template", what: "SubjectPublicKeyInfoTemplate",
|
|
150
|
+
build: function (m) {
|
|
151
|
+
return { algorithm: m.fields.algorithm.value.result,
|
|
152
|
+
placeholderKey: m.fields.placeholderKey.present ? m.fields.placeholderKey.value.bytes : null };
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// ExtensionTemplate ::= SEQUENCE { extnID OID, critical BOOLEAN DEFAULT FALSE,
|
|
157
|
+
// extnValue OCTET STRING OPTIONAL } -- extnValue absent means "client supplies";
|
|
158
|
+
// an explicit critical FALSE is non-DER (BOOLEAN DEFAULT FALSE). Not reusable
|
|
159
|
+
// from pkix.extension (whose extnValue is required), so the DER-critical rule
|
|
160
|
+
// is applied here directly.
|
|
161
|
+
var EXTENSION_TEMPLATE = schema.decode(function (n, ctx) {
|
|
162
|
+
if (!schema.isUniversal(n, T.SEQUENCE) || !n.children || n.children.length < 1 || n.children.length > 3) {
|
|
163
|
+
throw ctx.E("csrattrs/bad-extension-template", "ExtensionTemplate must be a SEQUENCE of {extnID, critical?, extnValue?}");
|
|
164
|
+
}
|
|
165
|
+
var k = n.children, i = 1;
|
|
166
|
+
var extnID = asn1.read.oid(k[0]);
|
|
167
|
+
var critical = false, value = null;
|
|
168
|
+
if (i < k.length && schema.isUniversal(k[i], T.BOOLEAN)) {
|
|
169
|
+
critical = asn1.read.boolean(k[i]);
|
|
170
|
+
if (critical === false) throw ctx.E("csrattrs/bad-extension-template", "an explicit critical FALSE must be omitted (BOOLEAN DEFAULT FALSE)");
|
|
171
|
+
i += 1;
|
|
172
|
+
}
|
|
173
|
+
if (i < k.length) { value = asn1.read.octetString(k[i]); i += 1; }
|
|
174
|
+
if (i !== k.length) throw ctx.E("csrattrs/bad-extension-template", "ExtensionTemplate has an unexpected trailing element");
|
|
175
|
+
return { oid: extnID, name: ctx.oid.name(extnID) || null, critical: critical, value: value };
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ExtensionReqTemplate ::= ExtensionTemplates ::= SEQUENCE SIZE (1..MAX) OF
|
|
179
|
+
// ExtensionTemplate (RFC 9908 sec. 3.4) -- the id-aa-extensionReqTemplate value
|
|
180
|
+
// is a SEQUENCE OF ExtensionTemplate, not a single one.
|
|
181
|
+
var EXTENSION_REQ_TEMPLATE = schema.seqOf(EXTENSION_TEMPLATE, {
|
|
182
|
+
assert: "sequence", min: 1, code: "csrattrs/bad-extension-template", what: "ExtensionReqTemplate",
|
|
183
|
+
build: function (m) { return m.items.map(function (it) { return it.value; }); },
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// CertificationRequestInfoTemplate ::= SEQUENCE { version, subject NameTemplate
|
|
187
|
+
// OPTIONAL, subjectPKInfo [0] IMPLICIT OPTIONAL, attributes [1] IMPLICIT
|
|
188
|
+
// Attributes }. attributes is present (may be an empty SET). The RFC 9908
|
|
189
|
+
// sec. 3.4 inner-attribute MUSTs (<=1 extensionReqTemplate; not both
|
|
190
|
+
// extension-request kinds; a values SET of exactly one ExtensionTemplate) are
|
|
191
|
+
// enforced in the build.
|
|
192
|
+
var CERT_REQ_INFO_TEMPLATE = schema.seq([
|
|
193
|
+
schema.field("version", TEMPLATE_VERSION),
|
|
194
|
+
schema.optional("subject", NAME_TEMPLATE, { whenUniversal: [T.SEQUENCE] }),
|
|
195
|
+
schema.optional("subjectPKInfo", SPKI_TEMPLATE, { tag: 0 }),
|
|
196
|
+
schema.field("attributes", schema.implicitSetOf(1, ATTRIBUTE, { min: 0, code: "csrattrs/bad-template-attrs", what: "template attributes" })),
|
|
197
|
+
], {
|
|
198
|
+
assert: "sequence", code: "csrattrs/bad-template", what: "CertificationRequestInfoTemplate",
|
|
199
|
+
build: function (m, ctx) {
|
|
200
|
+
var attributes = m.fields.attributes.value.items.map(function (it) { return it.value.result; });
|
|
201
|
+
var extReqTemplates = 0, extReqs = 0, hasExtReq = false, extensionTemplates = [], extensions = null;
|
|
202
|
+
for (var i = 0; i < attributes.length; i++) {
|
|
203
|
+
var a = attributes[i];
|
|
204
|
+
if (a.type === OID_EXTENSION_REQUEST) {
|
|
205
|
+
hasExtReq = true;
|
|
206
|
+
extReqs += 1;
|
|
207
|
+
// A template id-ExtensionReq carries one Extensions value (RFC 9908 sec. 3.2);
|
|
208
|
+
// decode it so a client reads the requirements off .extensions instead of
|
|
209
|
+
// re-parsing raw bytes (the top-level extensionRequest exposes it the same way).
|
|
210
|
+
if (a.values.length !== 1) throw NS.E("csrattrs/bad-template-attrs", "a template id-ExtensionReq values must be a SET of exactly one Extensions (RFC 9908 sec. 3.2)");
|
|
211
|
+
extensions = schema.walk(EXTENSIONS, asn1.decode(a.values[0]), ctx).result;
|
|
212
|
+
}
|
|
213
|
+
if (a.type === OID_EXT_REQ_TEMPLATE) {
|
|
214
|
+
extReqTemplates += 1;
|
|
215
|
+
// The attribute's values SET holds exactly one ExtensionReqTemplate
|
|
216
|
+
// (RFC 9908 sec. 3.4); that value is itself a SEQUENCE OF ExtensionTemplate.
|
|
217
|
+
if (a.values.length !== 1) throw NS.E("csrattrs/bad-template-attrs", "an id-aa-extensionReqTemplate values must be a SET of exactly one ExtensionReqTemplate (RFC 9908 sec. 3.4)");
|
|
218
|
+
extensionTemplates = extensionTemplates.concat(schema.walk(EXTENSION_REQ_TEMPLATE, asn1.decode(a.values[0]), ctx).result);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (extReqs > 1) throw NS.E("csrattrs/bad-template-attrs", "a template must not carry more than one id-ExtensionReq (RFC 9908 sec. 3.2)");
|
|
222
|
+
if (extReqTemplates > 1) throw NS.E("csrattrs/bad-template-attrs", "a template must not carry more than one id-aa-extensionReqTemplate (RFC 9908 sec. 3.4)");
|
|
223
|
+
if (extReqTemplates >= 1 && hasExtReq) throw NS.E("csrattrs/bad-template-attrs", "a template must not carry both id-ExtensionReq and id-aa-extensionReqTemplate (RFC 9908 sec. 3.4)");
|
|
224
|
+
return {
|
|
225
|
+
version: m.fields.version.value,
|
|
226
|
+
subject: m.fields.subject.present ? m.fields.subject.value.result : null,
|
|
227
|
+
subjectPKInfo: m.fields.subjectPKInfo.present ? m.fields.subjectPKInfo.value.result : null,
|
|
228
|
+
attributes: attributes,
|
|
229
|
+
extensions: extensions,
|
|
230
|
+
extensionTemplates: extensionTemplates,
|
|
231
|
+
};
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// ---- AttrOrOID + the semantic enrichment ---------------------------------
|
|
236
|
+
|
|
237
|
+
// Decode the three RFC 9908-meaningful attribute types onto an already-walked
|
|
238
|
+
// Attribute { type, name, values }. Unknown types keep only their raw values.
|
|
239
|
+
// A recognized public-key type is flagged isKeyType so a consumer (the pki.est
|
|
240
|
+
// enroll planner) treats every accepted key type uniformly, not just RSA/EC.
|
|
241
|
+
function _enrichAttribute(item, ctx) {
|
|
242
|
+
if (KEY_TYPE_OIDS.has(item.oid)) item.isKeyType = true;
|
|
243
|
+
if (item.oid === OID_EXTENSION_REQUEST) {
|
|
244
|
+
if (item.values.length !== 1) throw ctx.E("csrattrs/bad-extension-req", "an id-ExtensionReq attribute values must be a SET of exactly one Extensions (RFC 9908 sec. 3.2)");
|
|
245
|
+
item.extensions = schema.walk(EXTENSIONS, asn1.decode(item.values[0]), ctx).result;
|
|
246
|
+
} else if (item.oid === OID_EC_PUBLIC_KEY) {
|
|
247
|
+
// A key-type hint's values SET is empty ("any curve") or a singleton naming
|
|
248
|
+
// ONE curve (RFC 9908 sec. 3.2); a multi-valued SET is ambiguous -- fail
|
|
249
|
+
// closed rather than letting DER ordering pick the advertised constraint.
|
|
250
|
+
if (item.values.length > 1) throw ctx.E("csrattrs/bad-key-type-attr", "an ecPublicKey attribute values must be empty or a SET of exactly one named-curve OBJECT IDENTIFIER (RFC 9908 sec. 3.2)");
|
|
251
|
+
if (item.values.length === 1) {
|
|
252
|
+
try { item.curve = asn1.read.oid(asn1.decode(item.values[0])); }
|
|
253
|
+
catch (e) { throw ctx.E("csrattrs/bad-key-type-attr", "an ecPublicKey attribute value must be a named-curve OBJECT IDENTIFIER (RFC 9908 sec. 3.2)", e); }
|
|
254
|
+
}
|
|
255
|
+
} else if (item.oid === OID_RSA_ENCRYPTION) {
|
|
256
|
+
// Empty ("any size") or a singleton naming ONE key size; reject a multi-valued SET.
|
|
257
|
+
if (item.values.length > 1) throw ctx.E("csrattrs/bad-key-type-attr", "an rsaEncryption attribute values must be empty or a SET of exactly one INTEGER key size (RFC 9908 sec. 3.2)");
|
|
258
|
+
if (item.values.length === 1) {
|
|
259
|
+
var sz;
|
|
260
|
+
try { sz = asn1.read.integer(asn1.decode(item.values[0])); }
|
|
261
|
+
catch (e) { throw ctx.E("csrattrs/bad-key-type-attr", "an rsaEncryption attribute value must be an INTEGER key size (RFC 9908 sec. 3.2)", e); }
|
|
262
|
+
// Bound before narrowing to a Number: an RSA modulus-size hint of 1..65536
|
|
263
|
+
// bits covers every real key; a value past 2^53 would round silently.
|
|
264
|
+
if (sz < 1n || sz > 65536n) throw ctx.E("csrattrs/bad-key-type-attr", "the rsaEncryption key size " + sz + " is out of range (1..65536 bits)");
|
|
265
|
+
item.keySize = Number(sz);
|
|
266
|
+
}
|
|
267
|
+
} else if (item.oid === OID_TEMPLATE) {
|
|
268
|
+
if (item.values.length !== 1) throw ctx.E("csrattrs/bad-template", "a certificationRequestInfoTemplate attribute values must be a SET of exactly one template (RFC 9908 sec. 3.4)");
|
|
269
|
+
item.template = schema.walk(CERT_REQ_INFO_TEMPLATE, asn1.decode(item.values[0]), ctx).result;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// AttrOrOID ::= CHOICE { oid OBJECT IDENTIFIER, attribute Attribute }. The two
|
|
274
|
+
// arms are the two universal tags; anything else is not an AttrOrOID. A decode
|
|
275
|
+
// leaf (not schema.choice) so the enrichment + the paired write live together.
|
|
276
|
+
var ATTR_OR_OID = schema.decode(function (n, ctx) {
|
|
277
|
+
if (schema.isUniversal(n, T.OBJECT_IDENTIFIER) && !n.constructed) {
|
|
278
|
+
var o = asn1.read.oid(n);
|
|
279
|
+
return { kind: "oid", oid: o, name: ctx.oid.name(o) || null };
|
|
280
|
+
}
|
|
281
|
+
if (schema.isUniversal(n, T.SEQUENCE) && n.children) {
|
|
282
|
+
// Peek the type OID to choose the values-arity rule: only a recognized non-key
|
|
283
|
+
// attribute (id-ExtensionReq / template) requires SET SIZE(1..MAX); a key-type
|
|
284
|
+
// hint OR an unknown OID MAY carry an empty SET (RFC 9908 sec. 3.2 / RFC 8951).
|
|
285
|
+
var typeOid = (n.children.length >= 1 && schema.isUniversal(n.children[0], T.OBJECT_IDENTIFIER) && !n.children[0].constructed) ? asn1.read.oid(n.children[0]) : null;
|
|
286
|
+
var attrSchema = STRICT_VALUES_OIDS.has(typeOid) ? ATTRIBUTE : ATTRIBUTE_OPTIONAL_VALUES;
|
|
287
|
+
var a = schema.walk(attrSchema, n, ctx).result; // { type, name, values:[bytes] }
|
|
288
|
+
var item = { kind: "attribute", oid: a.type, name: a.name, values: a.values };
|
|
289
|
+
_enrichAttribute(item, ctx);
|
|
290
|
+
return item;
|
|
291
|
+
}
|
|
292
|
+
throw ctx.E("csrattrs/bad-attr-or-oid", "each CsrAttrs element must be an OBJECT IDENTIFIER or an Attribute SEQUENCE (RFC 8951 sec. 3.5)");
|
|
293
|
+
}, function (item) {
|
|
294
|
+
// Paired encoder (one structure, both directions): reconstruct from the arm's
|
|
295
|
+
// identifying fields + raw values -- the typed enrichment is decode-only.
|
|
296
|
+
if (item.kind === "oid") return asn1.build.oid(item.oid);
|
|
297
|
+
return asn1.build.sequence([asn1.build.oid(item.oid), asn1.build.set(item.values)]);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// CsrAttrs ::= SEQUENCE SIZE (0..MAX) OF AttrOrOID. min:0 -- an empty SEQUENCE is
|
|
301
|
+
// a complete valid document. The cross-element MUSTs (RFC 9908 sec. 3.2 single
|
|
302
|
+
// id-ExtensionReq; sec. 3.4 single template) are enforced in the build.
|
|
303
|
+
var CSR_ATTRS = schema.seqOf(ATTR_OR_OID, {
|
|
304
|
+
assert: "sequence", code: "csrattrs/not-csrattrs", what: "CsrAttrs",
|
|
305
|
+
build: function (m) {
|
|
306
|
+
var items = m.items.map(function (it) { return it.value; });
|
|
307
|
+
var extReq = 0, tpl = 0;
|
|
308
|
+
for (var i = 0; i < items.length; i++) {
|
|
309
|
+
if (items[i].kind !== "attribute") continue;
|
|
310
|
+
if (items[i].oid === OID_EXTENSION_REQUEST) extReq += 1;
|
|
311
|
+
if (items[i].oid === OID_TEMPLATE) tpl += 1;
|
|
312
|
+
}
|
|
313
|
+
if (extReq > 1) throw NS.E("csrattrs/duplicate-extension-req", "at most one id-ExtensionReq attribute is permitted in CsrAttrs (RFC 9908 sec. 3.2)");
|
|
314
|
+
if (tpl > 1) throw NS.E("csrattrs/duplicate-template", "at most one certificationRequestInfoTemplate attribute is permitted in CsrAttrs (RFC 9908 sec. 3.4)");
|
|
315
|
+
return { items: items };
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* @primitive pki.schema.csrattrs.parse
|
|
321
|
+
* @signature pki.schema.csrattrs.parse(der) -> { items }
|
|
322
|
+
* @since 0.1.24
|
|
323
|
+
* @status experimental
|
|
324
|
+
* @spec RFC 8951, RFC 9908, RFC 7030
|
|
325
|
+
* @related pki.schema.parse, pki.est.buildEnrollAttributes
|
|
326
|
+
*
|
|
327
|
+
* Parse a DER `Buffer` of EST CSR Attributes (`CsrAttrs`, RFC 8951 sec. 3.5) into
|
|
328
|
+
* `{ items }`. Each item is `{ kind, oid, name }` -- `kind` `"oid"` for a bare
|
|
329
|
+
* OID, `"attribute"` for an `Attribute`, which adds raw `values` plus, for the
|
|
330
|
+
* RFC 9908 meaningful types, `extensions` (id-ExtensionReq), `curve` / `keySize`
|
|
331
|
+
* (EC / RSA key-type conventions), or `template` (the request-info template). An
|
|
332
|
+
* empty `SEQUENCE` yields `{ items: [] }` ("no additional information desired").
|
|
333
|
+
* Unknown OIDs / attribute types are tolerated (surfaced raw); a malformed
|
|
334
|
+
* structure or an RFC 9908 semantic violation throws a typed `CsrattrsError`
|
|
335
|
+
* (`csrattrs/*`) and a leaf-level codec fault surfaces as `asn1/*`.
|
|
336
|
+
*
|
|
337
|
+
* @example
|
|
338
|
+
* var a = pki.schema.csrattrs.parse(der);
|
|
339
|
+
* a.items[0].kind; // -> "oid" | "attribute"
|
|
340
|
+
*/
|
|
341
|
+
var parse = pkix.makeParser({ pemLabel: null, PemError: frameworkError.PemError, ErrorClass: CsrattrsError, prefix: "csrattrs", what: "CsrAttrs", topSchema: CSR_ATTRS, ns: NS });
|
|
342
|
+
|
|
343
|
+
// matches(root): a CsrAttrs is a universal SEQUENCE whose children are each a
|
|
344
|
+
// bare universal OID or an Attribute (a universal SEQUENCE of exactly 2 whose
|
|
345
|
+
// first child is an OID and second a SET). An empty SEQUENCE (30 00) is a valid
|
|
346
|
+
// CsrAttrs and no other registered document root accepts it. Registered BEFORE
|
|
347
|
+
// ocsp-request (a permissive superset probe) as the strict refinement, the same
|
|
348
|
+
// resolution tsp / crmf already use.
|
|
349
|
+
function matches(root) {
|
|
350
|
+
var k = pkix.rootSequenceChildren(root, 0);
|
|
351
|
+
if (!k) return false;
|
|
352
|
+
if (k.length === 0) return true;
|
|
353
|
+
for (var i = 0; i < k.length; i++) {
|
|
354
|
+
var c = k[i];
|
|
355
|
+
// bare-OID arm: a primitive universal OBJECT IDENTIFIER.
|
|
356
|
+
if (schema.isUniversal(c, T.OBJECT_IDENTIFIER) && !c.constructed) continue;
|
|
357
|
+
// Attribute arm: a universal SEQUENCE of exactly { OID, SET }.
|
|
358
|
+
if (!schema.isUniversal(c, T.SEQUENCE) || !c.children || c.children.length !== 2) return false;
|
|
359
|
+
if (!schema.isUniversal(c.children[0], T.OBJECT_IDENTIFIER)) return false;
|
|
360
|
+
if (!(schema.isUniversal(c.children[1], T.SET) && c.children[1].children)) return false;
|
|
361
|
+
}
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
module.exports = {
|
|
366
|
+
parse: parse,
|
|
367
|
+
matches: matches,
|
|
368
|
+
// The top schema, exposed (not on the curated pki.schema.csrattrs surface) so a
|
|
369
|
+
// composer can drive the encode direction and prove the round-trip.
|
|
370
|
+
csrAttrsSchema: CSR_ATTRS,
|
|
371
|
+
};
|