@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 +33 -0
- package/README.md +4 -2
- package/index.js +9 -0
- package/lib/acme.js +2 -1
- package/lib/byte-reader.js +68 -0
- package/lib/ct.js +6 -38
- package/lib/edwards-point.js +91 -0
- package/lib/framework-error.js +6 -0
- package/lib/inspect.js +13 -3
- package/lib/oid.js +24 -0
- package/lib/path-validate.js +3 -1
- package/lib/validator-all.js +47 -0
- package/lib/validator-attcert.js +110 -0
- package/lib/validator-cose.js +176 -0
- package/lib/validator-keydesc.js +89 -0
- package/lib/validator-sig.js +57 -0
- package/lib/validator-tpm.js +135 -0
- package/lib/webauthn.js +621 -0
- package/lib/webcrypto.js +5 -1
- package/package.json +3 -1
- package/sbom.cdx.json +6 -6
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),
|
|
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); }
|
|
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
|
|
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
|
|
@@ -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 };
|
package/lib/framework-error.js
CHANGED
|
@@ -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))); }
|
|
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) {
|
|
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;
|
|
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();
|
package/lib/path-validate.js
CHANGED
|
@@ -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
|
+
};
|