@blamejs/pki 0.2.4 → 0.2.6

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 CHANGED
@@ -4,6 +4,39 @@ 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.6 — 2026-07-13
8
+
9
+ WebAuthn attestation verification covers Ed448 and the RFC 9864 fully-specified COSE algorithms, and hardens credential-key conformance.
10
+
11
+ ### Added
12
+
13
+ - pki.webauthn now verifies the RFC 9864 fully-specified COSE algorithm identifiers a WebAuthn relying party may receive: ESP256 (-9), ESP384 (-51), ESP512 (-52), Ed25519 (-19), and Ed448 (-53). Ed448 (-53) is the only WebAuthn path to Ed448, so an Ed448 credential now verifies rather than erroring as an unsupported algorithm.
14
+ - The verifier is now checked against the official W3C WebAuthn Level 3 test-vector suite -- every published vector across ES256 / ES384 / ES512 / RS256 / Ed25519 / Ed448 and the packed / self / tpm / apple / fido-u2f / none formats -- as an independent cross-implementation oracle.
15
+
16
+ ### Changed
17
+
18
+ - A WebAuthn credential key with COSE alg -8 (EdDSA) now requires curve Ed25519 (crv 6), matching the WebAuthn algorithm-identifier profile; an -8 key claiming Ed448 (crv 7) is rejected -- Ed448 is carried under the fully-specified identifier -53 instead.
19
+ - An EC2 credential key must use the uncompressed point form; the compressed sign-bit y encoding is rejected for WebAuthn credential keys.
20
+
21
+ ### Fixed
22
+
23
+ - The WebAuthn credential public-key point is now validated on its curve, so an off-curve or identity EC/Edwards point fails closed at decode rather than being carried into a later verify step.
24
+ - An ECDSA attestation signature is now enforced as a minimally-encoded DER ECDSA-Sig-Value (X.690): a non-minimal, negative, zero, or over-size r/s coordinate is rejected as malformed instead of being stripped and accepted.
25
+
26
+ ## v0.2.5 — 2026-07-12
27
+
28
+ WebAuthn / passkey attestation verification joins the toolkit as pki.webauthn.
29
+
30
+ ### Added
31
+
32
+ - 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.
33
+ - 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.
34
+ - 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.
35
+
36
+ ### Changed
37
+
38
+ - 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.
39
+
7
40
  ## v0.2.4 — 2026-07-12
8
41
 
9
42
  Human-readable certificate inspection joins the toolkit as pki.inspect.
package/README.md CHANGED
@@ -228,8 +228,9 @@ is callable today; nothing below is a stub.
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
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. The credential-key check covers the full WebAuthn COSE algorithm set — ES256/384/512, RS256/384/512, PS256, EdDSA (Ed25519), and the RFC 9864 fully-specified identifiers **ESP256/384/512, Ed25519, and Ed448** — validating the public-key point on its curve, rejecting the compressed EC point form, and enforcing a minimally-encoded DER ECDSA signature. 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` |
231
232
  | `pki.C` / `pki.constants` | Version-stable constants — functional scale helpers (`C.TIME.*`, `C.BYTES.*`), codec `LIMITS`, `version` |
232
- | `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 |
233
234
  | `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>` |
234
235
 
235
236
  ### CLI
@@ -324,7 +325,8 @@ modules reached by explicit call rather than DER routing: `pki.path` (RFC 5280
324
325
  path validation), `pki.trust` (trust anchors), `pki.ct` (Certificate Transparency
325
326
  SCTs), `pki.hpke` (RFC 9180), `pki.shbs` (HSS/LMS stateful hash signatures),
326
327
  `pki.merkle` (RFC 9162 transparency proofs), `pki.sigstore` (offline npm-provenance
327
- verification), and the `jose` / `acme` / `est` enrollment surfaces. Each composes
328
+ verification), `pki.webauthn` (WebAuthn / passkey attestation verification), and the
329
+ `jose` / `acme` / `est` enrollment surfaces. Each composes
328
330
  the shared structure, foundation, and crypto layers directly. Alongside the schema
329
331
  engine, the fail-closed **guard family** (`guard-*`) centralizes each CVE-class
330
332
  defense — detached-buffer re-view, resource caps, constant-time compares,
package/index.js CHANGED
@@ -45,6 +45,7 @@ var jose = require("./lib/jose");
45
45
  var acme = require("./lib/acme");
46
46
  var trust = require("./lib/trust");
47
47
  var inspect = require("./lib/inspect");
48
+ var webauthn = require("./lib/webauthn");
48
49
 
49
50
  module.exports = {
50
51
  version: constants.version,
@@ -116,6 +117,14 @@ module.exports = {
116
117
  // OID registry, naming extension/algorithm OIDs OpenSSL shows only as raw bytes.
117
118
  // Pure, no OpenSSL dependency; best-effort (a bad extension falls back to hex).
118
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,
119
128
  // A ready W3C Crypto instance (globalThis.crypto shape) with the classes for
120
129
  // constructing more attached under the same namespace (pki.webcrypto.CryptoKey,
121
130
  // .SubtleCrypto, .Crypto, .WebCryptoError). PQC-first, classical-capable, zero-dep.
package/lib/acme.js CHANGED
@@ -85,7 +85,8 @@ function _isUrl(v) {
85
85
  var u;
86
86
  // A parse failure IS the "not a URL" verdict for this boolean predicate -- there
87
87
  // is no PkiError here to thread a cause into, so the error is intentionally ignored.
88
- try { u = new URL(v); } catch (_e) { return false; }
88
+ try { u = new URL(v); }
89
+ catch (_e) { return false; }
89
90
  return (u.protocol === "http:" || u.protocol === "https:") && u.hostname.length > 0;
90
91
  }
91
92
  // A URI string with any RFC 3986 scheme (mailto:, tel:, http(s):, ...). An account
@@ -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/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 one net-new primitive (an ENCODING layer, not a schema
68
- // combinator): a bounded big-endian TLS-vector cursor with a hard [pos, end).
69
- // A lying inner length can overrun only the current sub-reader's `end`, never
70
- // the parent buffer, so bounds-before-slice is structural.
71
- function TlsReader(buf, start, end) { this.buf = buf; this.pos = start; this.end = 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
@@ -0,0 +1,91 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ //
5
+ // @internal -- no operator-facing namespace. Composed by validator-cose to validate an
6
+ // OKP (Ed25519 / Ed448) credential public-key point.
7
+ //
8
+ // edwards-point -- point validation for the Edwards curves EdDSA uses. node:crypto imports
9
+ // an RFC 8410 OKP SubjectPublicKeyInfo WITHOUT checking the point is a valid, full-order
10
+ // Edwards point (an all-zeroes Ed25519/Ed448 key parses, and an all-zeroes key with an
11
+ // all-zeroes signature even verifies TRUE -- a trivial forgery), so an OKP credential key
12
+ // needs an explicit check. This decodes the compressed point per RFC 8032 sec. 5.1.3
13
+ // (Ed25519) / sec. 5.2.3 (Ed448) -- which fails closed on an off-curve or non-canonical
14
+ // encoding -- and rejects a low-order point via the cofactor check [8]P == identity (an
15
+ // all-zeroes key is a low-order point ON the curve, so the decode alone does not catch it).
16
+ // A conformant authenticator never emits a low-order credential key.
17
+ //
18
+ // Pure BigInt modular arithmetic (constant-time is not a goal here: the input is a public
19
+ // key, and the verdict is a public validity bit, not a secret).
20
+
21
+ // ---- field parameters ------------------------------------------------------
22
+ var P25519 = (1n << 255n) - 19n;
23
+ // d = -121665 / 121666 (mod p); sqrt(-1) = 2^((p-1)/4) (mod p).
24
+ var D25519 = _mul(P25519 - 121665n, _inv(121666n, P25519), P25519);
25
+ var SQRTM1 = _pow(2n, (P25519 - 1n) / 4n, P25519);
26
+ var P448 = (1n << 448n) - (1n << 224n) - 1n;
27
+ var D448 = P448 - 39081n; // d = -39081 (mod p)
28
+
29
+ var CURVE = {
30
+ 6: { p: P25519, d: D25519, a: P25519 - 1n, len: 32, signBit: 255n }, // Ed25519, a = -1
31
+ 7: { p: P448, d: D448, a: 1n, len: 57, signBit: 455n }, // Ed448, a = 1
32
+ };
33
+
34
+ function _mul(x, y, p) { return (x % p) * (y % p) % p; }
35
+ function _pow(b, e, p) { var r = 1n; b %= p; while (e > 0n) { if (e & 1n) r = r * b % p; b = b * b % p; e >>= 1n; } return r; }
36
+ function _inv(a, p) { return _pow(((a % p) + p) % p, p - 2n, p); } // Fermat inverse (p prime)
37
+ // A fixed 32/57-byte little-endian public key -> BigInt in one shot (big-endian hex of the
38
+ // reversed bytes), never a per-byte shift-accumulate (the quadratic-BigInt antipattern).
39
+ function _leToBig(buf) { return BigInt("0x" + Buffer.from(buf).reverse().toString("hex")); }
40
+
41
+ // Decode a compressed Edwards point (RFC 8032) -> { x, y } or null (off-curve / non-canonical).
42
+ function _decode(raw, c) {
43
+ var num = _leToBig(raw);
44
+ var sign = (num >> c.signBit) & 1n;
45
+ var y = num & ((1n << c.signBit) - 1n);
46
+ if (y >= c.p) return null; // non-canonical y
47
+ var p = c.p, y2 = y * y % p, x;
48
+ var u = ((y2 - 1n) % p + p) % p;
49
+ if (c.a === 1n) {
50
+ // Ed448: v = d*y^2 - 1; x = u^3 v (u^5 v^3)^((p-3)/4); require v*x^2 == u.
51
+ var v = ((c.d * y2 % p - 1n) % p + p) % p;
52
+ x = _mul(_mul(_pow(u, 3n, p), v, p), _pow(_mul(_pow(u, 5n, p), _pow(v, 3n, p), p), (p - 3n) / 4n, p), p);
53
+ if (_mul(v, x * x % p, p) !== u) return null;
54
+ } else {
55
+ // Ed25519: v = d*y^2 + 1; x = u v^3 (u v^7)^((p-5)/8); v*x^2 == u, or == -u then x *= sqrt(-1).
56
+ var v25 = (c.d * y2 % p + 1n) % p;
57
+ x = _mul(_mul(u, _pow(v25, 3n, p), p), _pow(_mul(u, _pow(v25, 7n, p), p), (p - 5n) / 8n, p), p);
58
+ var vxx = _mul(v25, x * x % p, p);
59
+ if (vxx === u) { /* x is a root */ }
60
+ else if (vxx === (p - u) % p) { x = x * SQRTM1 % p; }
61
+ else return null;
62
+ }
63
+ if (x === 0n && sign === 1n) return null;
64
+ if ((x & 1n) !== sign) x = (p - x) % p;
65
+ return { x: x, y: y };
66
+ }
67
+
68
+ // Affine doubling on a*x^2 + y^2 = 1 + d*x^2*y^2: x3 = 2xy/(a x^2 + y^2),
69
+ // y3 = (y^2 - a x^2)/(2 - a x^2 - y^2).
70
+ function _double(P, c) {
71
+ var p = c.p, x2 = P.x * P.x % p, y2 = P.y * P.y % p, ax2 = c.a * x2 % p;
72
+ var nx = 2n * P.x % p * P.y % p, dx = (ax2 + y2) % p;
73
+ var ny = ((y2 - ax2) % p + p) % p, dy = ((2n - ax2 - y2) % p + 2n * p) % p;
74
+ return { x: _mul(nx, _inv(dx, p), p), y: _mul(ny, _inv(dy, p), p) };
75
+ }
76
+
77
+ // validate(raw, crv) -> true iff `raw` is a canonical, on-curve, full-order Edwards point for
78
+ // the OKP curve (6 = Ed25519, 7 = Ed448). Off-curve, non-canonical, wrong-length, and
79
+ // low-order (cofactor) points all return false.
80
+ function validate(raw, crv) {
81
+ var c = CURVE[crv];
82
+ if (!c || !Buffer.isBuffer(raw) || raw.length !== c.len) return false;
83
+ var pt = _decode(raw, c);
84
+ if (!pt) return false;
85
+ // Low-order iff [8]P is the identity (0, 1). One double per cofactor bit (8 = 2^3).
86
+ var q = _double(_double(_double(pt, c), c), c);
87
+ if (q.x === 0n && q.y === 1n) return false;
88
+ return true;
89
+ }
90
+
91
+ module.exports = { validate: validate };
@@ -298,6 +298,11 @@ var SigstoreError = defineClass("SigstoreError", { withCause: true });
298
298
  // whose label does not decode. Rendering itself is best-effort and never throws on
299
299
  // a malformed field -- an undecodable extension falls back to a hex dump.
300
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 });
301
306
 
302
307
  module.exports = {
303
308
  PkiError: PkiError,
@@ -325,6 +330,7 @@ module.exports = {
325
330
  HpkeError: HpkeError,
326
331
  SigstoreError: SigstoreError,
327
332
  InspectError: InspectError,
333
+ WebauthnError: WebauthnError,
328
334
  MerkleError: MerkleError,
329
335
  SmimeError: SmimeError,
330
336
  CsrattrsError: CsrattrsError,
package/lib/inspect.js CHANGED
@@ -121,7 +121,8 @@ function _keyBlock(spki, pad) {
121
121
 
122
122
  if (algName === "ecPublicKey" || algName === "id-ecPublicKey") {
123
123
  var curveName = null;
124
- try { curveName = oid.name(asn1.read.oid(asn1.decode(spki.algorithm.parameters))); } catch (_e) { /* unknown curve */ }
124
+ try { curveName = oid.name(asn1.read.oid(asn1.decode(spki.algorithm.parameters))); }
125
+ catch (_e) { /* unknown curve */ }
125
126
  var bits = CURVE_BITS[curveName] || (pub ? ((pub.length - 1) / 2) * 8 : 0);
126
127
  out.push(inner + "Public-Key: (" + bits + " bit)");
127
128
  if (pub) { out.push(inner + "pub:"); out.push(_hexColon(pub, { wrap: 16, indent: (pad.length + 8) })); }
@@ -211,6 +212,8 @@ function _gnRaw(buf) {
211
212
  if (t === 1 || t === 2 || t === 6) return GN_KIND[t] + ":" + _clean(node.content.toString("latin1"));
212
213
  if (t === 7) return "IP Address:" + _ipString(node.content);
213
214
  return _hexColon(buf, {}); // dirName / otherName / registeredID -> best-effort hex
215
+ // allow:swallow-unverified drop-silent display fallback (tier-3): fullName GNs reach here as
216
+ // schema-validated TLVs so asn1.decode cannot throw -- an unreachable defensive best-effort hex render.
214
217
  } catch (_e) { return _hexColon(buf, {}); }
215
218
  }
216
219
 
@@ -262,7 +265,12 @@ var EXT_RENDERERS = {
262
265
  return inner + Object.keys(KU_LABEL).filter(function (k) { return decoded[k]; }).map(function (k) { return KU_LABEL[k]; }).join(", ");
263
266
  },
264
267
  extKeyUsage: function (decoded, inner) {
265
- return inner + decoded.map(function (o) { var n = null; try { n = oid.name(o); } catch (_e) { /* unregistered EKU OID */ } return EKU_LABEL[n] || n || o; }).join(", ");
268
+ return inner + decoded.map(function (o) {
269
+ var n = null;
270
+ try { n = oid.name(o); }
271
+ catch (_e) { /* unregistered EKU OID */ }
272
+ return EKU_LABEL[n] || n || o;
273
+ }).join(", ");
266
274
  },
267
275
  basicConstraints: function (decoded, inner) {
268
276
  var s = "CA:" + (decoded.cA ? "TRUE" : "FALSE");
@@ -282,7 +290,9 @@ var EXT_RENDERERS = {
282
290
  try {
283
291
  (asn1.decode(p.qualifiersBytes).children || []).forEach(function (pqi) {
284
292
  var qid = asn1.read.oid(pqi.children[0]), q = pqi.children[1];
285
- var label = null; try { label = oid.name(qid); } catch (_e) { /* unregistered qualifier */ }
293
+ var label = null;
294
+ try { label = oid.name(qid); }
295
+ catch (_e) { /* unregistered qualifier */ }
286
296
  var val = (q && !q.constructed && Buffer.isBuffer(q.content) && _printable(q.content))
287
297
  ? _clean(q.content.toString("latin1"))
288
298
  : _hexColon(q && Buffer.isBuffer(q.bytes) ? q.bytes : Buffer.alloc(0), {});
package/lib/oid.js CHANGED
@@ -300,6 +300,30 @@ var FAMILIES = {
300
300
 
301
301
  // Misc datatypes (RFC 4519 domainComponent).
302
302
  datatype: { base: [0, 9, 2342, 19200300, 100, 1], of: { domainComponent: 25 } },
303
+
304
+ // FIDO Alliance generated-credential certificate extensions (arc
305
+ // 1.3.6.1.4.1.45724.1.1). id-fido-gen-ce-aaguid carries the authenticator's
306
+ // AAGUID -- a WebAuthn packed-attestation leaf-cert extension (WebAuthn L3 8.2.1)
307
+ // whose OCTET STRING value MUST equal the attestedCredentialData aaguid.
308
+ fidoGenCe: { base: [1, 3, 6, 1, 4, 1, 45724, 1, 1], of: { idFidoGenCeAaguid: 4 } },
309
+
310
+ // Android Keystore key-attestation extension (arc 1.3.6.1.4.1.11129.2.1). The
311
+ // keyDescription extension carries the hardware-backed KeyDescription SEQUENCE a
312
+ // WebAuthn android-key attestation leaf cert asserts (WebAuthn L3 8.4.1).
313
+ androidKeystore: { base: [1, 3, 6, 1, 4, 1, 11129, 2, 1], of: { keyDescription: 17 } },
314
+
315
+ // Apple anonymous-attestation extension (arc 1.2.840.113635.100.8). The value in
316
+ // a WebAuthn apple attestation leaf cert embeds the SHA-256 nonce over
317
+ // authenticatorData || clientDataHash (WebAuthn L3 8.8).
318
+ appleAttest: { base: [1, 2, 840, 113635, 100, 8], of: { appleAnonymousAttestation: 2 } },
319
+
320
+ // TCG (Trusted Computing Group) TPM identifiers on the 2.23.133 arc. tcgKp
321
+ // (id-tcg-kp) names the AIK-certificate extended key purpose a WebAuthn tpm
322
+ // attestation leaf cert MUST carry; tcgAt (id-tcg-at) names the tpmManufacturer /
323
+ // tpmModel / tpmVersion directory attributes its subjectAltName MUST carry
324
+ // (WebAuthn L3 8.3.1).
325
+ tcgKp: { base: [2, 23, 133, 8], of: { tcgKpAikCertificate: 3 } },
326
+ tcgAt: { base: [2, 23, 133, 2], of: { tpmManufacturer: 1, tpmModel: 2, tpmVersion: 3 } },
303
327
  };
304
328
 
305
329
  var _byOid = new Map();
@@ -1460,8 +1460,10 @@ function crlIssuerNamesIssuer(cRLIssuer, issuerRdns) {
1460
1460
  for (var i = 0; i < cRLIssuer.names.length; i++) {
1461
1461
  var n = cRLIssuer.names[i];
1462
1462
  if (n.tagNumber !== 4 || !n.value || !n.value.rdns) continue;
1463
+ // dnEqual throws only on a control-byte / malformed cRLIssuer DN; returning false excludes
1464
+ // that DP from the correspondence (a malformed indirect-CRL issuer name never corresponds).
1463
1465
  try { if (dnEqual(n.value.rdns, issuerRdns)) return true; }
1464
- catch (_e) { return false; }
1466
+ catch (_e) { return false; } // allow:swallow-unverified fail-closed predicate: error -> no match -> DP excluded (restrictive)
1465
1467
  }
1466
1468
  return false;
1467
1469
  }
@@ -0,0 +1,47 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ //
5
+ // @internal -- no operator-facing namespace. This is lib composition only; the
6
+ // documented surface is the primitives whose type handling composes these validators.
7
+ //
8
+ // validator-all -- the validator-family orchestrator (schema-all's role for the
9
+ // validator family, guard-all's role for the guards). It assembles the per-type
10
+ // conformance validators into one namespaced surface every format module composes,
11
+ // so a format depends on the family rather than re-deriving a decoded type's rule set
12
+ // inline (the drift that leaks a spec MUST out one review round at a time):
13
+ //
14
+ // validator.cose.credentialKey / .toSpki
15
+ // -- the COMPLETE WebAuthn credential COSE_Key rule
16
+ // set (RFC 9052/9053 + WebAuthn sec. 6.5.1 +
17
+ // CTAP2 canonical + on-curve), one home
18
+ // validator.sig.ecdsaSigToRaw -- the COMPLETE DER ECDSA-Sig-Value conformance
19
+ // (RFC 3279 + X.690 strict-DER) + raw r||s conversion
20
+ // validator.attcert.packedCert / .aikCert / .aaguidExt / .requireV3 / .assertNotCa
21
+ // -- the WebAuthn attestation-certificate profile
22
+ // (sec. 8.2.1 packed, sec. 8.3.1 TPM AIK), one home
23
+ // validator.keydesc.androidKeyDescription
24
+ // -- the Android Key Attestation KeyDescription
25
+ // (AOSP schema + WebAuthn sec. 8.4.1), one home
26
+ // validator.tpm.parsePubArea / .parseCertInfo / .pubKeyEqualsCose
27
+ // -- the TPM 2.0 pubArea/certInfo structure conformance
28
+ // + key binding (WebAuthn sec. 8.3 + TCG Part 2), one home
29
+ //
30
+ // Each validator is enforced by the validator-reinlined codebase-patterns detector: a
31
+ // lib function that re-inlines a type's characteristic validation shape (declared on the
32
+ // validator via @validator-shape) is flagged, so a new format module cannot re-derive a
33
+ // partial subset and reintroduce the drift the validator exists to prevent.
34
+
35
+ var cose = require("./validator-cose");
36
+ var sig = require("./validator-sig");
37
+ var attcert = require("./validator-attcert");
38
+ var keydesc = require("./validator-keydesc");
39
+ var tpm = require("./validator-tpm");
40
+
41
+ module.exports = {
42
+ cose: cose,
43
+ sig: sig,
44
+ attcert: attcert,
45
+ keydesc: keydesc,
46
+ tpm: tpm,
47
+ };
@@ -0,0 +1,110 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ //
5
+ // @internal -- no operator-facing namespace. The documented surface is the verifier whose
6
+ // attestation-certificate handling composes this validator (pki.webauthn).
7
+ //
8
+ // validator-attcert -- the SINGLE home for "does this X.509 leaf meet the WebAuthn
9
+ // attestation-certificate profile" (WebAuthn sec. 8.2.1 packed, sec. 8.3.1 TPM AIK).
10
+ // Sibling to the guard family: a validator owns a decoded TYPE's COMPLETE conformance rule
11
+ // set once, so the packed and AIK profiles cannot drift apart or lose a gate (e.g. the v3
12
+ // check that previously lived split out in the tpm verifier rather than in the profile).
13
+ //
14
+ // The caller supplies its parsed certificate + an extension-accessor object `exts`
15
+ // { find(cert, oidName) -> rawExt|null, decode(cert, oidName) -> {critical, value}|null }
16
+ // so this validator stays decoupled from any one format's extension-decoder namespace, and
17
+ // its typed error CONSTRUCTOR E + domain code (a distinct mismatchCode for aaguidExt).
18
+ //
19
+ // Rule set (gap-checked verbatim against WebAuthn sec. 8.2.1 + sec. 8.3.1 + RFC 5280):
20
+ // - requireV3: the leaf MUST be X.509 v3 (packed 8.2.1, AIK 8.3.1).
21
+ // - assertNotCa: the leaf MUST carry basicConstraints with cA=FALSE (present, not merely
22
+ // "not cA=true": a genuine CA or unconstrained leaf must not be repurposed as a leaf).
23
+ // - packedCert: v3 + non-CA + subject sets C, O, OU="Authenticator Attestation", CN.
24
+ // - aikCert: v3 + non-CA + EMPTY subject + EKU contains tcg-kp-AIKCertificate + SAN
25
+ // directoryName carries tpmManufacturer/tpmModel/tpmVersion (TPMv2-EK-Profile 3.2.9).
26
+ // - aaguidExt: the id-fido-gen-ce-aaguid extension, if present, MUST NOT be critical and
27
+ // MUST equal the authenticatorData aaguid (a 16-byte OCTET STRING inside the ext OCTET
28
+ // STRING). Absence is tolerated; presence must match.
29
+
30
+ var oid = require("./oid");
31
+ var asn1 = require("./asn1-der");
32
+
33
+ // requireV3(cert, E, code) -- the leaf attestation certificate MUST be X.509 v3.
34
+ // @enforced-by behavioral -- a version-field check has no rename-proof code shape; the RED
35
+ // vector (a non-v3 attestation leaf rejected) is the guard.
36
+ function requireV3(cert, E, code) {
37
+ if (cert.version !== 3) throw new E(code, "an attestation certificate must be X.509 version 3");
38
+ }
39
+
40
+ // assertNotCa(cert, exts, E, code) -- the leaf MUST carry basicConstraints with cA=FALSE.
41
+ // @enforced-by behavioral -- a basicConstraints cA gate has no rename-proof code shape; the
42
+ // RED vector (a CA-asserting or constraint-omitting attestation leaf rejected) is the guard.
43
+ function assertNotCa(cert, exts, E, code) {
44
+ var bc = exts.decode(cert, "basicConstraints");
45
+ if (!bc) throw new E(code, "an attestation leaf certificate MUST carry a basicConstraints extension (WebAuthn 8.2.1 / 8.3.1)");
46
+ if (bc.value && bc.value.cA === true) throw new E(code, "an attestation leaf certificate MUST NOT be a CA (basicConstraints cA=true)");
47
+ }
48
+
49
+ // packedCert(cert, exts, E, code) -- the complete WebAuthn 8.2.1 packed attestation-cert
50
+ // profile (v3 + non-CA + the four subject attributes).
51
+ // @enforced-by behavioral -- the subject-attribute + OU-literal checks are WebAuthn 8.2.1
52
+ // business rules with no rename-proof code shape; the RED vectors (missing C/O/CN, wrong OU,
53
+ // CA=true, non-v3) are the guard.
54
+ function packedCert(cert, exts, E, code) {
55
+ requireV3(cert, E, code);
56
+ assertNotCa(cert, exts, E, code);
57
+ var have = {};
58
+ (cert.subject.rdns || []).forEach(function (rdn) { rdn.forEach(function (a) { have[a.type] = a.value; }); });
59
+ if (have[oid.byName("countryName")] == null || have[oid.byName("organizationName")] == null || have[oid.byName("commonName")] == null) {
60
+ throw new E(code, "the packed attestation certificate subject MUST set C, O, OU, and CN (WebAuthn 8.2.1)");
61
+ }
62
+ if (have[oid.byName("organizationalUnitName")] !== "Authenticator Attestation") {
63
+ throw new E(code, "the packed attestation certificate subject OU MUST be \"Authenticator Attestation\" (WebAuthn 8.2.1)");
64
+ }
65
+ }
66
+
67
+ // aikCert(cert, exts, E, code) -- the complete WebAuthn 8.3.1 TPM AIK attestation-cert
68
+ // profile (v3 + non-CA + empty subject + tcg-kp-AIKCertificate EKU + the tcg SAN attributes).
69
+ // @enforced-by behavioral -- the empty-subject + EKU + SAN-attribute checks are WebAuthn
70
+ // 8.3.1 business rules with no rename-proof code shape; the RED vectors are the guard.
71
+ function aikCert(cert, exts, E, code) {
72
+ requireV3(cert, E, code);
73
+ assertNotCa(cert, exts, E, code);
74
+ if ((cert.subject.rdns || []).length !== 0) throw new E(code, "the tpm AIK certificate subject MUST be empty (WebAuthn 8.3.1)");
75
+ var eku = exts.decode(cert, "extKeyUsage");
76
+ if (!eku || !Array.isArray(eku.value) || eku.value.indexOf(oid.byName("tcgKpAikCertificate")) === -1) {
77
+ throw new E(code, "the tpm AIK certificate lacks the tcg-kp-AIKCertificate extended key purpose (WebAuthn 8.3.1)");
78
+ }
79
+ var san = exts.decode(cert, "subjectAltName");
80
+ var dirName = san && san.value && san.value.names && san.value.names.filter(function (n) { return n.tagNumber === 4; })[0];
81
+ var types = {};
82
+ if (dirName && dirName.value && dirName.value.rdns) {
83
+ dirName.value.rdns.forEach(function (rdn) { rdn.forEach(function (a) { types[a.type] = true; }); });
84
+ }
85
+ if (!types[oid.byName("tpmManufacturer")] || !types[oid.byName("tpmModel")] || !types[oid.byName("tpmVersion")]) {
86
+ throw new E(code, "the tpm AIK certificate subjectAltName lacks the required tcg attributes (WebAuthn 8.3.1)");
87
+ }
88
+ }
89
+
90
+ // aaguidExt(cert, aaguid, exts, E, code, mismatchCode) -- the id-fido-gen-ce-aaguid
91
+ // extension, if present, MUST NOT be critical and MUST equal the authenticatorData aaguid.
92
+ // @enforced-by behavioral -- an optional-extension equality check has no rename-proof code
93
+ // shape; the RED vectors (a critical ext, a non-matching aaguid) are the guard.
94
+ function aaguidExt(cert, aaguid, exts, E, code, mismatchCode) {
95
+ var ext = exts.find(cert, "idFidoGenCeAaguid");
96
+ if (!ext) return; // absence is tolerated; presence must match
97
+ if (ext.critical) throw new E(code, "the id-fido-gen-ce-aaguid extension MUST NOT be critical (WebAuthn 8.2.1)");
98
+ var val;
99
+ try { val = asn1.read.octetString(asn1.decode(ext.value)); }
100
+ catch (e) { throw new E(code, "the id-fido-gen-ce-aaguid extension value is not a valid OCTET STRING", e); }
101
+ if (!val.equals(aaguid)) throw new E(mismatchCode, "the id-fido-gen-ce-aaguid extension value does not equal the authenticatorData aaguid");
102
+ }
103
+
104
+ module.exports = {
105
+ requireV3: requireV3,
106
+ assertNotCa: assertNotCa,
107
+ packedCert: packedCert,
108
+ aikCert: aikCert,
109
+ aaguidExt: aaguidExt,
110
+ };