@blamejs/pki 0.1.23 → 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.
@@ -219,6 +219,21 @@ var SmimeError = defineClass("SmimeError", { withCause: true });
219
219
  // inner `asn1/*` decode error) as `.cause`.
220
220
  var CtError = defineClass("CtError", { withCause: true });
221
221
 
222
+ // CsrattrsError -- a byte sequence that is not a well-formed RFC 8951 sec. 3.5
223
+ // CsrAttrs (SEQUENCE OF AttrOrOID), or one violating an RFC 9908 semantic rule
224
+ // (a repeated id-ExtensionReq, an extension-request values SET that is not
225
+ // exactly one Extensions, a template version other than v1(0), a mixed
226
+ // extension-request template). Carries the underlying leaf fault as `.cause`.
227
+ var CsrattrsError = defineClass("CsrattrsError", { withCause: true });
228
+
229
+ // EstError -- an RFC 7030 / 8951 / 9908 Enrollment-over-Secure-Transport
230
+ // protocol fault: a payload that fails a per-operation validator (a non-certs-
231
+ // only /cacerts response, a serverkeygen recipient-arm mismatch), a transfer /
232
+ // multipart framing violation, or an HTTP response the classifier rejects.
233
+ // Carries the underlying leaf / delegated fault (e.g. a cms/* or asn1/*) as
234
+ // `.cause`.
235
+ var EstError = defineClass("EstError", { withCause: true });
236
+
222
237
  module.exports = {
223
238
  PkiError: PkiError,
224
239
  defineClass: defineClass,
@@ -241,4 +256,6 @@ module.exports = {
241
256
  PathError: PathError,
242
257
  CtError: CtError,
243
258
  SmimeError: SmimeError,
259
+ CsrattrsError: CsrattrsError,
260
+ EstError: EstError,
244
261
  };
package/lib/oid.js CHANGED
@@ -211,10 +211,15 @@ var FAMILIES = {
211
211
  // signingCertificateV2 (ESSCertIDv2) carries a non-SHA-1 cert hash (RFC 5816 sec. 2.2.1).
212
212
  // The RFC 2634 originals (receiptRequest .. contentReference) are name-only: they
213
213
  // resolve to a name so an unsupported-attribute diagnostic is legible, not decoded.
214
+ // decryptKeyID / asymmDecryptKeyID name the out-of-band key-encryption key a
215
+ // /serverkeygen client references (RFC 4108 sec. 2.2.5 / RFC 7030 sec. 4.4.1);
216
+ // certificationRequestInfoTemplate / extensionReqTemplate are the RFC 9908
217
+ // CSR-attributes template carriers.
214
218
  smimeAa: { base: [1, 2, 840, 113549, 1, 9, 16, 2], of: {
215
219
  receiptRequest: 1, eSSSecurityLabel: 2, mlExpansionHistory: 3, contentHints: 4,
216
220
  msgSigDigest: 5, contentIdentifier: 7, equivalentLabels: 9, contentReference: 10,
217
- signingCertificate: 12, timeStampToken: 14, signingCertificateV2: 47 } },
221
+ signingCertificate: 12, timeStampToken: 14, decryptKeyID: 37, signingCertificateV2: 47,
222
+ asymmDecryptKeyID: 54, certificationRequestInfoTemplate: 61, extensionReqTemplate: 62 } },
218
223
 
219
224
  // ANSI X9.62 EC public key, named curve, and ECDSA signatures.
220
225
  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
+ };
@@ -340,7 +340,10 @@ function name(ns, opts) {
340
340
  rdns.push(atvs);
341
341
  parts.push(atvParts.join("+"));
342
342
  });
343
- return { rdns: rdns, dn: parts.join(", ") };
343
+ // `bytes` is the raw Name DER (the universal SEQUENCE TLV, recovered from the
344
+ // wire node even in IMPLICIT [tag] mode) -- a byte-exact comparison a consumer
345
+ // needs when a rendered DN is not enough (the EST re-enroll subject MUST).
346
+ return { rdns: rdns, dn: parts.join(", "), bytes: opts.implicitTag != null ? asn1.sequenceTlv(m.node) : m.node.bytes };
344
347
  }
345
348
  if (opts.implicitTag != null) {
346
349
  return schema.implicitSeqOf(opts.implicitTag, relativeDistinguishedName(ns), {
@@ -913,13 +916,17 @@ function spki(ns, opts) {
913
916
 
914
917
  // Attribute ::= SEQUENCE { type OBJECT IDENTIFIER, values SET OF AttributeValue }.
915
918
  // 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
919
+ // type never fails the parse. values is SET SIZE (1..MAX) by default: an empty SET
920
+ // is rejected; there is NO SET-OF uniqueness. Shared by the CSR (requested
918
921
  // attributes, RFC 2986 sec. 4.1) and PKCS#8 (private-key attributes, RFC 5958 sec. 2).
919
- function attribute(ns) {
922
+ // opts.minValues (default 1) lowers the floor to 0 for the EST CsrAttrs key-type
923
+ // hints, whose empty values SET means "any key of this type" (RFC 9908 sec. 3.2).
924
+ function attribute(ns, opts) {
925
+ opts = opts || {};
926
+ var minValues = opts.minValues === undefined ? 1 : opts.minValues;
920
927
  return schema.seq([
921
928
  schema.field("type", schema.oidLeaf()),
922
- schema.field("values", schema.setOf(schema.any(), { assert: "set", min: 1, max: constants.LIMITS.ATTRIBUTE_MAX_VALUES,
929
+ schema.field("values", schema.setOf(schema.any(), { assert: "set", min: minValues, max: constants.LIMITS.ATTRIBUTE_MAX_VALUES,
923
930
  code: ns.prefix + "/bad-attribute-values", what: "attribute values" })),
924
931
  ], {
925
932
  assert: "sequence", arity: { exact: 2 }, code: ns.prefix + "/bad-attribute", what: "Attribute",
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.24",
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:dfb00f98-2ca7-4fd1-b0e7-90fccf92604a",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-10T06:16:22.938Z",
8
+ "timestamp": "2026-07-10T12:57:51.582Z",
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.24",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.1.23",
25
+ "version": "0.1.24",
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.24",
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.24",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]