@blamejs/pki 0.2.5 → 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,25 @@ 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
+
7
26
  ## v0.2.5 — 2026-07-12
8
27
 
9
28
  WebAuthn / passkey attestation verification joins the toolkit as pki.webauthn.
package/README.md CHANGED
@@ -228,7 +228,7 @@ 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. 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
+ | `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` |
232
232
  | `pki.C` / `pki.constants` | Version-stable constants — functional scale helpers (`C.TIME.*`, `C.BYTES.*`), codec `LIMITS`, `version` |
233
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 |
234
234
  | `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>` |
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,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/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), {});
@@ -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
+ };
@@ -0,0 +1,176 @@
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
6
+ // verifiers whose credential-key handling composes this validator (pki.webauthn).
7
+ //
8
+ // validator-cose -- the SINGLE home for "is this a conformant WebAuthn credential
9
+ // COSE_Key" (RFC 9052 sec. 7 structure + RFC 9053 EC2/OKP/RSA key parameters + the
10
+ // CTAP2 canonical-CBOR profile WebAuthn sec. 6.5.1 imposes). Sibling to the guard
11
+ // family: where a guard owns a CVE-class fail-closed defence once, a validator owns a
12
+ // decoded TYPE's COMPLETE conformance rule set once, so a format module composes the
13
+ // family rather than re-deriving a partial subset inline (the drift that leaks MUSTs
14
+ // out one review round at a time). Enforced by the validator-shape-reinlined
15
+ // codebase-patterns detector: a lib function that re-inlines COSE-key validation fires.
16
+ //
17
+ // Interface mirrors the guard family: (subject, E, code) where E is the caller's typed
18
+ // error CONSTRUCTOR and code its domain code, so every boundary keeps its own
19
+ // domain/reason (a future COSE consumer passes its own E + code).
20
+ //
21
+ // Rule set (gap-checked verbatim against RFC 9052 sec. 7 + RFC 9053 sec. 2/6 +
22
+ // WebAuthn sec. 6.5.1 + the IANA COSE Key Type / Key Type Parameters registries):
23
+ // - kty (label 1) REQUIRED; value an integer (CTAP2 canonical -- a tstr kty/alg is
24
+ // rejected as non-canonical for the WebAuthn profile).
25
+ // - alg (label 3) REQUIRED (the RP needs it to verify the later assertion).
26
+ // - EC2 (kty 2): crv (-1), x (-2), y (-3) all present; x/y length == the curve field
27
+ // size. OKP (kty 1): crv (-1) == Ed25519(6)/Ed448(7), x (-2) length == the key size.
28
+ // RSA (kty 3): n (-1), e (-2) present + non-empty.
29
+ // - CANONICAL: EXACTLY the type's parameters, nothing more (EC2 = 5, OKP/RSA = 4) --
30
+ // rejects a padded key / a private "d" component / kid / key_ops (WebAuthn 6.5.1
31
+ // CTAP2 canonical, stricter than open COSE `* label => values`).
32
+ // - PROFILE: alg <-> kty (and, for EC2, alg <-> crv) consistent; -8 (EdDSA) is Ed25519
33
+ // ONLY; the RFC 9864 fully-specified ids (-9/-51/-52/-19/-53) are accepted.
34
+ // - COMPRESSED: an EC2 credential key MUST use the uncompressed point form (y is a full
35
+ // coordinate byte string, never a CBOR bool sign bit) -- WebAuthn sec. alg identifier.
36
+ // - ON-CURVE: the public key point MUST be valid for its curve. For EC2 the SPKI is
37
+ // imported via node:crypto so OpenSSL validates the point (an off-curve or identity
38
+ // point fails to parse). For OKP, OpenSSL does NOT validate the Edwards point on
39
+ // import (an all-zeroes key parses, and even verifies a trivial signature), so the
40
+ // point is checked explicitly via edwards-point (RFC 8032 decode + cofactor low-order
41
+ // rejection) -- an off-curve, non-canonical, or low-order OKP key fails closed.
42
+
43
+ var cbor = require("./cbor-det");
44
+ var asn1 = require("./asn1-der");
45
+ var oid = require("./oid");
46
+ var edwardsPoint = require("./edwards-point");
47
+ var nodeCrypto = require("crypto");
48
+
49
+ // COSE EC2 curve (label -1) -> the fixed field-element byte length x/y carry, and the
50
+ // named-curve OID a certificate on that curve declares.
51
+ var EC2_CRV_LEN = { 1: 32, 2: 48, 3: 66 }; // P-256 / P-384 / P-521
52
+ var EC2_CRV_OID = { 1: "prime256v1", 2: "secp384r1", 3: "secp521r1" };
53
+ // COSE OKP curve (label -1) -> its RFC 8410 named-key OID + fixed public-key length.
54
+ var OKP_CRV = { 6: { oid: "Ed25519", len: 32 }, 7: { oid: "Ed448", len: 57 } };
55
+ // alg (label 3) -> the key type (and, for EC2, curve) it pins. WebAuthn (sec. alg
56
+ // identifier) adds guarantees over the open COSE registry: an ECDSA alg fixes its curve,
57
+ // -8 (EdDSA) is Ed25519 ONLY, and the RFC 9864 fully-specified ids (-9 ESP256, -51 ESP384,
58
+ // -52 ESP512, -19 Ed25519, -53 Ed448) each pin key type + curve. A verifier accepts the
59
+ // fully-specified ids even though WebAuthn recommends against them for credential creation.
60
+ var ALG_PROFILE = {
61
+ "-7": { kty: 2, crv: 1 }, "-35": { kty: 2, crv: 2 }, "-36": { kty: 2, crv: 3 },
62
+ "-9": { kty: 2, crv: 1 }, "-51": { kty: 2, crv: 2 }, "-52": { kty: 2, crv: 3 },
63
+ "-8": { kty: 1, crv: 6 }, "-19": { kty: 1, crv: 6 }, "-53": { kty: 1, crv: 7 },
64
+ "-257": { kty: 3 }, "-258": { kty: 3 }, "-259": { kty: 3 }, "-37": { kty: 3 }, "-65535": { kty: 3 },
65
+ };
66
+
67
+ // The value node of an integer-labelled COSE_Key parameter (labels are ints), or null.
68
+ function _mapGetInt(node, key) {
69
+ if (!node || node.majorType !== 5 || !node.children) return null;
70
+ for (var i = 0; i < node.children.length; i++) {
71
+ var k = node.children[i][0];
72
+ if ((k.majorType === 0 || k.majorType === 1) && cbor.read.int(k) === BigInt(key)) return node.children[i][1];
73
+ }
74
+ return null;
75
+ }
76
+
77
+ // credentialKey(node, E, code) -> the decoded + validated credential public key
78
+ // { kty, alg, crv?, x?, y?, n?, e? }, or throws new E(code, ...). The complete COSE_Key
79
+ // conformance gate for a WebAuthn credential key; a format module MUST route a credential
80
+ // key through here, never re-inline the kty/alg/crv/length/canonical/profile/on-curve
81
+ // checks.
82
+ // @enforced-by validator-shape-reinlined
83
+ // @validator-shape kty\s*===\s*2n
84
+ // @validator-shape EC2_CRV_LEN|ALG_PROFILE
85
+ function credentialKey(node, E, code) {
86
+ function bad(msg, cause) { return new E(code, msg, cause); }
87
+ if (!node || node.majorType !== 5) throw bad("a COSE_Key must be a CBOR map (RFC 9052 sec. 7)");
88
+ // Every parameter read maps a wrong-type cbor/* fault to the caller's domain -- a
89
+ // wrong-typed COSE label (x as an integer, kty as a string) is bad input, not a leak.
90
+ function ib(label) { var n = _mapGetInt(node, label); if (!n) return null; try { return cbor.read.byteString(n); } catch (e) { throw bad("COSE_Key parameter " + label + " must be a byte string", e); } }
91
+ function ii(label) { var n = _mapGetInt(node, label); if (!n) return null; try { return cbor.read.int(n); } catch (e) { throw bad("COSE_Key parameter " + label + " must be an integer", e); } }
92
+ var ktyN = _mapGetInt(node, 1), algN = _mapGetInt(node, 3);
93
+ if (!ktyN) throw bad("a COSE_Key is missing the kty (label 1) parameter");
94
+ if (!algN) throw bad("a COSE_Key is missing the alg (label 3) parameter");
95
+ var kty, algv;
96
+ try { kty = cbor.read.int(ktyN); algv = cbor.read.int(algN); }
97
+ catch (e) { throw bad("COSE_Key kty (label 1) and alg (label 3) must be integers", e); }
98
+ var key = { kty: Number(kty), alg: Number(algv) };
99
+ if (kty === 2n) {
100
+ key.crv = ii(-1) != null ? Number(ii(-1)) : null;
101
+ // WebAuthn EC2 credential keys MUST use the uncompressed point form: y (-3) is the full
102
+ // y-coordinate byte string, never a CBOR bool sign bit (WebAuthn sec. alg identifier).
103
+ var yNode = _mapGetInt(node, -3);
104
+ if (yNode && yNode.majorType === 7) throw bad("an EC2 credential key must use the uncompressed point form (a compressed y sign-bit is not permitted for WebAuthn)");
105
+ key.x = ib(-2); key.y = ib(-3);
106
+ if (key.crv == null || !key.x || !key.y) throw bad("an EC2 COSE_Key must carry crv (-1), x (-2), and y (-3)");
107
+ var el = EC2_CRV_LEN[key.crv];
108
+ if (!el || key.x.length !== el || key.y.length !== el) throw bad("an EC2 COSE_Key x/y length is inconsistent with its curve");
109
+ } else if (kty === 1n) {
110
+ key.crv = ii(-1) != null ? Number(ii(-1)) : null; key.x = ib(-2);
111
+ var okp = OKP_CRV[key.crv];
112
+ if (!okp || !key.x || key.x.length !== okp.len) throw bad("an OKP COSE_Key must be Ed25519 (crv 6) or Ed448 (crv 7) with a matching-length x (-2)");
113
+ } else if (kty === 3n) {
114
+ key.n = ib(-1); key.e = ib(-2);
115
+ if (!key.n || !key.n.length || !key.e || !key.e.length) throw bad("an RSA COSE_Key must carry n (-1) and e (-2)");
116
+ } else {
117
+ throw bad("unsupported COSE_Key kty " + Number(kty));
118
+ }
119
+ // CANONICAL CTAP2 COSE_Key: exactly the type's parameters, nothing more.
120
+ var expectedParams = kty === 2n ? 5 : 4;
121
+ if (node.children.length !== expectedParams) throw bad("the COSE_Key carries parameters beyond the canonical set for its key type (WebAuthn sec. 6.5.1)");
122
+ // PROFILE: the declared alg must match the key type (and, for EC2, the curve).
123
+ var prof = ALG_PROFILE[String(key.alg)];
124
+ if (!prof) throw bad("unsupported credential key algorithm " + key.alg);
125
+ if (prof.kty !== key.kty) throw bad("credential key algorithm " + key.alg + " is inconsistent with key type " + key.kty);
126
+ if (prof.crv != null && prof.crv !== key.crv) throw bad("credential key algorithm " + key.alg + " requires a different curve");
127
+ // ON-CURVE: import the SPKI so OpenSSL validates the EC point on its curve. An off-curve
128
+ // x/y or the identity point fails to parse here.
129
+ try { nodeCrypto.createPublicKey({ key: toSpki(key, E, code), format: "der", type: "spki" }); }
130
+ catch (e) { throw bad("the credential public key point is not valid for its curve", e); }
131
+ // OpenSSL does NOT validate an OKP (Ed25519/Ed448) point on import -- an all-zeroes key
132
+ // parses, and even verifies a trivial signature -- so an OKP point needs an explicit
133
+ // on-curve + full-order (non-low-order) check (RFC 8032 decode + the cofactor check).
134
+ if (kty === 1n && !edwardsPoint.validate(key.x, key.crv)) throw bad("the OKP credential public key is not a valid, full-order Edwards point");
135
+ return key;
136
+ }
137
+
138
+ // toSpki(key, E, code) -> a self-contained SubjectPublicKeyInfo DER for a validated COSE
139
+ // key, so a credential key and a certificate key import/compare by one path.
140
+ // @enforced-by behavioral -- an SPKI encoding of a validated COSE key has no rename-proof
141
+ // code shape distinct from the ecPublicKey/rsaEncryption OID-name tokens that legitimately
142
+ // recur in the oid registry + inspect renderer; its consumers route through
143
+ // validatorCose.toSpki and credentialKey calls it on the on-curve path, so the webauthn
144
+ // KAT round-trip (a real SPKI imports + verifies) is the behavioral guard.
145
+ function toSpki(key, E, code) {
146
+ function bad(msg) { return new E(code, msg); }
147
+ if (key.kty === 2 && key.x && key.y) {
148
+ var curveOid = EC2_CRV_OID[key.crv];
149
+ if (!curveOid) throw bad("unsupported EC2 curve " + key.crv);
150
+ var b = asn1.build;
151
+ return b.sequence([
152
+ b.sequence([b.oid(oid.byName("ecPublicKey")), b.oid(oid.byName(curveOid))]),
153
+ b.bitString(Buffer.concat([Buffer.from([0x04]), key.x, key.y])),
154
+ ]);
155
+ }
156
+ if (key.kty === 3 && key.n && key.n.length && key.e && key.e.length) {
157
+ var bb = asn1.build;
158
+ return bb.sequence([
159
+ bb.sequence([bb.oid(oid.byName("rsaEncryption")), bb.nullValue()]),
160
+ bb.bitString(bb.sequence([bb.integer(BigInt("0x" + key.n.toString("hex"))), bb.integer(BigInt("0x" + key.e.toString("hex")))])),
161
+ ]);
162
+ }
163
+ if (key.kty === 1 && OKP_CRV[key.crv] && key.x && key.x.length === OKP_CRV[key.crv].len) {
164
+ var eb = asn1.build;
165
+ return eb.sequence([eb.sequence([eb.oid(oid.byName(OKP_CRV[key.crv].oid))]), eb.bitString(key.x)]);
166
+ }
167
+ throw bad("cannot build an SPKI for this COSE key type");
168
+ }
169
+
170
+ module.exports = {
171
+ credentialKey: credentialKey,
172
+ toSpki: toSpki,
173
+ EC2_CRV_LEN: EC2_CRV_LEN,
174
+ EC2_CRV_OID: EC2_CRV_OID,
175
+ OKP_CRV: OKP_CRV,
176
+ };
@@ -0,0 +1,89 @@
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
+ // android-key handling composes this validator (pki.webauthn).
7
+ //
8
+ // validator-keydesc -- the SINGLE home for the Android Key Attestation KeyDescription
9
+ // conformance (the AOSP key-attestation schema + WebAuthn sec. 8.4.1 authorization-list
10
+ // checks). Sibling to the guard family: a validator owns a decoded TYPE's COMPLETE
11
+ // conformance rule set once, so the four 8.4.1 checks cannot lose a gate to drift.
12
+ //
13
+ // The caller supplies its parsed certificate + an extension-accessor `exts`
14
+ // { find(cert, oidName) -> rawExt|null } + its typed error CONSTRUCTOR E with two codes: a
15
+ // STRUCTURAL `code` (a malformed KeyDescription is bad input) and a semantic `failCode` (a
16
+ // well-formed description that fails an 8.4.1 MUST is a failed verification).
17
+ //
18
+ // Rule set (gap-checked verbatim against WebAuthn sec. 8.4.1 + the AOSP KeyDescription
19
+ // schema):
20
+ // - KeyDescription is a positional SEQUENCE; attestationChallenge (position 4) MUST equal
21
+ // clientDataHash.
22
+ // - allApplications (tag 600) MUST be absent from BOTH the softwareEnforced (position 6)
23
+ // and teeEnforced (position 7) AuthorizationLists (the key MUST be scoped to the RP).
24
+ // - origin (tag 702) MUST equal KM_ORIGIN_GENERATED (0): at least one list declares it,
25
+ // and every list that declares one says GENERATED (a mixed IMPORTED/GENERATED key is
26
+ // contradictory and rejected).
27
+ // - purpose (tag 1) MUST be EXACTLY { KM_PURPOSE_SIGN (2) } over the union of the lists.
28
+
29
+ var asn1 = require("./asn1-der");
30
+
31
+ // The EXPLICIT-unwrapped value node for a [tag] field of an AuthorizationList (a SEQUENCE
32
+ // of context-tagged OPTIONAL fields with non-contiguous tag numbers), or null when absent.
33
+ function _alGet(seqNode, tag) {
34
+ if (!seqNode || !seqNode.children) return null;
35
+ for (var i = 0; i < seqNode.children.length; i++) {
36
+ var c = seqNode.children[i];
37
+ if (c.tagClass === "context" && c.tagNumber === tag) return c.children && c.children[0] ? c.children[0] : c;
38
+ }
39
+ return null;
40
+ }
41
+
42
+ function _alInt(node, E, code) {
43
+ try { return asn1.read.integer(node); }
44
+ catch (e) { throw new E(code, "an android KeyDescription authorization value is not an INTEGER", e); }
45
+ }
46
+
47
+ function _purposeUnion(a, b, E, code) {
48
+ var out = [];
49
+ [a, b].forEach(function (list) {
50
+ var p = _alGet(list, 1);
51
+ if (p && p.children) p.children.forEach(function (c) { var v = _alInt(c, E, code); if (out.indexOf(v) === -1) out.push(v); });
52
+ });
53
+ return out;
54
+ }
55
+
56
+ // androidKeyDescription(cert, clientDataHash, exts, E, code, failCode) -- the complete
57
+ // WebAuthn 8.4.1 KeyDescription gate. A verifier MUST route the android-key description
58
+ // through here, never re-derive a partial subset of the four checks inline.
59
+ // @enforced-by behavioral -- the challenge / allApplications / origin / purpose checks are
60
+ // WebAuthn 8.4.1 business rules with no rename-proof code shape; the RED vectors (challenge
61
+ // mismatch, allApplications present, origin != GENERATED, purpose != SIGN, and the empty-
62
+ // authorization-list spec vector) are the guard.
63
+ function androidKeyDescription(cert, clientDataHash, exts, E, code, failCode) {
64
+ var ext = exts.find(cert, "keyDescription");
65
+ if (!ext) throw new E(code, "the android-key attestation certificate is missing the key-description extension (WebAuthn 8.4.1)");
66
+ var kd;
67
+ try { kd = asn1.decode(ext.value); } catch (e) { throw new E(code, "the android KeyDescription is not decodable", e); }
68
+ if (!kd.children || kd.children.length < 8) throw new E(code, "the android KeyDescription is not a positional 8-field SEQUENCE");
69
+ // (1) attestationChallenge (position 4) == clientDataHash.
70
+ var challenge;
71
+ try { challenge = asn1.read.octetString(kd.children[4]); } catch (e) { throw new E(code, "the android attestationChallenge is not an OCTET STRING", e); }
72
+ if (!challenge.equals(clientDataHash)) throw new E(failCode, "the android attestationChallenge does not equal clientDataHash");
73
+ var softwareEnforced = kd.children[6], hardwareEnforced = kd.children[7];
74
+ // (2) [600] allApplications MUST be ABSENT in both lists.
75
+ if (_alGet(softwareEnforced, 600) || _alGet(hardwareEnforced, 600)) throw new E(failCode, "android allApplications MUST be absent (WebAuthn 8.4.1)");
76
+ // (3) origin == KM_ORIGIN_GENERATED (0) over the union: at least one list declares it,
77
+ // and every list that declares one says GENERATED.
78
+ var origins = [_alGet(softwareEnforced, 702), _alGet(hardwareEnforced, 702)].filter(Boolean);
79
+ if (!origins.length || !origins.every(function (o) { return _alInt(o, E, code) === 0n; })) {
80
+ throw new E(failCode, "android key origin is not KM_ORIGIN_GENERATED in every authorization list that declares it (WebAuthn 8.4.1)");
81
+ }
82
+ // (4) purpose == exactly { KM_PURPOSE_SIGN (2) } over the union.
83
+ var purposes = _purposeUnion(softwareEnforced, hardwareEnforced, E, code);
84
+ if (purposes.length !== 1 || purposes[0] !== 2n) {
85
+ throw new E(failCode, "android key purpose is not exactly KM_PURPOSE_SIGN (WebAuthn 8.4.1)");
86
+ }
87
+ }
88
+
89
+ module.exports = { androidKeyDescription: androidKeyDescription };