@blamejs/pki 0.2.30 → 0.2.32
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 +25 -0
- package/README.md +1 -1
- package/lib/inspect.js +33 -0
- package/lib/lint.js +27 -8
- package/lib/oid.js +21 -1
- package/lib/path-validate.js +21 -0
- package/lib/schema-pkix.js +173 -1
- package/lib/sigstore.js +12 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,31 @@ 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.2.32 — 2026-07-16
|
|
8
|
+
|
|
9
|
+
X.509 certificates now decode the Microsoft Active Directory Certificate Services enrollment extensions, and pki.lint's critical-extension check is aligned with certification-path validation.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.schema.x509.parse decodes the Microsoft Active Directory Certificate Services enrollment extensions ([MS-WCCE] / [MS-CRTD]): the v2 certificate template (szOID-CERTIFICATE_TEMPLATE -- template OID plus major/minor version), the legacy v1 template name (szOID-ENROLL_CERTTYPE_EXTENSION), the CA version (szOID-CERTSRV_CA_VERSION, surfaced as the raw value and as the CA key index / certificate index split), the previous-CA-certificate hash (szOID-CERTSRV_PREVIOUS_CERT_HASH), and the application policies (szOID-APPLICATION_CERT_POLICIES, decoded as RFC 5280 certificate policies). Each is rendered by pki.inspect and fails closed with a typed error on a malformed shape. The version fields accept the full 32-bit range Active Directory uses, not a signed subset.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- pki.lint's unrecognized-critical-extension check now mirrors certification-path validation: it reports a critical extension whose semantics the path validator does not process -- an authority/subject key identifier, a freshest-CRL pointer, an SCT list, a qualified-certificate statement, or a Microsoft enterprise-CA extension -- rather than treating any extension it can merely decode as processed. precertificatePoison, which RFC 6962 requires to be critical, is not reported. A conforming relying party must reject a critical extension it cannot process, so a certificate whose critical enterprise or qualified-certificate constraints this toolkit does not enforce is now surfaced by the linter.
|
|
18
|
+
|
|
19
|
+
## v0.2.31 — 2026-07-16
|
|
20
|
+
|
|
21
|
+
X.509 certificates now decode the RFC 3739 / ETSI EN 319 412-5 qualified-certificate qcStatements extension, and the sigstore verifier gains a low-order EdDSA public-key gate.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- The toolkit decodes the RFC 3739 sec. 3.2.6 / ETSI EN 319 412-5 qualified-certificate qcStatements extension (id-pe-qcStatements). The decoded statements -- QcCompliance (EU-qualified), QcLimitValue (reliance limit), QcSSCD (key in a QSCD), QcType (certificate purpose: esign / eseal / web), QcRetentionPeriod, QcPDS (disclosure-statement URLs), QcCClegislation (country of qualification), QcIdentMethod, QcQSCDlegislation, and the PKIX SemanticsInformation -- are decoded and validated by the toolkit's certificate-extension decoders and rendered by pki.inspect. An unknown statementId is preserved opaque (its raw bytes, semantics not executed); a malformed shape fails closed with a typed error.
|
|
26
|
+
|
|
27
|
+
### Security
|
|
28
|
+
|
|
29
|
+
- The sigstore keyless-bundle verifier now routes an Ed25519/Ed448 Fulcio leaf or Rekor log public key through the shared full-order Edwards-point gate before verification. A low-order OKP key (which node imports without complaint and which can verify a forged EdDSA signature) is rejected at key-parse (sigstore/bad-key) instead of being handed to verify -- the same defense the webauthn and certification-path EdDSA verifiers already apply.
|
|
30
|
+
- Certification-path validation rejects a CRITICAL qcStatements extension (RFC 5280 sec. 6.1.4). A critical qualified-certificate statement asserts semantics a relying party must enforce -- a QcLimitValue reliance limit, a QcType certificate purpose -- and the validator does not enforce them, so treating the extension as processed would let a caller rely on a certificate outside its asserted qualified-certificate constraints; it is left unprocessed and fails as an unrecognized critical extension. A non-critical qcStatements is informational and does not affect the verdict; the extension is still decoded for pki.inspect and lint, and the linter flags a critical qcStatements as an unrecognized critical extension for the same reason -- structural decodability is not validation processing.
|
|
31
|
+
|
|
7
32
|
## v0.2.30 — 2026-07-16
|
|
8
33
|
|
|
9
34
|
C509 CBOR-encoded certificates arrive as pki.schema.c509.parse: decode the compact CBOR profile of X.509 in both its natively-signed and X.509-re-encoded forms, reconstructing the original DER byte-for-byte so the original signature still verifies.
|
package/README.md
CHANGED
|
@@ -203,7 +203,7 @@ is callable today; nothing below is a stub.
|
|
|
203
203
|
| `pki.oid` | Two-way OID ↔ name registry — `name`, `byName`, `register`, `toArcs`/`fromArcs`, `toDER`/`fromDER`; seeded with RFC 5280 + NIST PQC arcs |
|
|
204
204
|
| `pki.webcrypto` | A W3C WebCrypto (`SubtleCrypto`) engine over `node:crypto` — `sign`/`verify`/`encrypt`/`decrypt`/`deriveBits`/`digest`/`generateKey`/`importKey`/`exportKey` across RSA, ECDSA, ECDH, Ed25519/Ed448, AES, HMAC, HKDF, PBKDF2, SHA — **and** post-quantum ML-DSA-44/65/87 and SLH-DSA signatures, plus ML-KEM-512/768/1024 key generation and certificate/PKCS#8 import — the RFC 9935 seed / expandedKey / both private-key CHOICE is validated fail-closed, so an OpenSSL-legacy bare-seed or an internally inconsistent key is rejected with a typed error (KEM encapsulation lands with CMS KEM-decrypt). Zero-dependency, OpenSSL-interoperable |
|
|
205
205
|
| `pki.schema` | The schema family — `parse` detects which PKI format DER / PEM encodes and routes to the right parser, `all` enumerates the registered formats, and the engine + per-format members are grouped here |
|
|
206
|
-
| `pki.schema.x509` | Parse DER / PEM certificates into structured, validated fields — `parse`, `pemDecode`, `pemEncode` |
|
|
206
|
+
| `pki.schema.x509` | Parse DER / PEM certificates into structured, validated fields, with named + partly-decoded extensions — including the RFC 3739 / ETSI EN 319 412-5 qualified-certificate `qcStatements` (EU-qualified declaration, reliance limit, QSCD flag, certificate type, retention, PDS URLs, country of qualification; unknown statements preserved opaque) and the Microsoft Active Directory Certificate Services enrollment extensions (certificate template, CA version, previous-CA-certificate hash, application policies), fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
207
207
|
| `pki.schema.c509` | Parse C509 CBOR-encoded certificates (draft-ietf-cose-cbor-encoded-cert) — the compact CBOR profile of X.509, decoded fail-closed under deterministic CBOR; an explicit `parse` call (CBOR, not DER, so not auto-routed) |
|
|
208
208
|
| `pki.schema.crl` | Parse DER / PEM X.509 CRLs per RFC 5280 §5 — revoked serials with real-`Date` revocation times, named + partly-decoded extensions, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
209
209
|
| `pki.schema.csr` | Parse DER / PEM PKCS#10 certification requests per RFC 2986 — subject DN, public key, requested attributes, signature, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
package/lib/inspect.js
CHANGED
|
@@ -317,6 +317,37 @@ var EXT_RENDERERS = {
|
|
|
317
317
|
if (decoded.pathLenConstraint != null) s += ", pathlen:" + decoded.pathLenConstraint;
|
|
318
318
|
return inner + s;
|
|
319
319
|
},
|
|
320
|
+
qcStatements: function (decoded, inner) {
|
|
321
|
+
return decoded.map(function (s) {
|
|
322
|
+
var label = inner + (s.name || s.statementId), info = s.info;
|
|
323
|
+
if (!info) return label; // presence-only (QcCompliance / QcSSCD)
|
|
324
|
+
if (info.opaque) return label + " (opaque)";
|
|
325
|
+
if (typeof info.amount !== "undefined") return label + ": " + info.amount + " " + info.currency + (info.exponent ? " x10^" + info.exponent : ""); // QcLimitValue
|
|
326
|
+
if (typeof info.years !== "undefined") return label + ": " + info.years + " years"; // QcRetentionPeriod
|
|
327
|
+
if (info.typeNames) return label + ": " + info.typeNames.map(function (n, i) { return n || info.types[i]; }).join(", "); // QcType
|
|
328
|
+
if (info.methodNames) return label + ": " + info.methodNames.map(function (n, i) { return n || info.methods[i]; }).join(", "); // QcIdentMethod
|
|
329
|
+
if (info.locations) return label + ": " + info.locations.map(function (l) { return l.url + " (" + l.language + ")"; }).join(", "); // QcPDS
|
|
330
|
+
if (info.countries) return label + ": " + info.countries.join(", "); // QcCClegislation / QcQSCDlegislation
|
|
331
|
+
if (typeof info.semanticsIdentifier !== "undefined") { // SemanticsInformation (id-qcs)
|
|
332
|
+
var nra = info.nameRegistrationAuthorities && info.nameRegistrationAuthorities.length;
|
|
333
|
+
return label + (info.semanticsIdentifier ? ": " + info.semanticsIdentifier : "") + (nra ? " (" + nra + " NRA)" : "");
|
|
334
|
+
}
|
|
335
|
+
return label;
|
|
336
|
+
}).join("\n");
|
|
337
|
+
},
|
|
338
|
+
msCertificateTemplate: function (decoded, inner) {
|
|
339
|
+
var v = decoded.templateMajorVersion === null ? "" : " v" + decoded.templateMajorVersion + "." + (decoded.templateMinorVersion === null ? 0 : decoded.templateMinorVersion);
|
|
340
|
+
return inner + "Template: " + (decoded.name || decoded.templateID) + v;
|
|
341
|
+
},
|
|
342
|
+
msEnrollCertType: function (decoded, inner) {
|
|
343
|
+
return inner + "Cert Type: " + _clean(String(decoded));
|
|
344
|
+
},
|
|
345
|
+
msCaVersion: function (decoded, inner) {
|
|
346
|
+
return inner + "CA Version: V" + decoded.caKeyIndex + "." + decoded.certIndex;
|
|
347
|
+
},
|
|
348
|
+
msPreviousCertHash: function (decoded, inner) {
|
|
349
|
+
return inner + _hexColon(Buffer.isBuffer(decoded) ? decoded : Buffer.alloc(0), { upper: true });
|
|
350
|
+
},
|
|
320
351
|
subjectAltName: _renderAltName,
|
|
321
352
|
issuerAltName: _renderAltName,
|
|
322
353
|
certificatePolicies: function (decoded, inner) {
|
|
@@ -404,6 +435,8 @@ var EXT_RENDERERS = {
|
|
|
404
435
|
return akiLines.length ? akiLines.join("\n") : inner + "keyid:(none)";
|
|
405
436
|
},
|
|
406
437
|
};
|
|
438
|
+
// [MS-WCCE] szOID-APPLICATION_CERT_POLICIES decodes as certificatePolicies, so it renders identically.
|
|
439
|
+
EXT_RENDERERS.msApplicationPolicies = EXT_RENDERERS.certificatePolicies;
|
|
407
440
|
|
|
408
441
|
// Coverage residual -- the EXT_RENDERERS entry fallbacks are unreachable because each shared
|
|
409
442
|
// decoder (and asn1.read.oid) already narrows the shape before the renderer runs:
|
package/lib/lint.js
CHANGED
|
@@ -29,6 +29,7 @@ var guard = require("./guard-all");
|
|
|
29
29
|
var oid = require("./oid");
|
|
30
30
|
var x509 = require("./schema-x509");
|
|
31
31
|
var pkix = require("./schema-pkix");
|
|
32
|
+
var path = require("./path-validate");
|
|
32
33
|
var C = require("./constants");
|
|
33
34
|
var ipUtils = require("./ip-utils");
|
|
34
35
|
|
|
@@ -237,14 +238,32 @@ function _subjectCNs(cert) {
|
|
|
237
238
|
return out;
|
|
238
239
|
}
|
|
239
240
|
|
|
240
|
-
// An extension is
|
|
241
|
-
// it
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
|
|
241
|
+
// An extension's CRITICALITY is honoured (RFC 5280 4.2: a consumer MUST reject a critical extension
|
|
242
|
+
// it cannot PROCESS) only when this toolkit actually processes its semantics. That authority is
|
|
243
|
+
// path-validate's PROCESSED_EXTENSIONS (the RFC 5280 sec. 6.1 path-processing set), NOT the decoder
|
|
244
|
+
// table: certExtensionDecoders also DECODES many extensions purely for display (qcStatements, the MS
|
|
245
|
+
// enterprise-CA extensions, the SCT list, the *KeyIdentifiers, freshestCRL, issuerAltName) whose
|
|
246
|
+
// critical semantics are not enforced -- path-validate rejects a critical instance of any of those
|
|
247
|
+
// as unrecognized-critical, so the linter flags them too (structural decodability is not validation
|
|
248
|
+
// processing). The one decode-only extension that is LEGITIMATELY critical is precertificatePoison
|
|
249
|
+
// (RFC 6962 sec. 3.1 REQUIRES it critical), so it is allow-listed. Everything else -- a processed
|
|
250
|
+
// extension, or an unregistered OID (algorithm / EKU-purpose OID the name registry resolves but that
|
|
251
|
+
// is no extension) -- falls out correctly: processed -> recognized, unregistered -> unknown. Driving
|
|
252
|
+
// this off the validator's set (not the decoder table) means a newly-decoded extension is
|
|
253
|
+
// flagged-when-critical automatically until its semantics are actually processed.
|
|
254
|
+
var _CRITICAL_LEGIT = {};
|
|
255
|
+
_CRITICAL_LEGIT[oid.byName("precertificatePoison")] = true;
|
|
256
|
+
// Mirrors path-validate's unrecognizedCriticalExtension. An extension not in PROCESSED_EXTENSIONS is
|
|
257
|
+
// unknown everywhere (precertificatePoison is the one legitimately-critical exception). An extension
|
|
258
|
+
// that IS processed for an intermediate but is unprocessed on the TARGET (policyMappings) is also
|
|
259
|
+
// flagged: path-validate decides target status purely by path position (i === n), so ANY certificate
|
|
260
|
+
// -- a CA included -- can be validated as the final/target certificate, where a critical instance is
|
|
261
|
+
// rejected. A linter has no path context and cannot rule that out, so it flags these regardless of
|
|
262
|
+
// cA-ness (policyMappings is SHOULD-be-non-critical everywhere, so this never mis-flags conformant input).
|
|
263
|
+
function _isUnknownExtension(extOid) {
|
|
264
|
+
if (path.PROCESSED_EXTENSIONS[extOid] !== true) return _CRITICAL_LEGIT[extOid] !== true;
|
|
265
|
+
return path.TARGET_UNPROCESSED_IF_CRITICAL[extOid] === true;
|
|
266
|
+
}
|
|
248
267
|
|
|
249
268
|
// RSA-modulus / EC-curve helpers for the weak-key lint. Each returns null when the key
|
|
250
269
|
// material cannot be read as expected (a malformed RSA modulus, or EC parameters that are
|
package/lib/oid.js
CHANGED
|
@@ -80,7 +80,27 @@ var FAMILIES = {
|
|
|
80
80
|
// aaControls sec. 7.4, ac-proxying sec. 7.2).
|
|
81
81
|
pkixAccess: { base: [1, 3, 6, 1, 5, 5, 7, 1], of: {
|
|
82
82
|
authorityInfoAccess: 1, acAuditIdentity: 4, aaControls: 6, acProxying: 10,
|
|
83
|
-
acmeIdentifier: 31
|
|
83
|
+
acmeIdentifier: 31,
|
|
84
|
+
// id-pe-qcStatements (RFC 3739 sec. 3.2.6) -- the qualified-certificate-statements extension.
|
|
85
|
+
qcStatements: 3 } },
|
|
86
|
+
|
|
87
|
+
// RFC 3739 sec. 3.2.6.1 id-qcs -- the PKIX QCStatement statementIds carrying SemanticsInformation.
|
|
88
|
+
pkixQcSyntax: { base: [1, 3, 6, 1, 5, 5, 7, 11], of: { qcsPkixQCSyntaxV1: 1, qcsPkixQCSyntaxV2: 2 } },
|
|
89
|
+
|
|
90
|
+
// ETSI EN 319 412-5 qualified-certificate statements (esi4-qcStatement-N) on the id-etsi-qcs arc.
|
|
91
|
+
etsiQcs: { base: [0, 4, 0, 1862, 1], of: {
|
|
92
|
+
qcCompliance: 1, qcLimitValue: 2, qcRetentionPeriod: 3, qcSSCD: 4, qcPDS: 5,
|
|
93
|
+
qcType: 6, qcCClegislation: 7, qcIdentMethod: 8, qcQSCDlegislation: 9 } },
|
|
94
|
+
|
|
95
|
+
// ETSI EN 319 412-5 QcType member OIDs (the certificate-purpose types) on the id-etsi-qct arc.
|
|
96
|
+
etsiQcType: { base: [0, 4, 0, 1862, 1, 6], of: { qctEsign: 1, qctEseal: 2, qctWeb: 3 } },
|
|
97
|
+
|
|
98
|
+
// [MS-WCCE] AD CS enrollment extensions on the id-microsoft-21 arc (szOID-CERTSRV / szOID-CERTIFICATE_TEMPLATE / szOID-APPLICATION_CERT_POLICIES).
|
|
99
|
+
msEnrollment: { base: [1, 3, 6, 1, 4, 1, 311, 21], of: {
|
|
100
|
+
msCaVersion: 1, msPreviousCertHash: 2, msCertificateTemplate: 7, msApplicationPolicies: 10 } },
|
|
101
|
+
|
|
102
|
+
// [MS-CRTD] legacy v1 enroll cert-type name on the id-microsoft-20 arc (szOID-ENROLL_CERTTYPE_EXTENSION).
|
|
103
|
+
msEnrollmentLegacy: { base: [1, 3, 6, 1, 4, 1, 311, 20], of: { msEnrollCertType: 2 } },
|
|
84
104
|
|
|
85
105
|
// id-aca -- RFC 5755 attribute-certificate attribute types on the id-pkix 10 arc:
|
|
86
106
|
// authenticationInfo sec. 4.4.1 .. group sec. 4.4.4, plus encAttrs sec. 7.1 (the encrypted-
|
package/lib/path-validate.js
CHANGED
|
@@ -91,10 +91,21 @@ var PROCESSED_EXTENSIONS = {};
|
|
|
91
91
|
// validateCriticalExtensionStructure via the registered decoder. freshestCRL
|
|
92
92
|
// stays OUT: sec. 4.2.1.15 requires it non-critical and the validator does not
|
|
93
93
|
// consult it (no delta merge), so a critical instance fails unrecognized.
|
|
94
|
+
// qcStatements is deliberately NOT processed: a critical QC statement (QcLimitValue reliance limit,
|
|
95
|
+
// QcType certificate purpose) asserts qualified-certificate semantics a relying party MUST enforce, and
|
|
96
|
+
// this validator does not enforce them (nor expose a handler to). Marking it processed would let a caller
|
|
97
|
+
// treat a certificate as valid outside its asserted critical QC constraints, so a critical qcStatements
|
|
98
|
+
// fails as an unrecognized-critical extension (RFC 5280 sec. 6.1.4); a non-critical instance is
|
|
99
|
+
// informational and does not affect the verdict. The extension is still decoded for pki.inspect / lint.
|
|
94
100
|
[OID.basicConstraints, OID.keyUsage, OID.nameConstraints, OID.certificatePolicies,
|
|
95
101
|
OID.policyMappings, OID.policyConstraints, OID.inhibitAnyPolicy, OID.subjectAltName,
|
|
96
102
|
OID.extKeyUsage, OID.cRLDistributionPoints].
|
|
97
103
|
forEach(function (o) { PROCESSED_EXTENSIONS[o] = true; });
|
|
104
|
+
// Frozen after seeding: this exact object is both consulted by the critical-extension check here and
|
|
105
|
+
// exported (for pki.lint to stay consistent). A caller must not be able to add an OID -- doing so
|
|
106
|
+
// would make an attacker's critical, decoder-less extension pass as "processed" and skip both the
|
|
107
|
+
// unrecognized-critical check and structural validation. Freezing makes any such write a no-op.
|
|
108
|
+
Object.freeze(PROCESSED_EXTENSIONS);
|
|
98
109
|
|
|
99
110
|
// ---- signature verify bridge (NEW 6) ---------------------------------------
|
|
100
111
|
|
|
@@ -1055,6 +1066,7 @@ function applyPolicyMappings(state, mappings, i) {
|
|
|
1055
1066
|
// a terminal CA cert is conforming and is NOT treated as unprocessed here.)
|
|
1056
1067
|
var TARGET_UNPROCESSED_IF_CRITICAL = {};
|
|
1057
1068
|
TARGET_UNPROCESSED_IF_CRITICAL[OID.policyMappings] = true;
|
|
1069
|
+
Object.freeze(TARGET_UNPROCESSED_IF_CRITICAL);
|
|
1058
1070
|
|
|
1059
1071
|
function unrecognizedCriticalExtension(cert, isTarget) {
|
|
1060
1072
|
for (var i = 0; i < cert.extensions.length; i++) {
|
|
@@ -2017,4 +2029,13 @@ module.exports = {
|
|
|
2017
2029
|
crlChecker: crlChecker,
|
|
2018
2030
|
ocspChecker: ocspChecker,
|
|
2019
2031
|
verifyOcspResponse: verifyOcspResponse,
|
|
2032
|
+
// The set of extension OIDs whose CRITICAL semantics this validator processes (RFC 5280 sec. 6.1).
|
|
2033
|
+
// Exposed so a linter can distinguish "processed" from "merely decoded" and stay consistent with
|
|
2034
|
+
// the path-validation verdict on a critical extension -- a decoder in certExtensionDecoders is NOT
|
|
2035
|
+
// by itself proof the criticality is honoured. Both sets are frozen so a caller cannot mutate them.
|
|
2036
|
+
PROCESSED_EXTENSIONS: PROCESSED_EXTENSIONS,
|
|
2037
|
+
// Extensions that ARE processed for an intermediate CA but are unprocessed on the target/leaf, so a
|
|
2038
|
+
// critical instance on the target fails closed (RFC 5280 sec. 6.1.5(f)) -- policyMappings is
|
|
2039
|
+
// prepare-next-only and SHOULD-be-non-critical. A linter mirrors this for a leaf certificate.
|
|
2040
|
+
TARGET_UNPROCESSED_IF_CRITICAL: TARGET_UNPROCESSED_IF_CRITICAL,
|
|
2020
2041
|
};
|
package/lib/schema-pkix.js
CHANGED
|
@@ -571,7 +571,10 @@ function distributionPointName(ns, node, code) {
|
|
|
571
571
|
// inside. Each decoder takes that raw Buffer and returns the decoded value, or
|
|
572
572
|
// throws a typed `<prefix>/bad-*` code, mirroring the CRL's fail-closed
|
|
573
573
|
// `decodeExt` pattern (asn1.decode -> read.* / schema.walk, wrapped). Returns
|
|
574
|
-
// { byOid } keyed by dotted OID so a consumer dispatches by ext.oid.
|
|
574
|
+
// { byOid } keyed by dotted OID so a consumer dispatches by ext.oid. The decoded
|
|
575
|
+
// set spans the RFC 5280 sec. 4.2.1 extensions plus the RFC 3739 sec. 3.2.6 /
|
|
576
|
+
// ETSI EN 319 412-5 qualified-certificate `qcStatements` extension (each known
|
|
577
|
+
// statement decoded, an unknown statementId preserved opaque).
|
|
575
578
|
var _T = asn1.TAGS;
|
|
576
579
|
|
|
577
580
|
// assertPolicyQualifiers(qNode, fail) -- RFC 5280 sec. 4.2.1.4: a PolicyInformation's
|
|
@@ -916,7 +919,171 @@ function certExtensionDecoders(ns) {
|
|
|
916
919
|
if (typeof d !== "string") throw new TypeError("certExtensionDecoders: " + JSON.stringify(nm) + " is not a registered OID name");
|
|
917
920
|
return d;
|
|
918
921
|
}
|
|
922
|
+
// qcStatements ::= SEQUENCE OF QCStatement (RFC 3739 sec. 3.2.6). QCStatement ::= SEQUENCE {
|
|
923
|
+
// statementId OBJECT IDENTIFIER, statementInfo ANY DEFINED BY statementId OPTIONAL }. Known statementIds
|
|
924
|
+
// (RFC 3739 id-qcs + the ETSI EN 319 412-5 esi4 catalog) decode their statementInfo; an unknown statementId
|
|
925
|
+
// is preserved OPAQUE (the RFC 3739 open type), never rejected. The DEFINED-BY selection is an OID re-dispatch.
|
|
926
|
+
function _qcStr(node, tag, C, what) {
|
|
927
|
+
if (!node || node.tagClass !== "universal" || node.tagNumber !== tag) throw ns.E(C, what);
|
|
928
|
+
try { return asn1.read.string(node); } catch (e) { throw ns.E(C, what, e); }
|
|
929
|
+
}
|
|
930
|
+
function _qcOidSeq(node, C, what) { // a non-empty SEQUENCE OF OBJECT IDENTIFIER
|
|
931
|
+
if (!node || node.tagClass !== "universal" || node.tagNumber !== _T.SEQUENCE || !node.children || !node.children.length) throw ns.E(C, what + " must be a non-empty SEQUENCE OF OBJECT IDENTIFIER");
|
|
932
|
+
return node.children.map(function (c) {
|
|
933
|
+
if (c.tagClass !== "universal" || c.tagNumber !== _T.OBJECT_IDENTIFIER) throw ns.E(C, what + " members must be OBJECT IDENTIFIERs");
|
|
934
|
+
try { return asn1.read.oid(c); } catch (e) { throw ns.E(C, what + " member is not a valid OID", e); }
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
function _qcPresenceOnly(nm) { return function (node, C) { if (node) throw ns.E(C, nm + " carries no statementInfo (ETSI EN 319 412-5)"); return null; }; }
|
|
938
|
+
function _qcSemantics(node, C) { // SemanticsInformation, WITH COMPONENTS at least one field present
|
|
939
|
+
if (node === null) return null; // statementInfo OPTIONAL for id-qcs syntax statements
|
|
940
|
+
if (node.tagClass !== "universal" || node.tagNumber !== _T.SEQUENCE || !node.children || !node.children.length) throw ns.E(C, "SemanticsInformation must be a SEQUENCE with at least one field (RFC 3739 sec. 3.2.6.1)");
|
|
941
|
+
var sid = null, i = 0;
|
|
942
|
+
if (node.children[0].tagClass === "universal" && node.children[0].tagNumber === _T.OBJECT_IDENTIFIER) {
|
|
943
|
+
try { sid = asn1.read.oid(node.children[0]); } catch (e) { throw ns.E(C, "SemanticsInformation semanticsIdentifier must be an OID", e); }
|
|
944
|
+
i++;
|
|
945
|
+
}
|
|
946
|
+
var nras = [];
|
|
947
|
+
if (node.children[i]) {
|
|
948
|
+
// nameRegistrationAuthorities ::= SEQUENCE SIZE (1..MAX) OF GeneralName -- decode + fully validate
|
|
949
|
+
// each element through the shared GeneralName decoder (context tag [0]..[8] + per-alternative
|
|
950
|
+
// structure), rather than surfacing arbitrary bytes as a name-registration authority.
|
|
951
|
+
nras = schema.walk(generalNames(ns, { decodeValue: true, code: C }), node.children[i], ns).result.names;
|
|
952
|
+
i++;
|
|
953
|
+
}
|
|
954
|
+
if (i !== node.children.length) throw ns.E(C, "SemanticsInformation has unexpected trailing fields");
|
|
955
|
+
return { semanticsIdentifier: sid, nameRegistrationAuthorities: nras };
|
|
956
|
+
}
|
|
957
|
+
var qcInfoByOid = {};
|
|
958
|
+
qcInfoByOid[O("qcCompliance")] = _qcPresenceOnly("QcCompliance");
|
|
959
|
+
qcInfoByOid[O("qcSSCD")] = _qcPresenceOnly("QcSSCD");
|
|
960
|
+
qcInfoByOid[O("qcsPkixQCSyntaxV1")] = _qcSemantics;
|
|
961
|
+
qcInfoByOid[O("qcsPkixQCSyntaxV2")] = _qcSemantics;
|
|
962
|
+
qcInfoByOid[O("qcType")] = function (node, C) {
|
|
963
|
+
var oids = _qcOidSeq(node, C, "QcType"); // SIZE(1) in EN 319 412-5 V2.5.1; accept >=1 for legacy certs
|
|
964
|
+
return { types: oids, typeNames: oids.map(function (d) { return ns.oid.name(d) || null; }) };
|
|
965
|
+
};
|
|
966
|
+
qcInfoByOid[O("qcRetentionPeriod")] = function (node, C) {
|
|
967
|
+
if (!node) throw ns.E(C, "QcRetentionPeriod requires an INTEGER statementInfo");
|
|
968
|
+
return { years: guard.range.uint31(readInt(node, C, "QcRetentionPeriod"), ns.E, C, "QcRetentionPeriod (years)") };
|
|
969
|
+
};
|
|
970
|
+
qcInfoByOid[O("qcLimitValue")] = function (node, C) { // MonetaryValue ::= SEQUENCE { currency, amount, exponent }
|
|
971
|
+
if (!node || node.tagClass !== "universal" || node.tagNumber !== _T.SEQUENCE || !node.children || node.children.length !== 3) throw ns.E(C, "QcLimitValue must be a MonetaryValue SEQUENCE { currency, amount, exponent }");
|
|
972
|
+
var cur = node.children[0], currency;
|
|
973
|
+
if (cur.tagClass === "universal" && cur.tagNumber === _T.PRINTABLE_STRING) {
|
|
974
|
+
// Route through _qcStr so a disallowed PrintableString byte surfaces the decoder's typed
|
|
975
|
+
// bad-qc-statement error, not a leaked asn1/bad-printable-string from the raw leaf read.
|
|
976
|
+
currency = _qcStr(cur, _T.PRINTABLE_STRING, C, "QcLimitValue alphabetic currency must be a PrintableString");
|
|
977
|
+
if (currency.length !== 3) throw ns.E(C, "QcLimitValue alphabetic currency must be a 3-letter ISO 4217 code");
|
|
978
|
+
} else if (cur.tagClass === "universal" && cur.tagNumber === _T.INTEGER) {
|
|
979
|
+
currency = guard.range.int(readInt(cur, C, "QcLimitValue numeric currency"), 1n, 999n, ns.E, C, "QcLimitValue numeric currency (ISO 4217, 1..999)");
|
|
980
|
+
} else throw ns.E(C, "QcLimitValue currency must be a PrintableString(3) or INTEGER(1..999)");
|
|
981
|
+
return {
|
|
982
|
+
currency: currency,
|
|
983
|
+
// MonetaryValue amount and exponent are unconstrained INTEGERs (the amount non-negative in practice);
|
|
984
|
+
// bound each only to the safe-integer range so it narrows to a Number losslessly -- an artificial
|
|
985
|
+
// tighter bound (uint31, or a small exponent window) would false-reject a valid MonetaryValue, and a
|
|
986
|
+
// wider one would round silently and mis-state the limit.
|
|
987
|
+
amount: guard.range.int(readInt(node.children[1], C, "QcLimitValue amount"), 0n, 9007199254740991n, ns.E, C, "QcLimitValue amount"),
|
|
988
|
+
exponent: guard.range.int(readInt(node.children[2], C, "QcLimitValue exponent"), -9007199254740991n, 9007199254740991n, ns.E, C, "QcLimitValue exponent"),
|
|
989
|
+
};
|
|
990
|
+
};
|
|
991
|
+
qcInfoByOid[O("qcCClegislation")] = function (node, C) { // SEQUENCE OF CountryName (PrintableString SIZE 2)
|
|
992
|
+
if (!node || node.tagClass !== "universal" || node.tagNumber !== _T.SEQUENCE || !node.children || !node.children.length) throw ns.E(C, "QcCClegislation must be a non-empty SEQUENCE OF CountryName");
|
|
993
|
+
return { countries: node.children.map(function (c) { var v = _qcStr(c, _T.PRINTABLE_STRING, C, "QcCClegislation CountryName must be a PrintableString"); if (v.length !== 2) throw ns.E(C, "QcCClegislation CountryName must be a 2-letter ISO 3166-1 code"); return v; }) };
|
|
994
|
+
};
|
|
995
|
+
qcInfoByOid[O("qcPDS")] = function (node, C) { // PdsLocations ::= SEQUENCE OF PdsLocation { url IA5, language Printable(2) }
|
|
996
|
+
if (!node || node.tagClass !== "universal" || node.tagNumber !== _T.SEQUENCE || !node.children || !node.children.length) throw ns.E(C, "QcPDS must be a non-empty SEQUENCE OF PdsLocation");
|
|
997
|
+
return { locations: node.children.map(function (loc) {
|
|
998
|
+
if (loc.tagClass !== "universal" || loc.tagNumber !== _T.SEQUENCE || !loc.children || loc.children.length !== 2) throw ns.E(C, "a QcPDS PdsLocation must be a SEQUENCE { url, language }");
|
|
999
|
+
var lang = _qcStr(loc.children[1], _T.PRINTABLE_STRING, C, "a QcPDS language must be a PrintableString");
|
|
1000
|
+
if (lang.length !== 2) throw ns.E(C, "a QcPDS language must be a 2-letter ISO 639-1 code");
|
|
1001
|
+
return { url: _qcStr(loc.children[0], _T.IA5_STRING, C, "a QcPDS url must be an IA5String"), language: lang };
|
|
1002
|
+
}) };
|
|
1003
|
+
};
|
|
1004
|
+
// ETSI EN 319 412-5 V2.5.1 additions: QcIdentMethod (SEQUENCE OF OID, like QcType) and
|
|
1005
|
+
// QcQSCDlegislation (SEQUENCE OF CountryName, like QcCClegislation) -- registered ids, decoded.
|
|
1006
|
+
qcInfoByOid[O("qcIdentMethod")] = function (node, C) {
|
|
1007
|
+
var oids = _qcOidSeq(node, C, "QcIdentMethod");
|
|
1008
|
+
return { methods: oids, methodNames: oids.map(function (d) { return ns.oid.name(d) || null; }) };
|
|
1009
|
+
};
|
|
1010
|
+
qcInfoByOid[O("qcQSCDlegislation")] = function (node, C) {
|
|
1011
|
+
if (!node || node.tagClass !== "universal" || node.tagNumber !== _T.SEQUENCE || !node.children || !node.children.length) throw ns.E(C, "QcQSCDlegislation must be a non-empty SEQUENCE OF CountryName");
|
|
1012
|
+
return { countries: node.children.map(function (c) { var v = _qcStr(c, _T.PRINTABLE_STRING, C, "QcQSCDlegislation CountryName must be a PrintableString"); if (v.length !== 2) throw ns.E(C, "QcQSCDlegislation CountryName must be a 2-letter ISO 3166-1 code"); return v; }) };
|
|
1013
|
+
};
|
|
1014
|
+
function qcStatements(buf) {
|
|
1015
|
+
var C = ns.prefix + "/bad-qc-statements", CS = ns.prefix + "/bad-qc-statement";
|
|
1016
|
+
var kids = seqChildren(buf, C, "QCStatements");
|
|
1017
|
+
if (!kids.length) throw ns.E(C, "QCStatements must contain at least one QCStatement (RFC 3739 sec. 3.2.6)");
|
|
1018
|
+
var out = []; // QCStatements ::= SEQUENCE OF QCStatement -- RFC 3739 imposes no statementId uniqueness; decode every statement in wire order (a linter may flag a repeat, the decoder must not drop it)
|
|
1019
|
+
for (var i = 0; i < kids.length; i++) {
|
|
1020
|
+
var st = kids[i];
|
|
1021
|
+
if (st.tagClass !== "universal" || st.tagNumber !== _T.SEQUENCE || !st.children || st.children.length < 1 || st.children.length > 2) {
|
|
1022
|
+
throw ns.E(CS, "each QCStatement must be a SEQUENCE of a statementId and an OPTIONAL statementInfo");
|
|
1023
|
+
}
|
|
1024
|
+
var id;
|
|
1025
|
+
try { id = asn1.read.oid(st.children[0]); } catch (e) { throw ns.E(CS, "a QCStatement statementId must be an OBJECT IDENTIFIER", e); }
|
|
1026
|
+
var infoNode = st.children.length === 2 ? st.children[1] : null;
|
|
1027
|
+
var decoder = qcInfoByOid[id];
|
|
1028
|
+
var info = decoder ? decoder(infoNode, CS) : { opaque: true, bytes: infoNode ? infoNode.bytes : null };
|
|
1029
|
+
out.push({ statementId: id, name: ns.oid.name(id) || null, info: info });
|
|
1030
|
+
}
|
|
1031
|
+
return out;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// [MS-WCCE] szOID-CERTIFICATE_TEMPLATE -- CertificateTemplateOID ::= SEQUENCE {
|
|
1035
|
+
// templateID OID, templateMajorVersion INTEGER OPTIONAL, templateMinorVersion INTEGER OPTIONAL }.
|
|
1036
|
+
// The two versions are DWORDs (0..2^32-1), NOT uint31 -- a CA key/template revision with bit 31
|
|
1037
|
+
// set is a legal DWORD, so bound to the whole safe-integer range, never guard.range.uint31.
|
|
1038
|
+
function msCertificateTemplate(buf) {
|
|
1039
|
+
var C = ns.prefix + "/bad-ms-certificate-template";
|
|
1040
|
+
var kids = seqChildren(buf, C, "CertificateTemplate");
|
|
1041
|
+
if (!kids.length || kids.length > 3) throw ns.E(C, "CertificateTemplateOID must be a SEQUENCE of a templateID and up to two version INTEGERs");
|
|
1042
|
+
if (kids[0].tagClass !== "universal" || kids[0].tagNumber !== _T.OBJECT_IDENTIFIER) throw ns.E(C, "CertificateTemplateOID templateID must be an OBJECT IDENTIFIER");
|
|
1043
|
+
var id;
|
|
1044
|
+
try { id = asn1.read.oid(kids[0]); } catch (e) { throw ns.E(C, "CertificateTemplateOID templateID must be an OBJECT IDENTIFIER", e); }
|
|
1045
|
+
function ver(i, what) {
|
|
1046
|
+
if (!kids[i]) return null;
|
|
1047
|
+
if (kids[i].tagClass !== "universal" || kids[i].tagNumber !== _T.INTEGER) throw ns.E(C, what + " must be an INTEGER");
|
|
1048
|
+
return guard.range.int(readInt(kids[i], C, what), 0n, 4294967295n, ns.E, C, what);
|
|
1049
|
+
}
|
|
1050
|
+
return { templateID: id, name: ns.oid.name(id) || null, templateMajorVersion: ver(1, "templateMajorVersion"), templateMinorVersion: ver(2, "templateMinorVersion") };
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// [MS-CRTD] szOID-ENROLL_CERTTYPE_EXTENSION -- the legacy v1 template name as a BARE BMPString.
|
|
1054
|
+
// asn1.read.string accepts any universal string tag, so the explicit BMP_STRING assert is the guard.
|
|
1055
|
+
function msEnrollCertType(buf) {
|
|
1056
|
+
var C = ns.prefix + "/bad-ms-enroll-cert-type";
|
|
1057
|
+
var n = decodeTop(buf, C, "EnrollCertType");
|
|
1058
|
+
if (n.tagClass !== "universal" || n.tagNumber !== _T.BMP_STRING) throw ns.E(C, "the enroll cert-type name must be a BMPString");
|
|
1059
|
+
try { return asn1.read.string(n); } catch (e) { throw ns.E(C, "the enroll cert-type name must be a well-formed BMPString", e); }
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// [MS-WCCE] szOID-CERTSRV_CA_VERSION -- INTEGER (0..2^32-1) = (caKeyIndex << 16) | certIndex.
|
|
1063
|
+
function msCaVersion(buf) {
|
|
1064
|
+
var C = ns.prefix + "/bad-ms-ca-version";
|
|
1065
|
+
var n = decodeTop(buf, C, "CACertVersion");
|
|
1066
|
+
if (n.tagClass !== "universal" || n.tagNumber !== _T.INTEGER) throw ns.E(C, "CACertVersion must be an INTEGER");
|
|
1067
|
+
var v = guard.range.int(readInt(n, C, "CACertVersion"), 0n, 4294967295n, ns.E, C, "CACertVersion (DWORD)");
|
|
1068
|
+
return { caVersion: v, caKeyIndex: v >>> 16, certIndex: v & 0xffff };
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// [MS-WCCE] szOID-CERTSRV_PREVIOUS_CERT_HASH -- the SHA-1 thumbprint of the prior CA certificate.
|
|
1072
|
+
// AD CS stamps the Windows certificate THUMBPRINT here, which Windows computes with SHA-1 regardless
|
|
1073
|
+
// of the certificate's signature algorithm, so the OCTET STRING is exactly 20 octets. An empty or
|
|
1074
|
+
// other-length value is malformed renewal metadata and fails closed.
|
|
1075
|
+
function msPreviousCertHash(buf) {
|
|
1076
|
+
var C = ns.prefix + "/bad-ms-previous-cert-hash";
|
|
1077
|
+
var n = decodeTop(buf, C, "CAPrevCertHash");
|
|
1078
|
+
var hash;
|
|
1079
|
+
try { hash = Buffer.concat([asn1.read.octetString(n)]); }
|
|
1080
|
+
catch (e) { throw ns.E(C, "CAPrevCertHash must be an OCTET STRING", e); }
|
|
1081
|
+
if (hash.length !== 20) throw ns.E(C, "CAPrevCertHash must be a 20-octet SHA-1 certificate thumbprint");
|
|
1082
|
+
return hash;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
919
1085
|
var byOid = {};
|
|
1086
|
+
byOid[O("qcStatements")] = qcStatements;
|
|
920
1087
|
byOid[O("basicConstraints")] = basicConstraints;
|
|
921
1088
|
byOid[O("keyUsage")] = keyUsage;
|
|
922
1089
|
byOid[O("nameConstraints")] = nameConstraints;
|
|
@@ -933,6 +1100,11 @@ function certExtensionDecoders(ns) {
|
|
|
933
1100
|
byOid[O("precertificatePoison")] = precertPoison;
|
|
934
1101
|
byOid[O("cRLDistributionPoints")] = crlDistributionPoints;
|
|
935
1102
|
byOid[O("freshestCRL")] = crlDistributionPoints;
|
|
1103
|
+
byOid[O("msCertificateTemplate")] = msCertificateTemplate;
|
|
1104
|
+
byOid[O("msEnrollCertType")] = msEnrollCertType;
|
|
1105
|
+
byOid[O("msCaVersion")] = msCaVersion;
|
|
1106
|
+
byOid[O("msPreviousCertHash")] = msPreviousCertHash;
|
|
1107
|
+
byOid[O("msApplicationPolicies")] = certificatePolicies; // [MS-WCCE] = RFC 5280 certificatePolicies, reused not re-implemented
|
|
936
1108
|
return { byOid: byOid };
|
|
937
1109
|
}
|
|
938
1110
|
|
package/lib/sigstore.js
CHANGED
|
@@ -33,6 +33,7 @@ var x509 = require("./schema-x509");
|
|
|
33
33
|
var pathValidate = require("./path-validate");
|
|
34
34
|
var merkle = require("./merkle");
|
|
35
35
|
var oid = require("./oid");
|
|
36
|
+
var edwardsPoint = require("./edwards-point");
|
|
36
37
|
|
|
37
38
|
var C = constants;
|
|
38
39
|
var SigstoreError = frameworkError.SigstoreError;
|
|
@@ -184,8 +185,18 @@ function _rawVerify(keyObj, data, derSig) {
|
|
|
184
185
|
return nodeCrypto.verify(hash, data, { key: keyObj, dsaEncoding: "der" }, derSig);
|
|
185
186
|
}
|
|
186
187
|
function _pubFromSpki(spkiDer, label) {
|
|
187
|
-
|
|
188
|
+
var key;
|
|
189
|
+
try { key = nodeCrypto.createPublicKey({ key: spkiDer, format: "der", type: "spki" }); }
|
|
188
190
|
catch (e) { throw _err("sigstore/bad-key", "invalid " + label + " public key", e); }
|
|
191
|
+
// node imports a low-order (e.g. all-zeroes) OKP public key without complaint, and such a key
|
|
192
|
+
// verifies a FORGED EdDSA signature -- route an Ed25519/Ed448 SPKI through the shared full-order,
|
|
193
|
+
// on-curve Edwards-point gate (as the webauthn and path-validation EdDSA paths already do) before it
|
|
194
|
+
// can be handed to verify.
|
|
195
|
+
var t = key.asymmetricKeyType;
|
|
196
|
+
if (t === "ed25519" || t === "ed448") {
|
|
197
|
+
edwardsPoint.validateSpki(spkiDer, t === "ed25519" ? 6 : 7, SigstoreError, "sigstore/bad-key");
|
|
198
|
+
}
|
|
199
|
+
return key;
|
|
189
200
|
}
|
|
190
201
|
// Parse a certificate, re-typing a malformed-DER fault to a sigstore/* error so
|
|
191
202
|
// the caller never sees a raw certificate/* / asn1/* leak from this boundary.
|
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:c1caa29b-a81a-443d-9296-76988e159993",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-16T23:57:14.646Z",
|
|
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.2.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.2.32",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.2.
|
|
25
|
+
"version": "0.2.32",
|
|
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.2.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.2.32",
|
|
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.2.
|
|
57
|
+
"ref": "@blamejs/pki@0.2.32",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|