@blamejs/pki 0.1.23 → 0.1.25

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/oid.js CHANGED
@@ -74,7 +74,8 @@ var FAMILIES = {
74
74
  // RFC 5755 attribute-certificate id-pe extensions: ac-auditIdentity sec. 4.3.1,
75
75
  // aaControls sec. 7.4, ac-proxying sec. 7.2).
76
76
  pkixAccess: { base: [1, 3, 6, 1, 5, 5, 7, 1], of: {
77
- authorityInfoAccess: 1, acAuditIdentity: 4, aaControls: 6, acProxying: 10 } },
77
+ authorityInfoAccess: 1, acAuditIdentity: 4, aaControls: 6, acProxying: 10,
78
+ acmeIdentifier: 31 } },
78
79
 
79
80
  // id-aca -- RFC 5755 attribute-certificate attribute types on the id-pkix 10 arc:
80
81
  // authenticationInfo sec. 4.4.1 .. group sec. 4.4.4, plus encAttrs sec. 7.1 (the encrypted-
@@ -211,10 +212,15 @@ var FAMILIES = {
211
212
  // signingCertificateV2 (ESSCertIDv2) carries a non-SHA-1 cert hash (RFC 5816 sec. 2.2.1).
212
213
  // The RFC 2634 originals (receiptRequest .. contentReference) are name-only: they
213
214
  // resolve to a name so an unsupported-attribute diagnostic is legible, not decoded.
215
+ // decryptKeyID / asymmDecryptKeyID name the out-of-band key-encryption key a
216
+ // /serverkeygen client references (RFC 4108 sec. 2.2.5 / RFC 7030 sec. 4.4.1);
217
+ // certificationRequestInfoTemplate / extensionReqTemplate are the RFC 9908
218
+ // CSR-attributes template carriers.
214
219
  smimeAa: { base: [1, 2, 840, 113549, 1, 9, 16, 2], of: {
215
220
  receiptRequest: 1, eSSSecurityLabel: 2, mlExpansionHistory: 3, contentHints: 4,
216
221
  msgSigDigest: 5, contentIdentifier: 7, equivalentLabels: 9, contentReference: 10,
217
- signingCertificate: 12, timeStampToken: 14, signingCertificateV2: 47 } },
222
+ signingCertificate: 12, timeStampToken: 14, decryptKeyID: 37, signingCertificateV2: 47,
223
+ asymmDecryptKeyID: 54, certificationRequestInfoTemplate: 61, extensionReqTemplate: 62 } },
218
224
 
219
225
  // ANSI X9.62 EC public key, named curve, and ECDSA signatures.
220
226
  ansiX962: { base: [1, 2, 840, 10045], of: {
package/lib/schema-all.js CHANGED
@@ -42,6 +42,7 @@ var ocsp = require("./schema-ocsp");
42
42
  var tsp = require("./schema-tsp");
43
43
  var crmf = require("./schema-crmf");
44
44
  var cmp = require("./schema-cmp");
45
+ var csrattrs = require("./schema-csrattrs");
45
46
  var attrcert = require("./schema-attrcert");
46
47
  // smime is a COMPANION decoder for CMS signed-attribute values, NOT a top-level
47
48
  // format -- it has no self-describing DER root (an ESS attribute value is a bare
@@ -122,6 +123,20 @@ var FORMATS = [
122
123
  detect: cmp.matches,
123
124
  parse: function (input) { return cmp.parse(input); },
124
125
  },
126
+ {
127
+ // CsrAttrs ::= SEQUENCE SIZE (0..MAX) OF AttrOrOID (RFC 8951 sec. 3.5) -- a
128
+ // SEQUENCE whose every child is a bare universal OID or an Attribute (a
129
+ // universal SEQUENCE of exactly 2: OID + SET). A 1-2-element all-Attribute
130
+ // CsrAttrs satisfies the permissive ocsp-request probe below (SEQUENCE +
131
+ // constructed first child), and the empty 30 00 is a CsrAttrs no other root
132
+ // accepts -- so csrattrs is the strict refinement and MUST sit ahead of
133
+ // ocsp-request, the same resolution tsp / cmp use. Disjoint from cmp (whose
134
+ // body child is context-[0..26], never a universal OID/Attribute).
135
+ name: "csrattrs",
136
+ module: csrattrs,
137
+ detect: csrattrs.matches,
138
+ parse: function (input) { return csrattrs.parse(input); },
139
+ },
125
140
  {
126
141
  // OCSPRequest ::= SEQUENCE { tbsRequest SEQUENCE, optionalSignature [0] EXPLICIT
127
142
  // OPTIONAL } -- a SEQUENCE of 1-2 whose first child is the tbsRequest SEQUENCE.
@@ -289,6 +304,7 @@ module.exports = {
289
304
  tsp: { parse: tsp.parse, parseTstInfo: tsp.parseTstInfo, parseToken: tsp.parseToken, pemDecode: tsp.pemDecode, pemEncode: tsp.pemEncode },
290
305
  crmf: { parse: crmf.parse, pemDecode: crmf.pemDecode, pemEncode: crmf.pemEncode },
291
306
  cmp: { parse: cmp.parse, pemDecode: cmp.pemDecode, pemEncode: cmp.pemEncode },
307
+ csrattrs: { parse: csrattrs.parse },
292
308
  attrcert: { parse: attrcert.parse, pemDecode: attrcert.pemDecode, pemEncode: attrcert.pemEncode },
293
309
  smime: { parseSigningCertificate: smime.parseSigningCertificate, parseSigningCertificateV2: smime.parseSigningCertificateV2, parseSmimeCapabilities: smime.parseSmimeCapabilities, decodeAttribute: smime.decodeAttribute },
294
310
  all: all,
package/lib/schema-csr.js CHANGED
@@ -75,7 +75,11 @@ var DIRECTORY_STRING_TAGS = [
75
75
  ];
76
76
  var RECOGNIZED_ATTRIBUTE_VALUE = {};
77
77
  RECOGNIZED_ATTRIBUTE_VALUE[oid.byName("extensionRequest")] = function (node, ctx) {
78
- try { schema.walk(EXTENSIONS, 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 }; }
79
83
  catch (e) {
80
84
  throw ctx.E("csr/bad-attribute-value",
81
85
  "extensionRequest value must be a well-formed Extensions SEQUENCE (RFC 2985 sec. 5.4.2): " + ((e && e.message) || String(e)), e);
@@ -115,7 +119,8 @@ var CERTIFICATION_REQUEST_INFO = schema.seq([
115
119
  if (valueItems.length !== 1) {
116
120
  throw ctx.E("csr/bad-attribute-value", (a.name || a.type) + " is a SINGLE VALUE attribute (RFC 2985 sec. 5.4)");
117
121
  }
118
- checkValue(valueItems[0].node, ctx);
122
+ var enriched = checkValue(valueItems[0].node, ctx);
123
+ if (enriched) { Object.keys(enriched).forEach(function (k) { a[k] = enriched[k]; }); }
119
124
  }
120
125
  return a;
121
126
  });
@@ -165,7 +170,10 @@ var CERTIFICATION_REQUEST = pkix.signedEnvelope(NS, CERTIFICATION_REQUEST_INFO,
165
170
  * Every field is validated on the way in; a malformed CertificationRequest /
166
171
  * CertificationRequestInfo throws a typed `CsrError` (`csr/*`) and a leaf-level
167
172
  * codec fault surfaces as `asn1/*`. Attribute values are returned as raw DER
168
- * 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).
169
177
  *
170
178
  * @example
171
179
  * var csr = pki.schema.csr.parse(der);
@@ -231,4 +239,10 @@ module.exports = {
231
239
  pemDecode: pemDecode,
232
240
  pemEncode: pemEncode,
233
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,
234
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
+ };
@@ -56,6 +56,38 @@ function pemDecode(text, label, PemError) {
56
56
  return der;
57
57
  }
58
58
 
59
+ // pemDecodeAll(text, label, PemError): decode EVERY PEM block under the STRICT
60
+ // RFC 7468 profile for application/pem-certificate-chain (RFC 8555 sec. 9.1) --
61
+ // each block must carry `label` (default CERTIFICATE), explanatory text MUST NOT
62
+ // appear before/between/after blocks (errata 5983: certchain = stricttextualmsg
63
+ // *(eol stricttextualmsg)), and at least one block is required. Every block's
64
+ // base64 body is canonical (the pemDecode rule); returns an array of DER buffers.
65
+ function pemDecodeAll(text, label, PemError) {
66
+ label = label || "CERTIFICATE";
67
+ if (Buffer.isBuffer(text)) {
68
+ if (text.length > constants.LIMITS.PEM_MAX_BYTES) throw new PemError("pem/too-large", "PEM input exceeds size cap");
69
+ text = text.toString("latin1");
70
+ }
71
+ if (typeof text !== "string") throw new PemError("pem/bad-input", "pemDecodeAll expects a string or Buffer");
72
+ if (text.length > constants.LIMITS.PEM_MAX_BYTES) throw new PemError("pem/too-large", "PEM input exceeds size cap");
73
+ var re = /-----BEGIN ([A-Z0-9 ]+)-----([\s\S]*?)-----END \1-----/g;
74
+ var blocks = [];
75
+ var lastEnd = 0, m;
76
+ while ((m = re.exec(text)) !== null) {
77
+ if (/\S/.test(text.slice(lastEnd, m.index))) throw new PemError("pem/explanatory-text", "explanatory text is not permitted around PEM blocks (RFC 8555 sec. 9.1)");
78
+ if (m[1] !== label) throw new PemError("pem/label-mismatch", "expected " + JSON.stringify(label) + " block, got " + JSON.stringify(m[1]));
79
+ var b64 = m[2].replace(/[\r\n\t ]+/g, "");
80
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(b64) || b64.length % 4 !== 0) throw new PemError("pem/bad-base64", "PEM body is not canonical base64 (RFC 4648 sec. 3.5)");
81
+ var der = Buffer.from(b64, "base64");
82
+ if (der.toString("base64") !== b64) throw new PemError("pem/bad-base64", "PEM base64 body is not canonical (RFC 4648 sec. 3.5)");
83
+ blocks.push(der);
84
+ lastEnd = re.lastIndex;
85
+ }
86
+ if (blocks.length === 0) throw new PemError("pem/no-block", "no PEM block found");
87
+ if (/\S/.test(text.slice(lastEnd))) throw new PemError("pem/explanatory-text", "explanatory text is not permitted after the PEM chain (RFC 8555 sec. 9.1)");
88
+ return blocks;
89
+ }
90
+
59
91
  // The label must be re-readable by this module's own PEM_RE (uppercase
60
92
  // alphanumeric words separated by single spaces, a subset of the RFC 7468
61
93
  // sec. 3 label grammar). Anything else (lowercase, a leading/trailing space,
@@ -340,7 +372,10 @@ function name(ns, opts) {
340
372
  rdns.push(atvs);
341
373
  parts.push(atvParts.join("+"));
342
374
  });
343
- return { rdns: rdns, dn: parts.join(", ") };
375
+ // `bytes` is the raw Name DER (the universal SEQUENCE TLV, recovered from the
376
+ // wire node even in IMPLICIT [tag] mode) -- a byte-exact comparison a consumer
377
+ // needs when a rendered DN is not enough (the EST re-enroll subject MUST).
378
+ return { rdns: rdns, dn: parts.join(", "), bytes: opts.implicitTag != null ? asn1.sequenceTlv(m.node) : m.node.bytes };
344
379
  }
345
380
  if (opts.implicitTag != null) {
346
381
  return schema.implicitSeqOf(opts.implicitTag, relativeDistinguishedName(ns), {
@@ -913,13 +948,17 @@ function spki(ns, opts) {
913
948
 
914
949
  // Attribute ::= SEQUENCE { type OBJECT IDENTIFIER, values SET OF AttributeValue }.
915
950
  // AttributeValue is ANY, kept as raw DER (node.bytes) -- an unrecognized attribute
916
- // type never fails the parse. values is SET SIZE (1..MAX): an empty SET is
917
- // rejected; there is NO SET-OF uniqueness. Shared by the CSR (requested
951
+ // type never fails the parse. values is SET SIZE (1..MAX) by default: an empty SET
952
+ // is rejected; there is NO SET-OF uniqueness. Shared by the CSR (requested
918
953
  // attributes, RFC 2986 sec. 4.1) and PKCS#8 (private-key attributes, RFC 5958 sec. 2).
919
- function attribute(ns) {
954
+ // opts.minValues (default 1) lowers the floor to 0 for the EST CsrAttrs key-type
955
+ // hints, whose empty values SET means "any key of this type" (RFC 9908 sec. 3.2).
956
+ function attribute(ns, opts) {
957
+ opts = opts || {};
958
+ var minValues = opts.minValues === undefined ? 1 : opts.minValues;
920
959
  return schema.seq([
921
960
  schema.field("type", schema.oidLeaf()),
922
- schema.field("values", schema.setOf(schema.any(), { assert: "set", min: 1, max: constants.LIMITS.ATTRIBUTE_MAX_VALUES,
961
+ schema.field("values", schema.setOf(schema.any(), { assert: "set", min: minValues, max: constants.LIMITS.ATTRIBUTE_MAX_VALUES,
923
962
  code: ns.prefix + "/bad-attribute-values", what: "attribute values" })),
924
963
  ], {
925
964
  assert: "sequence", arity: { exact: 2 }, code: ns.prefix + "/bad-attribute", what: "Attribute",
@@ -1015,6 +1054,7 @@ function signedEnvelope(ns, tbsSchema, opts) {
1015
1054
 
1016
1055
  module.exports = {
1017
1056
  pemDecode: pemDecode,
1057
+ pemDecodeAll: pemDecodeAll,
1018
1058
  pemEncode: pemEncode,
1019
1059
  coerceToDer: coerceToDer,
1020
1060
  decodeRoot: decodeRoot,
package/lib/webcrypto.js CHANGED
@@ -339,10 +339,11 @@ SubtleCrypto.prototype.verify = async function verify(algorithm, key, signature,
339
339
  hm.update(buf);
340
340
  // Verify must RESOLVE false for any invalid signature, including one of
341
341
  // the wrong length. timingSafeEqual throws RangeError on a length
342
- // mismatch, so gate on length first; the length is already public, so
343
- // this leaks nothing a constant-time compare would protect.
344
- var mac = hm.digest();
345
- return mac.length === sig.length && nodeCrypto.timingSafeEqual(mac, sig);
342
+ // mismatch, so gate on the (public) length first, then compare the bytes
343
+ // in constant time. The length check leaks nothing a constant-time compare
344
+ // would protect; the secret-dependent byte comparison is timingSafeEqual.
345
+ var digest = hm.digest();
346
+ return digest.length === sig.length && nodeCrypto.timingSafeEqual(digest, sig);
346
347
  }
347
348
  throw new WebCryptoError("webcrypto/not-supported", "verify: unsupported algorithm " + JSON.stringify(name));
348
349
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:a62885e0-295d-4f1c-aa93-536269dabd5b",
5
+ "serialNumber": "urn:uuid:fa2f7437-a88e-47ad-959f-8d019edc06e0",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-10T06:16:22.938Z",
8
+ "timestamp": "2026-07-10T18:29:08.522Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/pki@0.1.23",
22
+ "bom-ref": "@blamejs/pki@0.1.25",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.1.23",
25
+ "version": "0.1.25",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
29
- "purl": "pkg:npm/%40blamejs/pki@0.1.23",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.1.25",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/pki@0.1.23",
57
+ "ref": "@blamejs/pki@0.1.25",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]