@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/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 the parser deliberately omits those three
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 subject DN,
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 §4.1). A BARE
49
- // mandatory INTEGER whose ONLY legal value is 0 the inverse of the cert/CRL
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 } the shared pkix factory
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 no non-empty-DN guard.
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 field.value is the match; .result is the {rdns, dn} build
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: m.fields.attributes.value.items.map(function (it) { return it.value.result; }),
131
+ attributes: attributes,
78
132
  };
79
133
  },
80
134
  });
81
135
 
82
136
  // CertificationRequest ::= SEQUENCE { certificationRequestInfo, signatureAlgorithm,
83
- // signature BIT STRING } the shared SIGNED envelope. DIVERGENCE from the
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 this build
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; // "CN=req.example"
124
- * csr.attributes[0].type; // "1.2.840.113549.1.9.14"
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 a SEQUENCE
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
+ };