@blamejs/pki 0.1.21 → 0.1.23

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.
@@ -0,0 +1,363 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ /**
5
+ * @module pki.schema.smime
6
+ * @nav Schema
7
+ * @title S/MIME (ESS)
8
+ * @order 180
9
+ * @slug smime
10
+ *
11
+ * @intro
12
+ * S/MIME Enhanced Security Services signed-attribute values per RFC 5035 (ESS)
13
+ * and RFC 8551 (S/MIME 4.0). These are the DER-decoded VALUES of CMS signed
14
+ * attributes -- they ride inside a `SignerInfo.signedAttrs`, so this is a
15
+ * companion decoder a CMS consumer invokes by attribute OID, NOT a top-level
16
+ * format the schema orchestrator auto-routes.
17
+ *
18
+ * `parseSigningCertificate` / `parseSigningCertificateV2` decode the ESS
19
+ * signing-certificate attributes that bind a signature to the exact certificate
20
+ * that made it: each surfaces its list of `ESSCertID`(v2) -- the certificate hash
21
+ * (raw), the hash algorithm (decoded for v2, or the implied SHA-1 for v1), and
22
+ * the optional `issuerSerial` (issuer `GeneralNames` validated + surfaced raw,
23
+ * serial as a BigInt + hex) -- plus the optional certificate policies.
24
+ * `parseSmimeCapabilities` decodes the ordered `SMIMECapabilities` list (each a
25
+ * capability OID + raw parameters). `decodeAttribute` takes a CMS-shaped
26
+ * `{ type, values }` attribute, enforces the single-`AttributeValue` rule
27
+ * (RFC 8551 sec. 2.5.2), routes on the attribute OID, and recognize-and-defers an
28
+ * unknown attribute type with its raw values intact.
29
+ *
30
+ * Structure is decoded; verification is the consumer's -- the parser surfaces
31
+ * `certHash` + `hashAlgorithm` + `issuerSerial` so a verifier recomputes the
32
+ * certificate hash (compose `webcrypto`) and matches the issuer/serial against
33
+ * the actual signing certificate; it never recomputes a hash or trusts a cert.
34
+ * Whether an attribute is correctly placed in `signedAttrs` (vs `unsignedAttrs`)
35
+ * is the CMS consumer's knowledge. DER-only, fail-closed.
36
+ *
37
+ * @card
38
+ * Decode RFC 5035 ESS SigningCertificate / SigningCertificateV2 and RFC 8551
39
+ * SMIMECapabilities signed-attribute values -- cert-hash binding, validated
40
+ * issuer GeneralNames, ordered capability list, OID-dispatched, fail-closed.
41
+ */
42
+
43
+ var asn1 = require("./asn1-der");
44
+ var schema = require("./schema-engine");
45
+ var pkix = require("./schema-pkix");
46
+ var oid = require("./oid");
47
+ var frameworkError = require("./framework-error");
48
+
49
+ var SmimeError = frameworkError.SmimeError;
50
+ var PemError = frameworkError.PemError;
51
+
52
+ var NS = pkix.makeNS("smime", SmimeError, oid);
53
+ var TAGS = asn1.TAGS;
54
+
55
+ var ALGID = pkix.algorithmIdentifier(NS);
56
+
57
+ // Registry constants -- never a dotted literal in a format module.
58
+ var OID_SHA1 = oid.byName("sha1");
59
+ var OID_SHA256 = oid.byName("sha256");
60
+ var OID_SIGNING_CERTIFICATE = oid.byName("signingCertificate");
61
+ var OID_SIGNING_CERTIFICATE_V2 = oid.byName("signingCertificateV2");
62
+ var OID_SMIME_CAPABILITIES = oid.byName("smimeCapabilities");
63
+
64
+ // IssuerSerial ::= SEQUENCE { issuer GeneralNames, serialNumber CertificateSerialNumber }
65
+ // (RFC 5035 App A). A bare universal SEQUENCE of exactly two fields -- distinct
66
+ // from the RFC 5755 attribute-certificate IssuerSerial (three fields, reached
67
+ // IMPLICIT-tagged), so declared per-format. `issuer` composes the shared
68
+ // GeneralNames factory (every CHOICE arm validated + surfaced raw, SIZE 1..MAX)
69
+ // and is surfaced unchanged here: the RFC 5035 sec. 5 directoryName narrowing applies
70
+ // only to a non-attribute (public-key) certificate reference, and an ESSCertID
71
+ // entry may reference an attribute certificate whose issuer is a full GeneralNames.
72
+ // The FIRST cert is known to be the signing public-key cert (RFC 5035 sec. 3), so the
73
+ // directoryName rule is enforced there -- in the SigningCertificate build -- not here.
74
+ var GENERAL_NAME_DIRECTORY = 4;
75
+ var ISSUER_SERIAL = schema.seq([
76
+ schema.field("issuer", pkix.generalNames(NS, { code: "smime/bad-general-names" })),
77
+ schema.field("serialNumber", schema.integerLeaf()),
78
+ ], {
79
+ assert: "sequence", arity: { exact: 2 }, code: "smime/bad-issuer-serial", what: "IssuerSerial",
80
+ build: function (m) {
81
+ return {
82
+ issuer: m.fields.issuer.value.result,
83
+ serialNumber: m.fields.serialNumber.value,
84
+ serialNumberHex: m.fields.serialNumber.node.content.toString("hex"),
85
+ };
86
+ },
87
+ });
88
+
89
+ // RFC 5035 sec. 5: the signing certificate (certs[0], the public-key cert that made
90
+ // the signature per sec. 3) MUST identify its issuer as exactly one directoryName [4].
91
+ // Additional certs[1..] may reference attribute certificates, whose issuer is a
92
+ // GeneralNames -- left unconstrained (a verifier-tier profile concern). `fail`
93
+ // throws the caller's code.
94
+ function assertSignerIssuerIsDirectoryName(certs, fail) {
95
+ if (!certs.length || !certs[0].issuerSerial) return;
96
+ var names = certs[0].issuerSerial.issuer.names;
97
+ if (names.length !== 1 || names[0].tagClass !== "context" || names[0].tagNumber !== GENERAL_NAME_DIRECTORY) {
98
+ fail("the signing certificate's IssuerSerial issuer MUST be exactly one directoryName [4] GeneralName (RFC 5035 sec. 5)");
99
+ }
100
+ }
101
+
102
+ // PolicyInformation ::= SEQUENCE { policyIdentifier CertPolicyId,
103
+ // policyQualifiers SEQUENCE OF PolicyQualifierInfo OPTIONAL } (RFC 5280 sec. 4.2.1.4).
104
+ // Decode the policy OID (+ registry name); surface policyQualifiers raw.
105
+ var POLICY_INFORMATION = schema.seq([
106
+ schema.field("policyIdentifier", schema.oidLeaf()),
107
+ schema.optional("policyQualifiers", schema.any(), { whenUniversal: [TAGS.SEQUENCE] }),
108
+ ], {
109
+ assert: "sequence", code: "smime/bad-policy-information", what: "PolicyInformation",
110
+ build: function (m, ctx) {
111
+ var qualifiers = null;
112
+ if (m.fields.policyQualifiers.present) {
113
+ // The qualifier body stays raw (per the ESS scope), but the RFC 5280
114
+ // sec. 4.2.1.4 structure is validated fail-closed by the shared assertion -- the
115
+ // same PolicyInformation shape the certificatePolicies decoder enforces.
116
+ var q = m.fields.policyQualifiers.node;
117
+ pkix.assertPolicyQualifiers(q, function (msg, cause) { throw ctx.E("smime/bad-policy-information", msg, cause); });
118
+ qualifiers = q.bytes;
119
+ }
120
+ return {
121
+ policyIdentifier: m.fields.policyIdentifier.value,
122
+ name: ctx.oid.name(m.fields.policyIdentifier.value) || null,
123
+ policyQualifiers: qualifiers,
124
+ };
125
+ },
126
+ });
127
+ // policies SEQUENCE OF PolicyInformation OPTIONAL -- order-preserving; the ASN.1
128
+ // module carries no SIZE bound, so an explicitly present but empty list is legal.
129
+ var POLICIES = schema.seqOf(POLICY_INFORMATION, {
130
+ assert: "sequence", min: 0, code: "smime/bad-policies", what: "policies",
131
+ build: function (m) { return m.items.map(function (it) { return it.value.result; }); },
132
+ });
133
+
134
+ // ESSCertID ::= SEQUENCE { certHash Hash, issuerSerial IssuerSerial OPTIONAL }
135
+ // (RFC 5035 sec. 5.4.2). Hash ::= OCTET STRING -- for v1 it is the SHA-1 hash of the
136
+ // whole certificate with NO algorithm field, so the hash algorithm is SYNTHESIZED
137
+ // as the implied SHA-1 to make v1 shape-compatible with v2 for a verifier.
138
+ var ESS_CERT_ID = schema.seq([
139
+ schema.field("certHash", schema.octetString()),
140
+ schema.optional("issuerSerial", ISSUER_SERIAL, { whenUniversal: [TAGS.SEQUENCE] }),
141
+ ], {
142
+ assert: "sequence", code: "smime/bad-ess-cert-id", what: "ESSCertID",
143
+ build: function (m) {
144
+ return {
145
+ certHash: m.fields.certHash.value,
146
+ hashAlgorithm: { oid: OID_SHA1, name: "sha1", parameters: null, implied: true },
147
+ issuerSerial: m.fields.issuerSerial.present ? m.fields.issuerSerial.value.result : null,
148
+ };
149
+ },
150
+ });
151
+
152
+ // ESSCertIDv2 ::= SEQUENCE { hashAlgorithm AlgorithmIdentifier DEFAULT
153
+ // {algorithm id-sha256}, certHash Hash, issuerSerial IssuerSerial OPTIONAL }
154
+ // (RFC 5035 sec. 5.4.1 / sec. 4). The mandatory certHash OCTET STRING pivots between the
155
+ // two optional SEQUENCEs, so a leading SEQUENCE is hashAlgorithm and a leading
156
+ // OCTET STRING means hashAlgorithm defaulted.
157
+ var ESS_CERT_ID_V2 = schema.seq([
158
+ schema.optional("hashAlgorithm", ALGID, { whenUniversal: [TAGS.SEQUENCE] }),
159
+ schema.field("certHash", schema.octetString()),
160
+ schema.optional("issuerSerial", ISSUER_SERIAL, { whenUniversal: [TAGS.SEQUENCE] }),
161
+ ], {
162
+ assert: "sequence", code: "smime/bad-ess-cert-id-v2", what: "ESSCertIDv2",
163
+ build: function (m, ctx) {
164
+ var hashAlgorithm;
165
+ if (m.fields.hashAlgorithm.present) {
166
+ var alg = m.fields.hashAlgorithm.value.result;
167
+ // X.690 sec. 11.5: a DEFAULT value MUST be omitted in DER. The default is
168
+ // {algorithm id-sha256, parameters ABSENT}; an explicit encoding byte-equal
169
+ // to it is a non-canonical DEFAULT and is rejected fail-closed (the
170
+ // structured-value analogue of the primitive-DEFAULT rejection the toolkit
171
+ // already enforces). A present hashAlgorithm carrying a redundant NULL
172
+ // parameters is NOT byte-equal to the params-absent default and decodes.
173
+ if (alg.oid === OID_SHA256 && alg.parameters === null) {
174
+ throw ctx.E("smime/non-canonical-default",
175
+ "ESSCertIDv2 hashAlgorithm equal to the DEFAULT {algorithm id-sha256} MUST be omitted (X.690 sec. 11.5)");
176
+ }
177
+ hashAlgorithm = { oid: alg.oid, name: alg.name, parameters: alg.parameters, defaulted: false };
178
+ } else {
179
+ hashAlgorithm = { oid: OID_SHA256, name: "sha256", parameters: null, defaulted: true };
180
+ }
181
+ return {
182
+ certHash: m.fields.certHash.value,
183
+ hashAlgorithm: hashAlgorithm,
184
+ issuerSerial: m.fields.issuerSerial.present ? m.fields.issuerSerial.value.result : null,
185
+ };
186
+ },
187
+ });
188
+
189
+ // SigningCertificate ::= SEQUENCE { certs SEQUENCE OF ESSCertID,
190
+ // policies SEQUENCE OF PolicyInformation OPTIONAL } (RFC 5035 sec. 5.4.2). certs is
191
+ // order-preserving -- RFC 5035 sec. 3 makes the FIRST element the signing certificate
192
+ // -- and non-empty (an empty certs cannot name a signing cert).
193
+ function signingCertificateSchema(essCertId, code, what) {
194
+ return schema.seq([
195
+ schema.field("certs", schema.seqOf(essCertId, { assert: "sequence", min: 1, code: "smime/bad-certs", what: "certs" })),
196
+ schema.optional("policies", POLICIES, { whenUniversal: [TAGS.SEQUENCE] }),
197
+ ], {
198
+ assert: "sequence", code: code, what: what,
199
+ build: function (m, ctx) {
200
+ var certs = m.fields.certs.value.items.map(function (it) { return it.value.result; });
201
+ assertSignerIssuerIsDirectoryName(certs, function (msg) { throw ctx.E("smime/bad-issuer-serial", msg); });
202
+ return {
203
+ certs: certs,
204
+ policies: m.fields.policies.present ? m.fields.policies.value.result : null,
205
+ };
206
+ },
207
+ });
208
+ }
209
+ var SIGNING_CERTIFICATE = signingCertificateSchema(ESS_CERT_ID, "smime/bad-signing-certificate", "SigningCertificate");
210
+ var SIGNING_CERTIFICATE_V2 = signingCertificateSchema(ESS_CERT_ID_V2, "smime/bad-signing-certificate-v2", "SigningCertificateV2");
211
+
212
+ // SMIMECapability ::= SEQUENCE { capabilityID OBJECT IDENTIFIER,
213
+ // parameters ANY DEFINED BY capabilityID OPTIONAL } (RFC 8551 sec. 2.5.2). Distinct
214
+ // domain from AlgorithmIdentifier though isomorphic -- surfaced as capabilityID +
215
+ // raw parameters (the ANY-DEFINED-BY interpretation is a negotiation concern).
216
+ var SMIME_CAPABILITY = schema.seq([
217
+ schema.field("capabilityID", schema.oidLeaf()),
218
+ schema.optional("parameters", schema.any(), { whenAny: true }),
219
+ ], {
220
+ assert: "sequence", code: "smime/bad-capability", what: "SMIMECapability",
221
+ build: function (m, ctx) {
222
+ return {
223
+ capabilityID: m.fields.capabilityID.value,
224
+ name: ctx.oid.name(m.fields.capabilityID.value) || null,
225
+ parameters: m.fields.parameters.present ? m.fields.parameters.node.bytes : null,
226
+ };
227
+ },
228
+ });
229
+ // SMIMECapabilities ::= SEQUENCE OF SMIMECapability -- ordered by preference
230
+ // (RFC 8551 sec. 2.5.2), never sorted; an empty list is legal.
231
+ var SMIME_CAPABILITIES = schema.seqOf(SMIME_CAPABILITY, {
232
+ assert: "sequence", min: 0, code: "smime/bad-capabilities", what: "SMIMECapabilities",
233
+ build: function (m) { return { capabilities: m.items.map(function (it) { return it.value.result; }) }; },
234
+ });
235
+
236
+ /**
237
+ * @primitive pki.schema.smime.parseSigningCertificate
238
+ * @signature pki.schema.smime.parseSigningCertificate(der) -> { certs, policies }
239
+ * @since 0.1.22
240
+ * @status experimental
241
+ * @spec RFC 5035, RFC 2634
242
+ * @related pki.schema.smime.parseSigningCertificateV2, pki.schema.smime.decodeAttribute
243
+ *
244
+ * Decode an ESS v1 `SigningCertificate` attribute value (RFC 5035 sec. 5.4.2) -- the
245
+ * raw `AttributeValue` a CMS consumer plucks off `SignerInfo.signedAttrs`. Returns
246
+ * `{ certs, policies }`: each `certs` entry is `{ certHash, hashAlgorithm,
247
+ * issuerSerial }` in wire order (the first is the signing certificate), where
248
+ * `hashAlgorithm` is the implied SHA-1 (v1 carries no algorithm field) and
249
+ * `issuerSerial` (or `null`) surfaces the issuer `GeneralNames` + serial. Throws a
250
+ * typed `smime/*` (or leaf `asn1/*`) error on malformed input.
251
+ *
252
+ * @example
253
+ * var b = pki.asn1.build;
254
+ * var essCertId = b.sequence([b.octetString(Buffer.alloc(20, 1))]); // SHA-1 hash, no issuerSerial
255
+ * var av = b.sequence([b.sequence([essCertId])]); // SigningCertificate { certs }
256
+ * var sc = pki.schema.smime.parseSigningCertificate(av);
257
+ * sc.certs[0].hashAlgorithm.name; // "sha1" (implied)
258
+ */
259
+ var parseSigningCertificate = pkix.makeParser({ pemLabel: null, PemError: PemError, ErrorClass: SmimeError, prefix: "smime", what: "SigningCertificate", topSchema: SIGNING_CERTIFICATE, ns: NS });
260
+
261
+ /**
262
+ * @primitive pki.schema.smime.parseSigningCertificateV2
263
+ * @signature pki.schema.smime.parseSigningCertificateV2(der) -> { certs, policies }
264
+ * @since 0.1.22
265
+ * @status experimental
266
+ * @spec RFC 5035, RFC 5816
267
+ * @related pki.schema.smime.parseSigningCertificate, pki.schema.smime.decodeAttribute
268
+ *
269
+ * Decode an ESS v2 `SigningCertificateV2` attribute value (RFC 5035 sec. 5.4.1).
270
+ * Identical shape to v1 but each `certs` entry carries a real `hashAlgorithm`:
271
+ * decoded when present, or the RFC 5035 sec. 4 default `id-sha256` (with
272
+ * `defaulted: true`) when omitted. An explicit `hashAlgorithm` byte-equal to that
273
+ * default is a non-canonical DER encoding and is rejected `smime/non-canonical-default`
274
+ * (X.690 sec. 11.5). Throws a typed `smime/*` error on malformed input.
275
+ *
276
+ * @example
277
+ * var b = pki.asn1.build;
278
+ * var essCertId = b.sequence([b.octetString(Buffer.alloc(32, 2))]); // hashAlgorithm defaulted
279
+ * var av = b.sequence([b.sequence([essCertId])]);
280
+ * var sc = pki.schema.smime.parseSigningCertificateV2(av);
281
+ * sc.certs[0].hashAlgorithm.defaulted; // true (SHA-256 default)
282
+ */
283
+ var parseSigningCertificateV2 = pkix.makeParser({ pemLabel: null, PemError: PemError, ErrorClass: SmimeError, prefix: "smime", what: "SigningCertificateV2", topSchema: SIGNING_CERTIFICATE_V2, ns: NS });
284
+
285
+ /**
286
+ * @primitive pki.schema.smime.parseSmimeCapabilities
287
+ * @signature pki.schema.smime.parseSmimeCapabilities(der) -> { capabilities }
288
+ * @since 0.1.22
289
+ * @status experimental
290
+ * @spec RFC 8551
291
+ * @related pki.schema.smime.decodeAttribute
292
+ *
293
+ * Decode an `SMIMECapabilities` attribute value (RFC 8551 sec. 2.5.2) into
294
+ * `{ capabilities }` -- an ORDERED list (preference order, never sorted), each
295
+ * `{ capabilityID, name, parameters }` with `parameters` the raw
296
+ * `ANY DEFINED BY capabilityID` bytes (or `null`). Throws a typed `smime/*` error
297
+ * on malformed input.
298
+ *
299
+ * @example
300
+ * var b = pki.asn1.build;
301
+ * var cap = b.sequence([b.oid(pki.oid.byName("aes256-CBC"))]);
302
+ * var caps = pki.schema.smime.parseSmimeCapabilities(b.sequence([cap]));
303
+ * caps.capabilities[0].name; // "aes256-CBC"
304
+ */
305
+ var parseSmimeCapabilities = pkix.makeParser({ pemLabel: null, PemError: PemError, ErrorClass: SmimeError, prefix: "smime", what: "SMIMECapabilities", topSchema: SMIME_CAPABILITIES, ns: NS });
306
+
307
+ /**
308
+ * @primitive pki.schema.smime.decodeAttribute
309
+ * @signature pki.schema.smime.decodeAttribute(attr) -> { kind, ... }
310
+ * @since 0.1.22
311
+ * @status experimental
312
+ * @spec RFC 8551, RFC 5035
313
+ * @related pki.schema.smime.parseSigningCertificate, pki.schema.cms.parse
314
+ *
315
+ * OID-dispatch convenience over the three value decoders for a CMS-shaped
316
+ * `{ type, values }` attribute (the shape `cms.parse` surfaces on
317
+ * `signerInfos[i].signedAttrs`). Enforces the single-`AttributeValue` rule
318
+ * (RFC 8551 sec. 2.5.2 / sec. 2.5) -- a `values` length other than 1 is rejected
319
+ * `smime/multi-valued-attribute` -- then routes on `attr.type`:
320
+ * `signingCertificate` / `signingCertificateV2` / `smimeCapabilities` decode to
321
+ * `{ kind, ...result }`; any other type is recognize-and-deferred
322
+ * `smime/unsupported-attribute` (its `type`, registry `name`, and raw `values`
323
+ * carried on the error so a caller keeps the bytes).
324
+ *
325
+ * @example
326
+ * var b = pki.asn1.build;
327
+ * var essCertId = b.sequence([b.octetString(Buffer.alloc(32, 2))]); // ESSCertIDv2, hashAlgorithm defaulted
328
+ * var av = b.sequence([b.sequence([essCertId])]); // SigningCertificateV2 { certs: [ ESSCertIDv2 ] }
329
+ * var got = pki.schema.smime.decodeAttribute({ type: pki.oid.byName("signingCertificateV2"), values: [av] });
330
+ * got.kind; // "signingCertificateV2"
331
+ */
332
+ function decodeAttribute(attr) {
333
+ if (!attr || typeof attr !== "object" || typeof attr.type !== "string" || !Array.isArray(attr.values)) {
334
+ throw new SmimeError("smime/bad-input", "decodeAttribute expects a CMS attribute { type, values }");
335
+ }
336
+ // Route on the attribute OID FIRST. The single-AttributeValue MUST (RFC 8551
337
+ // sec. 2.5.2) is specific to the ESS / SMIMECapabilities attributes, so it is
338
+ // enforced only inside the known-type branch; an unknown / custom attribute
339
+ // recognize-and-defers with its raw values intact regardless of value count
340
+ // (its own cardinality rules are not this decoder's to enforce).
341
+ if (attr.type === OID_SIGNING_CERTIFICATE || attr.type === OID_SIGNING_CERTIFICATE_V2 || attr.type === OID_SMIME_CAPABILITIES) {
342
+ if (attr.values.length !== 1) {
343
+ throw new SmimeError("smime/multi-valued-attribute",
344
+ "an ESS / SMIMECapabilities attribute MUST carry exactly one AttributeValue, got " + attr.values.length + " (RFC 8551 sec. 2.5.2)");
345
+ }
346
+ var value = attr.values[0];
347
+ if (attr.type === OID_SIGNING_CERTIFICATE) { var v1 = parseSigningCertificate(value); return { kind: "signingCertificate", certs: v1.certs, policies: v1.policies }; }
348
+ if (attr.type === OID_SIGNING_CERTIFICATE_V2) { var v2 = parseSigningCertificateV2(value); return { kind: "signingCertificateV2", certs: v2.certs, policies: v2.policies }; }
349
+ return { kind: "smimeCapabilities", capabilities: parseSmimeCapabilities(value).capabilities };
350
+ }
351
+ var e = new SmimeError("smime/unsupported-attribute", "unsupported S/MIME attribute type " + attr.type + (oid.name(attr.type) ? " (" + oid.name(attr.type) + ")" : ""));
352
+ e.type = attr.type;
353
+ e.name = oid.name(attr.type) || null;
354
+ e.values = attr.values;
355
+ throw e;
356
+ }
357
+
358
+ module.exports = {
359
+ parseSigningCertificate: parseSigningCertificate,
360
+ parseSigningCertificateV2: parseSigningCertificateV2,
361
+ parseSmimeCapabilities: parseSmimeCapabilities,
362
+ decodeAttribute: decodeAttribute,
363
+ };