@blamejs/pki 0.1.21 → 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +1 -0
- package/lib/framework-error.js +9 -0
- package/lib/oid.js +6 -1
- package/lib/schema-all.js +6 -0
- package/lib/schema-pkix.js +26 -14
- package/lib/schema-smime.js +363 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ All notable changes to `@blamejs/pki` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## v0.1.22 — 2026-07-09
|
|
8
|
+
|
|
9
|
+
An RFC 5035 / RFC 8551 S/MIME ESS signed-attribute decoder joins the schema family.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.schema.smime.parseSigningCertificate(der) / parseSigningCertificateV2(der) — decode the RFC 5035 ESS SigningCertificate (v1) and SigningCertificateV2 (v2) signed-attribute values into { certs, policies }. Each certs entry is { certHash, hashAlgorithm, issuerSerial } in wire order: certHash raw; hashAlgorithm the implied SHA-1 for v1, or the decoded algorithm (or the id-sha256 default, flagged defaulted) for v2; issuerSerial (or null) the issuer GeneralNames (each CHOICE arm validated, surfaced raw) plus serialNumber as a BigInt and serialNumberHex. certs is non-empty and order-preserving; a v2 hashAlgorithm encoded byte-equal to its DEFAULT is rejected smime/non-canonical-default (X.690 §11.5). Malformed input fails closed with a typed smime/* (or leaf asn1/*) code.
|
|
14
|
+
- pki.schema.smime.parseSmimeCapabilities(der) — decode an RFC 8551 SMIMECapabilities value into { capabilities }, an ordered list (preference order preserved, never sorted) of { capabilityID, name, parameters } with parameters the raw ANY-DEFINED-BY bytes.
|
|
15
|
+
- pki.schema.smime.decodeAttribute(attr) — an OID-dispatch convenience over the three decoders for a CMS-shaped { type, values } attribute (the shape cms.parse surfaces on signerInfos[i].signedAttrs). It enforces the single-AttributeValue rule (a values length other than one is rejected smime/multi-valued-attribute), routes on the attribute OID, and recognize-and-defers an unknown type as smime/unsupported-attribute carrying the type, registry name, and raw values.
|
|
16
|
+
- The error taxonomy gains SmimeError, carrying a stable smime/* code.
|
|
17
|
+
- The OID registry gains the smimeCapabilities attribute identifier and the RFC 2634 ESS attribute names (receiptRequest through contentReference), so a CMS signed attribute of one of those types resolves to a name in an unsupported-attribute diagnostic.
|
|
18
|
+
|
|
7
19
|
## v0.1.21 — 2026-07-09
|
|
8
20
|
|
|
9
21
|
SLH-DSA signatures verify in certification-path validation, and the post-quantum / EdDSA parameters-absent rule is enforced across every format.
|
package/README.md
CHANGED
|
@@ -202,6 +202,7 @@ is callable today; nothing below is a stub.
|
|
|
202
202
|
| `pki.schema.crmf` | Parse DER / PEM RFC 4211 certificate request messages (CertReqMessages — the CMP / EST enrollment body) — the requested-certificate template (subject, public key, validity, extensions), proof-of-possession, and registration controls, with the raw `CertRequest` region a POP verifier hashes; names dual-accepted (IMPLICIT and EXPLICIT), fail-closed — `parse`, `pemDecode` |
|
|
203
203
|
| `pki.schema.pkcs12` | Parse DER / BER / PEM RFC 7292 PKCS#12 (PFX) stores — key bags via the PKCS#8 parser, shrouded keys (algorithm surfaced, ciphertext opaque), cert / CRL / secret bags raw and byte-exact, encrypted and enveloped safes structurally via CMS, `friendlyName` / `localKeyId` decoded, and the exact MAC byte range (`macedBytes`) plus RFC 9579 PBMAC1 recognition for external verification; BER accepted exactly where §4.1 requires it, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
204
204
|
| `pki.schema.cmp` | Parse DER / PEM RFC 9810 Certificate Management Protocol messages (PKIMessage) — the header (version, sender / recipient incl. the anonymous NULL-DN, nonces, transaction id, general info), the 27-arm body (certificate requests via the CRMF parser, an encrypted certificate's EnvelopedData via CMS, response / revocation / confirmation / error / support / polling arms structural, the rest raw), and the exact `headerBytes` / `bodyBytes` slices an external verifier reconstructs the protected part from; the CMP-before-OCSP dispatch order is enforced, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
205
|
+
| `pki.schema.smime` | Decode S/MIME ESS signed-attribute values (RFC 5035 / RFC 8551) — `parseSigningCertificate` / `parseSigningCertificateV2` bind a signature to its signing certificate (cert hash, hash algorithm, issuer `GeneralNames` + serial), `parseSmimeCapabilities` decodes the ordered capability list, and `decodeAttribute` OID-dispatches a CMS attribute (enforcing the single-value rule, recognize-and-defer for unknown types). A companion decoder for CMS signed attributes, not an auto-routed format, fail-closed — `parseSigningCertificate`, `parseSigningCertificateV2`, `parseSmimeCapabilities`, `decodeAttribute` |
|
|
205
206
|
| `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk` / `encode` / `embeddedDer` plus the schema combinators |
|
|
206
207
|
| `pki.path` | RFC 5280 §6 certification-path validation — `validate` runs the §6.1 state machine (signature chaining, validity windows, name chaining, basic constraints and path length, key usage, name constraints, the certificate-policy tree) over an ordered path and a trust anchor, returning a structured verdict with per-check reason codes; `crlChecker` supplies CRL-based revocation. Pure and re-entrant, fail-closed — `validate`, `crlChecker` |
|
|
207
208
|
| `pki.ct` | Parse RFC 6962 Certificate Transparency SCT lists — `parseSctList` decodes the `SignedCertificateTimestampList` a certificate or OCSP response carries in the SCT extension (a TLS-presentation-language payload inside the §3.3 double DER wrap) into per-SCT log id, exact `timestamp` (BigInt), named signature algorithm, and raw signature; `reconstructSignedData` rebuilds the exact `digitally-signed` preimage for external verification. Structure decoded, crypto surfaced raw, bounded decode, fail-closed — `parseSctList`, `reconstructSignedData` |
|
package/lib/framework-error.js
CHANGED
|
@@ -179,6 +179,14 @@ var CmpError = defineClass("CmpError", { withCause: true });
|
|
|
179
179
|
// `.cause`.
|
|
180
180
|
var PathError = defineClass("PathError", { withCause: true });
|
|
181
181
|
|
|
182
|
+
// SmimeError — a byte sequence that is not a well-formed RFC 5035 ESS
|
|
183
|
+
// signing-certificate attribute (SigningCertificate / SigningCertificateV2 /
|
|
184
|
+
// ESSCertID(v2) / IssuerSerial) or RFC 8551 SMIMECapabilities, or a CMS
|
|
185
|
+
// attribute violating a §2.5 coherence rule (single-AttributeValue, certs
|
|
186
|
+
// non-empty, non-canonical DEFAULT hashAlgorithm). Carries the underlying leaf
|
|
187
|
+
// fault (e.g. the inner asn1/* decode error) as `.cause`.
|
|
188
|
+
var SmimeError = defineClass("SmimeError", { withCause: true });
|
|
189
|
+
|
|
182
190
|
// CtError — a byte sequence that is not a well-formed RFC 6962 Certificate
|
|
183
191
|
// Transparency SCT list: a malformed inner DER OCTET-STRING wrap, or a TLS
|
|
184
192
|
// presentation-language framing violation (a lying vector length, a field read
|
|
@@ -209,4 +217,5 @@ module.exports = {
|
|
|
209
217
|
CmpError: CmpError,
|
|
210
218
|
PathError: PathError,
|
|
211
219
|
CtError: CtError,
|
|
220
|
+
SmimeError: SmimeError,
|
|
212
221
|
};
|
package/lib/oid.js
CHANGED
|
@@ -141,7 +141,8 @@ var FAMILIES = {
|
|
|
141
141
|
// and the PKCS#12 bag attributes friendlyName / localKeyId (RFC 7292 §4.2).
|
|
142
142
|
pkcs9: { base: [1, 2, 840, 113549, 1, 9], of: {
|
|
143
143
|
emailAddress: 1, contentType: 3, messageDigest: 4, signingTime: 5,
|
|
144
|
-
challengePassword: 7, extensionRequest: 14,
|
|
144
|
+
challengePassword: 7, extensionRequest: 14, smimeCapabilities: 15,
|
|
145
|
+
friendlyName: 20, localKeyId: 21 } },
|
|
145
146
|
|
|
146
147
|
// PKCS#9 CertBag / CRLBag value discriminators (RFC 7292 §4.2.3-§4.2.4).
|
|
147
148
|
pkcs9CertTypes: { base: [1, 2, 840, 113549, 1, 9, 22], of: { x509Certificate: 1, sdsiCertificate: 2 } },
|
|
@@ -188,7 +189,11 @@ var FAMILIES = {
|
|
|
188
189
|
// S/MIME authenticated attributes (id-aa, RFC 2634 / RFC 5035 / RFC 5816). The ESS
|
|
189
190
|
// signing-certificate attributes bind a CMS / TSP SignerInfo to its signing cert;
|
|
190
191
|
// signingCertificateV2 (ESSCertIDv2) carries a non-SHA-1 cert hash (RFC 5816 §2.2.1).
|
|
192
|
+
// The RFC 2634 originals (receiptRequest .. contentReference) are name-only: they
|
|
193
|
+
// resolve to a name so an unsupported-attribute diagnostic is legible, not decoded.
|
|
191
194
|
smimeAa: { base: [1, 2, 840, 113549, 1, 9, 16, 2], of: {
|
|
195
|
+
receiptRequest: 1, eSSSecurityLabel: 2, mlExpansionHistory: 3, contentHints: 4,
|
|
196
|
+
msgSigDigest: 5, contentIdentifier: 7, equivalentLabels: 9, contentReference: 10,
|
|
192
197
|
signingCertificate: 12, timeStampToken: 14, signingCertificateV2: 47 } },
|
|
193
198
|
|
|
194
199
|
// ANSI X9.62 EC public key, named curve, and ECDSA signatures.
|
package/lib/schema-all.js
CHANGED
|
@@ -43,6 +43,11 @@ var tsp = require("./schema-tsp");
|
|
|
43
43
|
var crmf = require("./schema-crmf");
|
|
44
44
|
var cmp = require("./schema-cmp");
|
|
45
45
|
var attrcert = require("./schema-attrcert");
|
|
46
|
+
// smime is a COMPANION decoder for CMS signed-attribute values, NOT a top-level
|
|
47
|
+
// format — it has no self-describing DER root (an ESS attribute value is a bare
|
|
48
|
+
// SEQUENCE) so it is deliberately absent from FORMATS / the detect-and-route
|
|
49
|
+
// `parse`; it is reached only by explicit OID dispatch on a CMS attribute.
|
|
50
|
+
var smime = require("./schema-smime");
|
|
46
51
|
var frameworkError = require("./framework-error");
|
|
47
52
|
|
|
48
53
|
var SchemaError = frameworkError.SchemaError;
|
|
@@ -277,6 +282,7 @@ module.exports = {
|
|
|
277
282
|
crmf: { parse: crmf.parse, pemDecode: crmf.pemDecode },
|
|
278
283
|
cmp: { parse: cmp.parse, pemDecode: cmp.pemDecode, pemEncode: cmp.pemEncode },
|
|
279
284
|
attrcert: { parse: attrcert.parse, pemDecode: attrcert.pemDecode },
|
|
285
|
+
smime: { parseSigningCertificate: smime.parseSigningCertificate, parseSigningCertificateV2: smime.parseSigningCertificateV2, parseSmimeCapabilities: smime.parseSmimeCapabilities, decodeAttribute: smime.decodeAttribute },
|
|
280
286
|
all: all,
|
|
281
287
|
parse: parse,
|
|
282
288
|
};
|
package/lib/schema-pkix.js
CHANGED
|
@@ -370,6 +370,28 @@ function generalNames(ns, opts) {
|
|
|
370
370
|
// `decodeExt` pattern (asn1.decode → read.* / schema.walk, wrapped). Returns
|
|
371
371
|
// { byOid } keyed by dotted OID so a consumer dispatches by ext.oid.
|
|
372
372
|
var _T = asn1.TAGS;
|
|
373
|
+
|
|
374
|
+
// assertPolicyQualifiers(qNode, fail) — RFC 5280 §4.2.1.4: a PolicyInformation's
|
|
375
|
+
// policyQualifiers is a non-empty SEQUENCE OF PolicyQualifierInfo, each a SEQUENCE
|
|
376
|
+
// { policyQualifierId OID, qualifier } of EXACTLY two members led by an OID. The
|
|
377
|
+
// structure is validated fail-closed; the qualifier body stays opaque (surfaced
|
|
378
|
+
// raw by the caller). `fail(msg)` throws the caller's typed code. Shared by the
|
|
379
|
+
// certificatePolicies extension decoder and the RFC 5035 ESS SigningCertificate
|
|
380
|
+
// policies field so the same PolicyInformation shape cannot drift between them.
|
|
381
|
+
// `fail(msg, cause?)` throws the caller's typed code, carrying the underlying
|
|
382
|
+
// leaf fault as the error cause when one is available.
|
|
383
|
+
function assertPolicyQualifiers(qNode, fail) {
|
|
384
|
+
if (qNode.tagClass !== "universal" || qNode.tagNumber !== _T.SEQUENCE || !qNode.children || !qNode.children.length) {
|
|
385
|
+
fail("policyQualifiers must be a non-empty SEQUENCE (RFC 5280 §4.2.1.4)");
|
|
386
|
+
}
|
|
387
|
+
qNode.children.forEach(function (pq) {
|
|
388
|
+
if (pq.tagClass !== "universal" || pq.tagNumber !== _T.SEQUENCE || !pq.children || pq.children.length !== 2) {
|
|
389
|
+
fail("policyQualifiers element must be a PolicyQualifierInfo SEQUENCE { policyQualifierId, qualifier } (RFC 5280 §4.2.1.4)");
|
|
390
|
+
}
|
|
391
|
+
try { asn1.read.oid(pq.children[0]); } catch (e) { fail("PolicyQualifierInfo must lead with a policyQualifierId OID", e); }
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
373
395
|
function certExtensionDecoders(ns) {
|
|
374
396
|
var GN_SUBTREE = generalName(ns, { decodeValue: true, subtreeBase: true, code: ns.prefix + "/bad-name-constraints" });
|
|
375
397
|
|
|
@@ -495,20 +517,9 @@ function certExtensionDecoders(ns) {
|
|
|
495
517
|
var qualifiers = null;
|
|
496
518
|
if (pi.children.length > 1) {
|
|
497
519
|
var q = pi.children[1];
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
501
|
-
// Each element is a PolicyQualifierInfo ::= SEQUENCE { policyQualifierId
|
|
502
|
-
// OID, qualifier ANY DEFINED BY policyQualifierId } — EXACTLY two members
|
|
503
|
-
// (qualifier is not OPTIONAL). A SEQUENCE missing the qualifier or
|
|
504
|
-
// carrying trailing extra fields is malformed; the qualifier body itself
|
|
505
|
-
// stays opaque.
|
|
506
|
-
q.children.forEach(function (pq) {
|
|
507
|
-
if (pq.tagClass !== "universal" || pq.tagNumber !== _T.SEQUENCE || !pq.children || pq.children.length !== 2) {
|
|
508
|
-
throw ns.E(C, "policyQualifiers element must be a PolicyQualifierInfo SEQUENCE { policyQualifierId, qualifier } (RFC 5280 §4.2.1.4)");
|
|
509
|
-
}
|
|
510
|
-
try { asn1.read.oid(pq.children[0]); } catch (e) { throw ns.E(C, "PolicyQualifierInfo must lead with a policyQualifierId OID", e); }
|
|
511
|
-
});
|
|
520
|
+
// PolicyQualifierInfo structure validated by the shared assertion; the
|
|
521
|
+
// qualifier body stays opaque (surfaced raw).
|
|
522
|
+
assertPolicyQualifiers(q, function (msg, cause) { throw ns.E(C, msg, cause); });
|
|
512
523
|
qualifiers = q.bytes;
|
|
513
524
|
}
|
|
514
525
|
return { policyIdentifier: pid, qualifiersBytes: qualifiers };
|
|
@@ -838,6 +849,7 @@ module.exports = {
|
|
|
838
849
|
makeParser: makeParser,
|
|
839
850
|
signedEnvelopeTbs: signedEnvelopeTbs,
|
|
840
851
|
rootSequenceChildren: rootSequenceChildren,
|
|
852
|
+
assertPolicyQualifiers: assertPolicyQualifiers,
|
|
841
853
|
signedEnvelope: signedEnvelope,
|
|
842
854
|
attrValueToString: attrValueToString,
|
|
843
855
|
attributeTypeAndValue: attributeTypeAndValue,
|
|
@@ -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 §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 §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 §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 §5: the signing certificate (certs[0], the public-key cert that made
|
|
90
|
+
// the signature per §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 §5)");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// PolicyInformation ::= SEQUENCE { policyIdentifier CertPolicyId,
|
|
103
|
+
// policyQualifiers SEQUENCE OF PolicyQualifierInfo OPTIONAL } (RFC 5280 §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
|
+
// §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 §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 §5.4.1 / §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 §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 §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 §5.4.2). certs is
|
|
191
|
+
// order-preserving — RFC 5035 §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 §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 §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 §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 §5.4.1).
|
|
270
|
+
* Identical shape to v1 but each `certs` entry carries a real `hashAlgorithm`:
|
|
271
|
+
* decoded when present, or the RFC 5035 §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 §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 §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 §2.5.2 / §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
|
+
// §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 §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
|
+
};
|
package/package.json
CHANGED
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:
|
|
5
|
+
"serialNumber": "urn:uuid:38c53855-3eaa-4d1a-8c48-b03540881d8a",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-09T18:46:47.249Z",
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.1.22",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.1.
|
|
25
|
+
"version": "0.1.22",
|
|
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.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.1.22",
|
|
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.
|
|
57
|
+
"ref": "@blamejs/pki@0.1.22",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|