@blamejs/pki 0.2.3 → 0.2.5
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 +22 -0
- package/README.md +5 -2
- package/index.js +15 -0
- package/lib/byte-reader.js +68 -0
- package/lib/constants.js +96 -0
- package/lib/ct.js +6 -38
- package/lib/framework-error.js +12 -0
- package/lib/guard-all.js +2 -0
- package/lib/guard-name.js +50 -1
- package/lib/inspect.js +485 -0
- package/lib/oid.js +24 -0
- package/lib/schema-attrcert.js +3 -1
- package/lib/schema-ocsp.js +2 -1
- package/lib/schema-pkix.js +10 -25
- package/lib/webauthn.js +896 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ 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.5 — 2026-07-12
|
|
8
|
+
|
|
9
|
+
WebAuthn / passkey attestation verification joins the toolkit as pki.webauthn.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.webauthn.parseAttestationObject(bytes) structurally decodes a WebAuthn attestation object (the CBOR { fmt, attStmt, authData }) and its authenticatorData over the strict pki.cbor codec, returning the format id, the decoded rpIdHash / flags / signCount, the attested credential data (aaguid, credentialId, and the decoded COSE credentialPublicKey), and the raw authenticatorData bytes a signature covers. Malformed input throws webauthn/bad-attestation-object. W3C WebAuthn Level 3.
|
|
14
|
+
- pki.webauthn.verify(attestationObject, clientDataHash, opts) verifies a WebAuthn attestation statement -- packed, tpm, android-key, apple, fido-u2f, or none -- checking the attestation signature over authenticatorData || clientDataHash and each format's structural bindings (the x5c leaf key, the apple nonce, the tpm certInfo Name / extraData over the pubArea, the android KeyDescription, the fido-u2f verificationData), binding each attestation certificate key to the credential public key, and enforcing each format's certificate requirements (a packed leaf's Authenticator Attestation subject and non-CA basic constraints, a tpm AIK's empty subject and non-CA constraints). It resolves the attestation type and trust path or throws a typed webauthn/* error; a signature that does not verify is a webauthn/verify-failed verdict, never a silent pass. The error taxonomy gains WebauthnError (webauthn/*). W3C WebAuthn Level 3, RFC 9052.
|
|
15
|
+
- The OID registry gains the FIDO id-fido-gen-ce-aaguid, Android key-attestation, Apple anonymous-attestation, and TCG TPM (AIK key purpose, tpmManufacturer / tpmModel / tpmVersion) arcs that WebAuthn attestation certificates carry.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- The bounded big-endian byte cursor under pki.ct's TLS-vector decoding is extracted to a shared engine primitive so the packed big-endian TPM structures in pki.webauthn read through the same bounds-before-slice cursor -- one definition of the length-checked read, carrying each caller's typed error domain.
|
|
20
|
+
|
|
21
|
+
## v0.2.4 — 2026-07-12
|
|
22
|
+
|
|
23
|
+
Human-readable certificate inspection joins the toolkit as pki.inspect.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- pki.inspect.certificate(input) renders a certificate (a PEM string, a DER Buffer, or a pki.schema.x509.parse result) as a human-readable OpenSSL-x509-text-style report, composed over the strict X.509 parser, the shared RFC 5280 extension decoders, and the two-way OID registry. Standard extensions (basic constraints, key usage, extended key usage with purpose names, subject/issuer alternative names, subject and authority key identifiers, and more) decode to their content; an extension with no decoder is named from the registry and shown as its string or a hex dump. A value that is not a certificate, DER, or PEM throws inspect/bad-input; malformed certificate bytes throw inspect/bad-certificate; a malformed single extension never sinks the report. The error taxonomy gains InspectError (inspect/*). RFC 5280.
|
|
28
|
+
|
|
7
29
|
## v0.2.3 — 2026-07-12
|
|
8
30
|
|
|
9
31
|
Offline Sigstore bundle verification joins the toolkit as pki.sigstore.
|
package/README.md
CHANGED
|
@@ -227,8 +227,10 @@ is callable today; nothing below is a stub.
|
|
|
227
227
|
| `pki.shbs` | Stateful hash-based signature **verification** — HSS/LMS (RFC 8554), carried in X.509 by RFC 9802 and CMS by RFC 9708, profiled by NIST SP 800-208 (CNSA 2.0 firmware signing). `verify` checks an HSS signature (every level must pass) and `verifyLms` a single-tree LMS, over the raw public-key / signature blobs the parsers already surface. Pure public-input SHA-256 / SHAKE256 hashing, a data-driven typecode registry, bounds-before-slice reads; a malformed blob throws a typed `ShbsError`, a well-formed-but-wrong signature returns `false`. **Verify only by design** — stateful signing needs atomic one-time-key state that belongs in an HSM — `verify`, `verifyLms` |
|
|
228
228
|
| `pki.hpke` | Hybrid Public Key Encryption (RFC 9180) — the encrypt-to-a-public-key primitive behind TLS ECH, MLS, and OHTTP. `setupS`/`setupR` establish a sender/recipient context (KEM encapsulation + HKDF key schedule); the context's `seal`/`open` AEAD-encrypt with a sequence-counter nonce and `export` derives further secrets; `seal`/`open` are single-shot wrappers. DHKEM (P-256, P-521, X25519, X448) × HKDF-SHA256/SHA512 × AES-GCM/ChaCha20Poly1305/export-only × all four modes, proven against the RFC 9180 Appendix A vectors. DHKEM(P-384) and HKDF-SHA384 are RFC-registered but Appendix A ships no vector for them, so they fail closed until an authoritative KAT exists. Pure composition over `node:crypto`; ML-KEM / X-Wing are a registry data-row extension pending stable drafts — `suites`, `setupS`, `setupR`, `seal`, `open` |
|
|
229
229
|
| `pki.sigstore` | Offline verifier for a Sigstore bundle — the exact artifact `npm publish --provenance` produces and the registry serves. `verifyBundle` composes five fail-closed legs against caller-pinned trust (Fulcio CA roots + Rekor log keys, never trusted from the bundle): the DSSE signature over its PAE preimage under the Fulcio leaf key; the ephemeral Fulcio certificate chain, validated as of the Rekor log time; the RFC 9162 inclusion proof folded to a Rekor-signed tree root; the log entry bound to this exact signature; and the in-toto SLSA subject digest the caller confirms against the published artifact. Zero runtime deps — reuses the X.509 parser, RFC 5280 path validator, and Merkle verifier; the net-new codecs are the DSSE PAE byte-builder and a fail-closed JSON reader. `pae`, `parseBundle`, `verifyBundle` |
|
|
230
|
+
| `pki.inspect` | Human-readable certificate inspection — the pure-JS equivalent of `openssl x509 -text`. `certificate(pem | der | parsed)` renders a familiar OpenSSL-style report: version, serial, signature algorithm, issuer/subject distinguished names, validity, public-key details (curve or modulus size + the raw point/modulus), every decoded extension with its critical flag, and the signature. Built over the strict parser and the two-way OID registry, so it names extension and algorithm OIDs an OpenSSL build shows only as raw bytes. No OpenSSL dependency; the format is stable and OpenSSL-familiar rather than pinned to one OpenSSL version; a malformed extension falls back to a hex dump rather than throwing — `certificate` |
|
|
231
|
+
| `pki.webauthn` | WebAuthn / passkey attestation verification — offline trust evaluation of a W3C WebAuthn (Level 3) attestation. `parseAttestationObject(bytes)` decodes the CBOR attestation object + authenticatorData + COSE credential key over the strict `pki.cbor` codec; `verify(attestationObject, clientDataHash, opts)` checks the attestation-statement signature and each format's structural bindings for **packed / tpm / android-key / apple / fido-u2f / none** — the x5c leaf key, the apple nonce, the tpm `certInfo` Name/`extraData` over the `pubArea`, the android `KeyDescription`, the fido-u2f `verificationData` — binding the credential public key to each attestation (via the signed authenticatorData for packed/fido-u2f, or a cert/`pubArea`-key equality check for android-key/apple/tpm) and enforcing each leaf's certificate requirements. A verifier, not a ceremony client; fail-closed with typed `webauthn/*` errors. Chaining the returned trust path to a pinned root (and aaguid→root via FIDO MDS) is the caller's step through `pki.path.validate` — `parseAttestationObject`, `verify` |
|
|
230
232
|
| `pki.C` / `pki.constants` | Version-stable constants — functional scale helpers (`C.TIME.*`, `C.BYTES.*`), codec `LIMITS`, `version` |
|
|
231
|
-
| `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError` / `Pkcs8Error` / `CmsError` / `OcspError` / `TspError` / `AttrCertError` / `CrmfError` / `Pkcs12Error` / `CmpError` / `PathError` / `CtError` / `JoseError` / `AcmeError`, each carrying a stable `code` in `domain/reason` form |
|
|
233
|
+
| `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError` / `Pkcs8Error` / `CmsError` / `OcspError` / `TspError` / `AttrCertError` / `CrmfError` / `Pkcs12Error` / `CmpError` / `PathError` / `CtError` / `JoseError` / `AcmeError` / `WebauthnError`, each carrying a stable `code` in `domain/reason` form |
|
|
232
234
|
| `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>` |
|
|
233
235
|
|
|
234
236
|
### CLI
|
|
@@ -323,7 +325,8 @@ modules reached by explicit call rather than DER routing: `pki.path` (RFC 5280
|
|
|
323
325
|
path validation), `pki.trust` (trust anchors), `pki.ct` (Certificate Transparency
|
|
324
326
|
SCTs), `pki.hpke` (RFC 9180), `pki.shbs` (HSS/LMS stateful hash signatures),
|
|
325
327
|
`pki.merkle` (RFC 9162 transparency proofs), `pki.sigstore` (offline npm-provenance
|
|
326
|
-
verification),
|
|
328
|
+
verification), `pki.webauthn` (WebAuthn / passkey attestation verification), and the
|
|
329
|
+
`jose` / `acme` / `est` enrollment surfaces. Each composes
|
|
327
330
|
the shared structure, foundation, and crypto layers directly. Alongside the schema
|
|
328
331
|
engine, the fail-closed **guard family** (`guard-*`) centralizes each CVE-class
|
|
329
332
|
defense — detached-buffer re-view, resource caps, constant-time compares,
|
package/index.js
CHANGED
|
@@ -44,6 +44,8 @@ var est = require("./lib/est");
|
|
|
44
44
|
var jose = require("./lib/jose");
|
|
45
45
|
var acme = require("./lib/acme");
|
|
46
46
|
var trust = require("./lib/trust");
|
|
47
|
+
var inspect = require("./lib/inspect");
|
|
48
|
+
var webauthn = require("./lib/webauthn");
|
|
47
49
|
|
|
48
50
|
module.exports = {
|
|
49
51
|
version: constants.version,
|
|
@@ -110,6 +112,19 @@ module.exports = {
|
|
|
110
112
|
// root list omits); pki.trust.anchor hands one to pki.path.validate. Offline:
|
|
111
113
|
// the operator supplies the text; no fetch.
|
|
112
114
|
trust: trust,
|
|
115
|
+
// `inspect` is human-readable inspection -- pki.inspect.certificate(pem|der|parsed)
|
|
116
|
+
// renders an OpenSSL-familiar `x509 -text`-style report from the strict parser +
|
|
117
|
+
// OID registry, naming extension/algorithm OIDs OpenSSL shows only as raw bytes.
|
|
118
|
+
// Pure, no OpenSSL dependency; best-effort (a bad extension falls back to hex).
|
|
119
|
+
inspect: inspect,
|
|
120
|
+
// `webauthn` verifies a W3C WebAuthn / passkey attestation -- pki.webauthn.verify
|
|
121
|
+
// checks the attestation-statement signature + each format's structural bindings
|
|
122
|
+
// (packed / tpm / android-key / apple / fido-u2f / none) and surfaces the x5c chain
|
|
123
|
+
// for the caller to anchor to a pinned root via pki.path.validate; it does not
|
|
124
|
+
// itself chain to a trust anchor. parseAttestationObject structurally decodes the
|
|
125
|
+
// attestation object + authenticatorData + COSE key over the strict pki.cbor codec.
|
|
126
|
+
// A verifier, not a ceremony client; fail-closed.
|
|
127
|
+
webauthn: webauthn,
|
|
113
128
|
// A ready W3C Crypto instance (globalThis.crypto shape) with the classes for
|
|
114
129
|
// constructing more attached under the same namespace (pki.webcrypto.CryptoKey,
|
|
115
130
|
// .SubtleCrypto, .Crypto, .WebCryptoError). PQC-first, classical-capable, zero-dep.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @internal
|
|
6
|
+
* lib/byte-reader.js -- a bounded big-endian byte cursor: the shared ENCODING-layer
|
|
7
|
+
* primitive under the toolkit's non-ASN.1 length-prefixed wire formats (TLS-vector
|
|
8
|
+
* SCTs in pki.ct, the packed big-endian TPM2B_* / authenticatorData structures in
|
|
9
|
+
* pki.webauthn). Every read is length-checked against a hard [pos, end) window
|
|
10
|
+
* BEFORE a byte is taken, so a lying inner length can overrun only the current
|
|
11
|
+
* sub-reader's `end`, never the parent buffer -- bounds-before-slice is structural,
|
|
12
|
+
* not a per-call discipline. It carries the CALLER's typed ErrorClass so every wire
|
|
13
|
+
* format keeps its own `domain/reason` fault code, exactly as the guard family does.
|
|
14
|
+
*
|
|
15
|
+
* This is an engine primitive, not a format: a new fixed-width or length-prefixed
|
|
16
|
+
* wire field is a method here (with its own bounds check), never a hand-rolled
|
|
17
|
+
* offset walk in a format module.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// A bounded cursor over `buf[start, end)`, faulting through the caller's ErrorClass
|
|
21
|
+
// `E` (constructed `new E(code, message)`). `defaultCode` is the fault code used
|
|
22
|
+
// when a read is not given its own.
|
|
23
|
+
function ByteReader(buf, start, end, E, defaultCode) {
|
|
24
|
+
if (!Buffer.isBuffer(buf)) throw new TypeError("ByteReader: buf must be a Buffer");
|
|
25
|
+
this.buf = buf;
|
|
26
|
+
this.pos = start | 0;
|
|
27
|
+
this.end = end == null ? buf.length : (end | 0);
|
|
28
|
+
this.E = E;
|
|
29
|
+
this.defaultCode = defaultCode || "byte-reader/truncated";
|
|
30
|
+
}
|
|
31
|
+
ByteReader.prototype._need = function (n, code) {
|
|
32
|
+
if (this.pos + n > this.end) {
|
|
33
|
+
throw new this.E(code || this.defaultCode, "need " + n + " byte(s), only " + (this.end - this.pos) + " remain");
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
ByteReader.prototype.u8 = function (code) { this._need(1, code); return this.buf[this.pos++]; };
|
|
37
|
+
ByteReader.prototype.u16 = function (code) { this._need(2, code); var v = this.buf.readUInt16BE(this.pos); this.pos += 2; return v; };
|
|
38
|
+
ByteReader.prototype.u24 = function (code) {
|
|
39
|
+
this._need(3, code);
|
|
40
|
+
var v = (this.buf[this.pos] << 16) | (this.buf[this.pos + 1] << 8) | this.buf[this.pos + 2];
|
|
41
|
+
this.pos += 3; return v >>> 0;
|
|
42
|
+
};
|
|
43
|
+
ByteReader.prototype.u32 = function (code) { this._need(4, code); var v = this.buf.readUInt32BE(this.pos); this.pos += 4; return v; };
|
|
44
|
+
ByteReader.prototype.u64 = function (code) { this._need(8, code); var v = this.buf.readBigUInt64BE(this.pos); this.pos += 8; return v; };
|
|
45
|
+
// A fixed-width opaque slice (zero-copy view into the backing buffer).
|
|
46
|
+
ByteReader.prototype.fixed = function (n, code) {
|
|
47
|
+
this._need(n, code);
|
|
48
|
+
var s = this.buf.subarray(this.pos, this.pos + n); this.pos += n; return s;
|
|
49
|
+
};
|
|
50
|
+
// opaque<min..max> -- a length-prefixed vector (lenWidth-byte big-endian prefix:
|
|
51
|
+
// 2 or 3). The length prefix itself is a plain read (a truncation there is the
|
|
52
|
+
// default code); only a LYING length whose body overruns the bound carries `code`.
|
|
53
|
+
ByteReader.prototype.vector = function (lenWidth, min, max, code) {
|
|
54
|
+
var len = lenWidth === 3 ? this.u24() : this.u16();
|
|
55
|
+
if (len < min) throw new this.E(code, "vector length " + len + " below minimum " + min);
|
|
56
|
+
if (max != null && len > max) throw new this.E(code, "vector length " + len + " above maximum " + max);
|
|
57
|
+
this._need(len, code);
|
|
58
|
+
var s = this.buf.subarray(this.pos, this.pos + len); this.pos += len; return s;
|
|
59
|
+
};
|
|
60
|
+
// A bounded child cursor over the next `len` bytes, sharing the same ErrorClass.
|
|
61
|
+
ByteReader.prototype.subReader = function (len, code) {
|
|
62
|
+
this._need(len, code);
|
|
63
|
+
var r = new ByteReader(this.buf, this.pos, this.pos + len, this.E, this.defaultCode); this.pos += len; return r;
|
|
64
|
+
};
|
|
65
|
+
ByteReader.prototype.remaining = function () { return this.end - this.pos; };
|
|
66
|
+
ByteReader.prototype.atEnd = function () { return this.pos === this.end; };
|
|
67
|
+
|
|
68
|
+
module.exports = ByteReader;
|
package/lib/constants.js
CHANGED
|
@@ -257,6 +257,101 @@ var LIMITS = {
|
|
|
257
257
|
TRUST_MAX_CSV_FIELD_BYTES: BYTES.kib(64),
|
|
258
258
|
};
|
|
259
259
|
|
|
260
|
+
// NAMES -- display-naming conventions shared by the human-readable renderers
|
|
261
|
+
// (pki.inspect today; a future lint / CLI report tomorrow). Centralized so every
|
|
262
|
+
// consumer speaks ONE convention -- the OpenSSL-familiar labels an operator expects
|
|
263
|
+
// -- rather than each renderer copying a table that silently drifts. These are
|
|
264
|
+
// DISPLAY strings, not wire identifiers (those resolve through pki.oid): a value here
|
|
265
|
+
// never affects parse or verify, only how a decoded field is titled in a report.
|
|
266
|
+
var NAMES = {
|
|
267
|
+
// ==== Distinguished-name attribute short names (RFC 4514 sec. 3 + RFC 4519 / CABF) ==
|
|
268
|
+
// The single source of truth: this table was previously copied in both
|
|
269
|
+
// lib/inspect.js and lib/schema-pkix.js and had already drifted (STREET vs street,
|
|
270
|
+
// SERIALNUMBER vs serialNumber). An entry whose type the OID registry does not yet
|
|
271
|
+
// resolve is simply never hit -- forward entries (jurisdiction*, organizationIdentifier)
|
|
272
|
+
// are harmless until their arc is registered.
|
|
273
|
+
DN_SHORT: {
|
|
274
|
+
commonName: "CN", surname: "SN", serialNumber: "SERIALNUMBER", countryName: "C",
|
|
275
|
+
localityName: "L", stateOrProvinceName: "ST", streetAddress: "STREET",
|
|
276
|
+
organizationName: "O", organizationalUnitName: "OU", title: "title", givenName: "GN",
|
|
277
|
+
initials: "initials", generationQualifier: "generationQualifier", distinguishedNameQualifier: "dnQualifier",
|
|
278
|
+
pseudonym: "pseudonym", domainComponent: "DC", userId: "UID", emailAddress: "emailAddress",
|
|
279
|
+
businessCategory: "businessCategory", postalCode: "postalCode", name: "name",
|
|
280
|
+
jurisdictionCountryName: "jurisdictionC", jurisdictionStateOrProvinceName: "jurisdictionST",
|
|
281
|
+
jurisdictionLocalityName: "jurisdictionL", organizationIdentifier: "organizationIdentifier",
|
|
282
|
+
},
|
|
283
|
+
// ==== X.509v3 extension section titles (OpenSSL-familiar); an extension absent
|
|
284
|
+
// here is titled from the OID registry name / dotted OID ====
|
|
285
|
+
EXTENSION: {
|
|
286
|
+
basicConstraints: "X509v3 Basic Constraints", keyUsage: "X509v3 Key Usage",
|
|
287
|
+
extKeyUsage: "X509v3 Extended Key Usage", subjectAltName: "X509v3 Subject Alternative Name",
|
|
288
|
+
issuerAltName: "X509v3 Issuer Alternative Name", subjectKeyIdentifier: "X509v3 Subject Key Identifier",
|
|
289
|
+
authorityKeyIdentifier: "X509v3 Authority Key Identifier", certificatePolicies: "X509v3 Certificate Policies",
|
|
290
|
+
policyMappings: "X509v3 Policy Mappings", policyConstraints: "X509v3 Policy Constraints",
|
|
291
|
+
inhibitAnyPolicy: "X509v3 Inhibit Any Policy", nameConstraints: "X509v3 Name Constraints",
|
|
292
|
+
cRLDistributionPoints: "X509v3 CRL Distribution Points", freshestCRL: "X509v3 Freshest CRL",
|
|
293
|
+
subjectInfoAccess: "Subject Information Access", authorityInfoAccess: "Authority Information Access",
|
|
294
|
+
cRLNumber: "X509v3 CRL Number", deltaCRLIndicator: "X509v3 Delta CRL Indicator",
|
|
295
|
+
issuingDistributionPoint: "X509v3 Issuing Distribution Point", cRLReason: "X509v3 CRL Reason Code",
|
|
296
|
+
invalidityDate: "Invalidity Date", certificateIssuer: "X509v3 Certificate Issuer",
|
|
297
|
+
signedCertificateTimestampList: "CT Precertificate SCTs", precertificatePoison: "CT Precertificate Poison",
|
|
298
|
+
},
|
|
299
|
+
// ==== KeyUsage bits (RFC 5280 sec. 4.2.1.3, bit order) ====
|
|
300
|
+
KEY_USAGE: {
|
|
301
|
+
digitalSignature: "Digital Signature", nonRepudiation: "Non Repudiation", keyEncipherment: "Key Encipherment",
|
|
302
|
+
dataEncipherment: "Data Encipherment", keyAgreement: "Key Agreement", keyCertSign: "Certificate Sign",
|
|
303
|
+
cRLSign: "CRL Sign", encipherOnly: "Encipher Only", decipherOnly: "Decipher Only",
|
|
304
|
+
},
|
|
305
|
+
// ==== ExtendedKeyUsage purposes (RFC 5280 sec. 4.2.1.12 + common); an unlisted
|
|
306
|
+
// purpose shows its registry name / OID ====
|
|
307
|
+
EXT_KEY_USAGE: {
|
|
308
|
+
anyExtendedKeyUsage: "Any Extended Key Usage", serverAuth: "TLS Web Server Authentication",
|
|
309
|
+
clientAuth: "TLS Web Client Authentication", codeSigning: "Code Signing",
|
|
310
|
+
emailProtection: "E-mail Protection", timeStamping: "Time Stamping", ocspSigning: "OCSP Signing",
|
|
311
|
+
},
|
|
312
|
+
// ==== GeneralName CHOICE labels, by context tag number (RFC 5280 sec. 4.2.1.6) ====
|
|
313
|
+
GENERAL_NAME: { 0: "othername", 1: "email", 2: "DNS", 3: "X400Name", 4: "DirName", 5: "EdiPartyName", 6: "URI", 7: "IP Address", 8: "Registered ID" },
|
|
314
|
+
// ==== Named-curve -> NIST alias, for the EC key line ====
|
|
315
|
+
NIST_CURVE: { prime256v1: "P-256", secp384r1: "P-384", secp521r1: "P-521" },
|
|
316
|
+
// ==== ReasonFlags BIT STRING bit -> name (RFC 5280 sec. 4.2.1.13, a CRL
|
|
317
|
+
// distribution point's revocation scope; bit 0 is "unused"). Distinct from the
|
|
318
|
+
// CRLReason ENUMERATED below -- a BIT STRING of scopes, not a single code. ====
|
|
319
|
+
REASON_FLAGS: {
|
|
320
|
+
1: "Key Compromise", 2: "CA Compromise", 3: "Affiliation Changed", 4: "Superseded",
|
|
321
|
+
5: "Cessation Of Operation", 6: "Certificate Hold", 7: "Privilege Withdrawn", 8: "AA Compromise",
|
|
322
|
+
},
|
|
323
|
+
// ==== CRLReason ENUMERATED value names (RFC 5280 sec. 5.3.1; value 7 is unused) ====
|
|
324
|
+
CRL_REASON: {
|
|
325
|
+
"0": "unspecified", "1": "keyCompromise", "2": "cACompromise", "3": "affiliationChanged",
|
|
326
|
+
"4": "superseded", "5": "cessationOfOperation", "6": "certificateHold",
|
|
327
|
+
"8": "removeFromCRL", "9": "privilegeWithdrawn", "10": "aACompromise",
|
|
328
|
+
},
|
|
329
|
+
// ==== OCSPResponseStatus value names (RFC 6960 sec. 4.2.1; value 4 is unassigned) ====
|
|
330
|
+
OCSP_STATUS: { "0": "successful", "1": "malformedRequest", "2": "internalError", "3": "tryLater", "5": "sigRequired", "6": "unauthorized" },
|
|
331
|
+
// ==== Attribute-cert objectDigestInfo digested-object types (RFC 5755 sec. 4.4.2) ====
|
|
332
|
+
OBJECT_DIGEST_TYPE: { "0": "publicKey", "1": "publicKeyCert", "2": "otherObjectTypes" },
|
|
333
|
+
// ==== Public-key / signature algorithm display names, classical + PQC. Forward-
|
|
334
|
+
// looking: the toolkit is PQC-first, so the FIPS 203/204/205 names render as-is;
|
|
335
|
+
// a consumer that finds no entry shows the OID registry name / dotted OID ====
|
|
336
|
+
ALGORITHM: {
|
|
337
|
+
rsaEncryption: "rsaEncryption", rsassaPss: "rsassaPss", rsaesOaep: "rsaesOaep", ecPublicKey: "id-ecPublicKey",
|
|
338
|
+
ed25519: "ED25519", ed448: "ED448", x25519: "X25519", x448: "X448",
|
|
339
|
+
"ML-DSA-44": "ML-DSA-44", "ML-DSA-65": "ML-DSA-65", "ML-DSA-87": "ML-DSA-87",
|
|
340
|
+
"ML-KEM-512": "ML-KEM-512", "ML-KEM-768": "ML-KEM-768", "ML-KEM-1024": "ML-KEM-1024",
|
|
341
|
+
"SLH-DSA-SHA2-128s": "SLH-DSA-SHA2-128s", "SLH-DSA-SHA2-128f": "SLH-DSA-SHA2-128f",
|
|
342
|
+
"SLH-DSA-SHA2-192s": "SLH-DSA-SHA2-192s", "SLH-DSA-SHA2-256s": "SLH-DSA-SHA2-256s",
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
// NAMES is BOTH a display table (pki.inspect) and the strict parsers' legal-value
|
|
347
|
+
// allow-list: the CRL reasonCode, OCSP response-status, and attribute-cert object-
|
|
348
|
+
// digest decoders check membership against CRL_REASON / OCSP_STATUS / OBJECT_DIGEST_TYPE
|
|
349
|
+
// to fail closed on an RFC-reserved / undefined value. Deep-freeze it so a caller can
|
|
350
|
+
// NEVER widen what those decoders accept by mutating the "display-only" table (e.g.
|
|
351
|
+
// adding CRLReason 7, RFC-reserved, which would otherwise make the decoders fail open).
|
|
352
|
+
Object.keys(NAMES).forEach(function (k) { Object.freeze(NAMES[k]); });
|
|
353
|
+
Object.freeze(NAMES);
|
|
354
|
+
|
|
260
355
|
// Single-sourced from the package manifest so the reported version can
|
|
261
356
|
// never drift from the published package (package.json is always present
|
|
262
357
|
// in the installed tarball). A hand-maintained literal here silently
|
|
@@ -267,5 +362,6 @@ module.exports = {
|
|
|
267
362
|
TIME: TIME,
|
|
268
363
|
BYTES: BYTES,
|
|
269
364
|
LIMITS: LIMITS,
|
|
365
|
+
NAMES: NAMES,
|
|
270
366
|
version: VERSION,
|
|
271
367
|
};
|
package/lib/ct.js
CHANGED
|
@@ -42,6 +42,7 @@ var asn1 = require("./asn1-der.js");
|
|
|
42
42
|
var constants = require("./constants.js");
|
|
43
43
|
var frameworkError = require("./framework-error.js");
|
|
44
44
|
var guard = require("./guard-all.js");
|
|
45
|
+
var ByteReader = require("./byte-reader.js");
|
|
45
46
|
|
|
46
47
|
var CtError = frameworkError.CtError;
|
|
47
48
|
var C = constants;
|
|
@@ -64,44 +65,11 @@ var SCT_MIN_BODY = 47;
|
|
|
64
65
|
var LOGID_BYTES = 32;
|
|
65
66
|
var MAX_SAFE = 9007199254740991n; // 2^53 - 1; above this a Number loses precision
|
|
66
67
|
|
|
67
|
-
// ---- TlsReader -- the
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
function TlsReader(buf, start, end) {
|
|
72
|
-
TlsReader.prototype._need = function (n, code) {
|
|
73
|
-
if (this.pos + n > this.end) {
|
|
74
|
-
throw new CtError(code || "ct/truncated", "need " + n + " byte(s), only " + (this.end - this.pos) + " remain");
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
TlsReader.prototype.u8 = function (code) { this._need(1, code); return this.buf[this.pos++]; };
|
|
78
|
-
TlsReader.prototype.u16 = function (code) { this._need(2, code); var v = this.buf.readUInt16BE(this.pos); this.pos += 2; return v; };
|
|
79
|
-
TlsReader.prototype.u24 = function (code) {
|
|
80
|
-
this._need(3, code);
|
|
81
|
-
var v = (this.buf[this.pos] << 16) | (this.buf[this.pos + 1] << 8) | this.buf[this.pos + 2];
|
|
82
|
-
this.pos += 3; return v;
|
|
83
|
-
};
|
|
84
|
-
TlsReader.prototype.u64 = function (code) { this._need(8, code); var v = this.buf.readBigUInt64BE(this.pos); this.pos += 8; return v; };
|
|
85
|
-
TlsReader.prototype.fixed = function (n, code) {
|
|
86
|
-
this._need(n, code);
|
|
87
|
-
var s = this.buf.subarray(this.pos, this.pos + n); this.pos += n; return s;
|
|
88
|
-
};
|
|
89
|
-
// opaque<min..max> -- a length-prefixed vector (lenWidth-byte big-endian prefix).
|
|
90
|
-
// The length prefix itself is a plain read (a truncation there is ct/truncated);
|
|
91
|
-
// only a LYING length whose body overruns the bound carries the field's own code.
|
|
92
|
-
TlsReader.prototype.vector = function (lenWidth, min, max, code) {
|
|
93
|
-
var len = lenWidth === 3 ? this.u24() : this.u16();
|
|
94
|
-
if (len < min) throw new CtError(code, "vector length " + len + " below minimum " + min);
|
|
95
|
-
if (max != null && len > max) throw new CtError(code, "vector length " + len + " above maximum " + max);
|
|
96
|
-
this._need(len, code);
|
|
97
|
-
var s = this.buf.subarray(this.pos, this.pos + len); this.pos += len; return s;
|
|
98
|
-
};
|
|
99
|
-
TlsReader.prototype.subReader = function (len, code) {
|
|
100
|
-
this._need(len, code);
|
|
101
|
-
var r = new TlsReader(this.buf, this.pos, this.pos + len); this.pos += len; return r;
|
|
102
|
-
};
|
|
103
|
-
TlsReader.prototype.remaining = function () { return this.end - this.pos; };
|
|
104
|
-
TlsReader.prototype.atEnd = function () { return this.pos === this.end; };
|
|
68
|
+
// ---- TlsReader -- the RFC 6962 TLS-vector cursor, the shared bounded big-endian
|
|
69
|
+
// ByteReader engine primitive bound to the ct/* fault domain. A lying inner length
|
|
70
|
+
// can overrun only the current sub-reader's `end`, never the parent buffer, so
|
|
71
|
+
// bounds-before-slice is structural (see lib/byte-reader.js).
|
|
72
|
+
function TlsReader(buf, start, end) { return new ByteReader(buf, start, end, CtError, "ct/truncated"); }
|
|
105
73
|
|
|
106
74
|
// Peel the RFC 6962 sec. 3.3 inner DER OCTET STRING (the certificate/OCSP layer
|
|
107
75
|
// already peeled the outer extnValue OCTET STRING). Rides the strict codec, so
|
package/lib/framework-error.js
CHANGED
|
@@ -293,6 +293,16 @@ var HpkeError = defineClass("HpkeError", { withCause: true });
|
|
|
293
293
|
// expected in-toto Statement. Carries the underlying leaf fault (an x509/*,
|
|
294
294
|
// merkle/*, or asn1/* error) as `.cause`.
|
|
295
295
|
var SigstoreError = defineClass("SigstoreError", { withCause: true });
|
|
296
|
+
// InspectError -- a bad input to the human-readable inspector (pki.inspect): a
|
|
297
|
+
// value that is neither a parsed structure, DER Buffer, nor PEM string, or a PEM
|
|
298
|
+
// whose label does not decode. Rendering itself is best-effort and never throws on
|
|
299
|
+
// a malformed field -- an undecodable extension falls back to a hex dump.
|
|
300
|
+
var InspectError = defineClass("InspectError", { withCause: true });
|
|
301
|
+
// WebauthnError -- a malformed or unverifiable WebAuthn attestation (pki.webauthn):
|
|
302
|
+
// a non-well-formed attestation object / COSE key, an authenticatorData that fails
|
|
303
|
+
// its bounded layout, an attestation-statement signature that does not verify, or a
|
|
304
|
+
// trust path that does not reach the caller's anchor. Fail-closed, typed reason.
|
|
305
|
+
var WebauthnError = defineClass("WebauthnError", { withCause: true });
|
|
296
306
|
|
|
297
307
|
module.exports = {
|
|
298
308
|
PkiError: PkiError,
|
|
@@ -319,6 +329,8 @@ module.exports = {
|
|
|
319
329
|
ShbsError: ShbsError,
|
|
320
330
|
HpkeError: HpkeError,
|
|
321
331
|
SigstoreError: SigstoreError,
|
|
332
|
+
InspectError: InspectError,
|
|
333
|
+
WebauthnError: WebauthnError,
|
|
322
334
|
MerkleError: MerkleError,
|
|
323
335
|
SmimeError: SmimeError,
|
|
324
336
|
CsrattrsError: CsrattrsError,
|
package/lib/guard-all.js
CHANGED
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
// to Number (silent-narrowing defence)
|
|
24
24
|
// guard.name.dnEqual / .rdnEqual / .assertNoControlBytes / .assertPrintableIa5
|
|
25
25
|
// -- canonical DN identity + name-string integrity
|
|
26
|
+
// guard.name.escapeControlBytes / .escapeDnValue
|
|
27
|
+
// -- render-side name safety: control-byte + RFC 4514 escaping
|
|
26
28
|
// (CVE-2009-2408 truncation / identity defence)
|
|
27
29
|
// guard.encoding.base64url / .base64 / .hex
|
|
28
30
|
// -- strict textual-encoding decode, canonical
|
package/lib/guard-name.js
CHANGED
|
@@ -108,4 +108,53 @@ function dnEqual(rdnsA, rdnsB, E, code, label) {
|
|
|
108
108
|
return true;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
// escapeControlBytes(str) -> str. The render-side sibling of assertNoControlBytes:
|
|
112
|
+
// where a name string must still be DISPLAYED best-effort (a human-readable report,
|
|
113
|
+
// a log line) rather than rejected, neutralize every C0 control byte and DEL by
|
|
114
|
+
// rendering it as \xHH. A bare CR / LF / NUL in a decoded dNSName or DN value would
|
|
115
|
+
// otherwise forge or overwrite report lines in a terminal or log (CWE-117 output-log
|
|
116
|
+
// injection / CWE-116 improper output encoding). Non-control bytes pass through.
|
|
117
|
+
// Written as a charCodeAt scan, not a /[\x00-\x1f]/ regex: eslint's no-control-regex
|
|
118
|
+
// (correctly) refuses control characters in a regex literal, and the control-byte
|
|
119
|
+
// range test is the rename-proof shape the detector keys on regardless.
|
|
120
|
+
// @enforced-by guard-shape-reinlined
|
|
121
|
+
// @guard-shape < 0x20 \|\| \w+ === 0x7f
|
|
122
|
+
function escapeControlBytes(str) {
|
|
123
|
+
var s = String(str), out = "";
|
|
124
|
+
for (var i = 0; i < s.length; i++) {
|
|
125
|
+
var c = s.charCodeAt(i);
|
|
126
|
+
out += (c < 0x20 || c === 0x7f) ? "\\x" + (c < 16 ? "0" : "") + c.toString(16).toUpperCase() : s.charAt(i);
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// escapeDnValue(v) -> str. RFC 4514 sec. 2.4 string-representation escaping of a DN
|
|
132
|
+
// attribute value (the input is the already-unwrapped semantic string): backslash-
|
|
133
|
+
// escape the always-specials , + " \ < > ; and a NUL (as \00), then the positional
|
|
134
|
+
// rules -- a trailing space as '\ ' and a leading '#' or space as '\#' / '\ '. Without
|
|
135
|
+
// this a value like `foo, CN=admin` renders as two RDNs, and a literal `#05` collides
|
|
136
|
+
// with the hexstring form, so the report misstates a subject/issuer name (CWE-116).
|
|
137
|
+
// The one place a DN attribute value is made display-safe; pki.schema.pkix's DN
|
|
138
|
+
// rendering composes it, and pki.inspect reuses that parser output (name.dn).
|
|
139
|
+
// @enforced-by behavioral -- the RFC 4514 separator class carries a quote inside a
|
|
140
|
+
// regex literal, which the codebase-patterns literal-stripper mis-tokenizes, so no
|
|
141
|
+
// rename-proof shape is detectable; the guard-name RED vectors + the schema-pkix DN
|
|
142
|
+
// round-trip vectors (a comma / plus / leading '#' renders backslash-escaped) are the guard.
|
|
143
|
+
function escapeDnValue(v) {
|
|
144
|
+
var s = String(v).replace(/([,+"\\<>;])/g, "\\$1"), out = "";
|
|
145
|
+
// RFC 4514 sec. 2.4: a NUL / control octet -> '\' + two hex digits, so an embedded
|
|
146
|
+
// CR / LF / NUL in a decoded DN value can never forge a report line when displayed.
|
|
147
|
+
for (var i = 0; i < s.length; i++) {
|
|
148
|
+
var c = s.charCodeAt(i);
|
|
149
|
+
out += (c < 0x20 || c === 0x7f) ? "\\" + (c < 16 ? "0" : "") + c.toString(16).toUpperCase() : s.charAt(i);
|
|
150
|
+
}
|
|
151
|
+
if (out.length && out.charAt(out.length - 1) === " ") out = out.slice(0, -1) + "\\ ";
|
|
152
|
+
if (out.charAt(0) === "#" || out.charAt(0) === " ") out = "\\" + out;
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = {
|
|
157
|
+
assertNoControlBytes: assertNoControlBytes, assertPrintableIa5: assertPrintableIa5,
|
|
158
|
+
dnEqual: dnEqual, rdnEqual: rdnEqual,
|
|
159
|
+
escapeControlBytes: escapeControlBytes, escapeDnValue: escapeDnValue,
|
|
160
|
+
};
|