@blamejs/pki 0.6.13 → 0.6.15

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.
Files changed (114) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/README.md +1 -1
  3. package/index.js +1 -1
  4. package/lib/acme.js +59 -815
  5. package/lib/asn1-der.js +7 -245
  6. package/lib/attrcert-sign.js +12 -260
  7. package/lib/attrcert-verify.js +1 -7
  8. package/lib/byte-reader.js +0 -8
  9. package/lib/byte-writer.js +0 -11
  10. package/lib/cbor-det.js +0 -54
  11. package/lib/cmc-build.js +15 -357
  12. package/lib/cmc-verify.js +6 -239
  13. package/lib/cmp-build.js +41 -317
  14. package/lib/cmp-session.js +73 -760
  15. package/lib/cmp-verify.js +32 -488
  16. package/lib/cms-compress.js +3 -32
  17. package/lib/cms-decrypt.js +29 -294
  18. package/lib/cms-encrypt.js +14 -234
  19. package/lib/cms-sign.js +29 -324
  20. package/lib/cms-verify.js +9 -500
  21. package/lib/composite-kem.js +2 -102
  22. package/lib/composite-sig.js +1 -77
  23. package/lib/constants.js +0 -249
  24. package/lib/crl-sign.js +9 -237
  25. package/lib/crl-verify.js +1 -10
  26. package/lib/crmf-sign.js +19 -198
  27. package/lib/crmf-verify.js +1 -12
  28. package/lib/csr-sign.js +3 -34
  29. package/lib/csr-verify.js +1 -12
  30. package/lib/ct.js +28 -253
  31. package/lib/edwards-point.js +10 -44
  32. package/lib/est.js +28 -764
  33. package/lib/framework-error.js +4 -182
  34. package/lib/guard-all.js +1 -58
  35. package/lib/guard-async.js +1 -36
  36. package/lib/guard-bytes.js +6 -650
  37. package/lib/guard-compress.js +5 -102
  38. package/lib/guard-crypto.js +1 -40
  39. package/lib/guard-der.js +2 -93
  40. package/lib/guard-encoding.js +2 -60
  41. package/lib/guard-header.js +1 -36
  42. package/lib/guard-identifier.js +7 -531
  43. package/lib/guard-intrinsic.js +1 -150
  44. package/lib/guard-json.js +10 -73
  45. package/lib/guard-limits.js +1 -77
  46. package/lib/guard-list.js +1 -49
  47. package/lib/guard-name.js +3 -209
  48. package/lib/guard-parsed.js +7 -335
  49. package/lib/guard-range.js +1 -71
  50. package/lib/guard-secret.js +1 -102
  51. package/lib/guard-text.js +1 -64
  52. package/lib/guard-time.js +1 -97
  53. package/lib/hpke.js +3 -105
  54. package/lib/http-digest.js +14 -142
  55. package/lib/http-retry-after.js +1 -47
  56. package/lib/http-transport.js +21 -131
  57. package/lib/inspect.js +30 -284
  58. package/lib/ip-utils.js +6 -47
  59. package/lib/jose.js +7 -142
  60. package/lib/key.js +2 -67
  61. package/lib/lint.js +9 -194
  62. package/lib/merkle.js +1 -27
  63. package/lib/mime.js +9 -82
  64. package/lib/ocsp-request-verify.js +1 -14
  65. package/lib/ocsp-verify.js +1 -82
  66. package/lib/ocsp.js +11 -169
  67. package/lib/oid.js +5 -337
  68. package/lib/path-validate.js +103 -1492
  69. package/lib/pbes2.js +2 -89
  70. package/lib/pkcs12-build.js +31 -379
  71. package/lib/pki-build.js +64 -290
  72. package/lib/rc2.js +2 -18
  73. package/lib/rfc3339.js +4 -27
  74. package/lib/scep.js +59 -233
  75. package/lib/schema-all.js +0 -122
  76. package/lib/schema-attrcert.js +4 -146
  77. package/lib/schema-c509.js +153 -802
  78. package/lib/schema-cmc.js +5 -220
  79. package/lib/schema-cmp.js +2 -223
  80. package/lib/schema-cms.js +13 -427
  81. package/lib/schema-crl.js +5 -68
  82. package/lib/schema-crmf.js +6 -167
  83. package/lib/schema-csr.js +1 -54
  84. package/lib/schema-csrattrs.js +2 -87
  85. package/lib/schema-engine.js +0 -152
  86. package/lib/schema-ocsp.js +0 -117
  87. package/lib/schema-pkcs12.js +2 -93
  88. package/lib/schema-pkcs8.js +3 -35
  89. package/lib/schema-pkix.js +24 -655
  90. package/lib/schema-smime.js +0 -54
  91. package/lib/schema-tsp.js +3 -99
  92. package/lib/schema-x509.js +4 -63
  93. package/lib/shbs.js +11 -95
  94. package/lib/sign-scheme.js +2 -97
  95. package/lib/sigstore.js +9 -169
  96. package/lib/sleep.js +2 -8
  97. package/lib/smime.js +15 -408
  98. package/lib/tls-cert-compress.js +0 -62
  99. package/lib/trust.js +19 -185
  100. package/lib/tsp-sign.js +16 -270
  101. package/lib/validator-all.js +1 -30
  102. package/lib/validator-attcert.js +2 -46
  103. package/lib/validator-cose.js +2 -153
  104. package/lib/validator-keydesc.js +1 -37
  105. package/lib/validator-sig.js +1 -46
  106. package/lib/validator-tls.js +1 -88
  107. package/lib/validator-tpm.js +19 -122
  108. package/lib/verify-seam.js +1 -24
  109. package/lib/webauthn-mds.js +3 -400
  110. package/lib/webauthn.js +12 -833
  111. package/lib/webcrypto.js +5 -485
  112. package/lib/x509-sign.js +4 -108
  113. package/package.json +1 -1
  114. package/sbom.cdx.json +6 -6
@@ -1,37 +1,8 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // Copyright (c) blamejs contributors
3
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 and no module re-derives 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.ecdsaDerToP1363 -- the COMPLETE order-aware DER ECDSA-Sig-Value
19
- // conformance (RFC 3279 + X.690 strict-DER +
20
- // FIPS 186-5 [1,n-1], CVE-2022-21449) + raw r||s
21
- // validator.attcert.packedCert / .aikCert / .aaguidExt / .requireV3 / .assertNotCa
22
- // -- the WebAuthn attestation-certificate profile
23
- // (sec. 8.2.1 packed, sec. 8.3.1 TPM AIK), one home
24
- // validator.keydesc.androidKeyDescription
25
- // -- the Android Key Attestation KeyDescription
26
- // (AOSP schema + WebAuthn sec. 8.4.1), one home
27
- // validator.tpm.parsePubArea / .parseCertInfo / .pubKeyEqualsCose
28
- // -- the TPM 2.0 pubArea/certInfo structure conformance
29
- // + key binding (WebAuthn sec. 8.3 + TCG Part 2), one home
30
- //
31
- // Each validator is enforced by the validator-reinlined codebase-patterns detector: a
32
- // lib function that re-inlines a type's characteristic validation shape (declared on the
4
+ // @internal
33
5
  // validator via @validator-shape) is flagged, so a new format module cannot re-derive a
34
- // partial subset and reintroduce the drift the validator exists to prevent.
35
6
 
36
7
  var cose = require("./validator-cose");
37
8
  var sig = require("./validator-sig");
@@ -1,62 +1,27 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // Copyright (c) blamejs contributors
3
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. The version
12
- // check belongs to the profile, not to whichever verifier reaches it first.
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.
4
+ // @internal
29
5
 
30
6
  var oid = require("./oid");
31
7
  var asn1 = require("./asn1-der");
32
8
  var guard = require("./guard-all");
33
9
 
34
- // Does this extended-key-usage list assert the given purpose? The answer decides whether a
35
- // certificate qualifies as the kind the rule names, so it goes through the membership guard
36
- // instead of a prototype method. See guard-list.
37
10
  var _asserts = guard.list.contains;
38
11
 
39
- // requireV3(cert, E, code) -- the leaf attestation certificate MUST be X.509 v3.
40
12
  // @enforced-by behavioral -- a version-field check has no rename-proof code shape; the RED
41
- // vector (a non-v3 attestation leaf rejected) is the guard.
42
13
  function requireV3(cert, E, code) {
43
14
  if (cert.version !== 3) throw new E(code, "an attestation certificate must be X.509 version 3");
44
15
  }
45
16
 
46
- // assertNotCa(cert, exts, E, code) -- the leaf MUST carry basicConstraints with cA=FALSE.
47
17
  // @enforced-by behavioral -- a basicConstraints cA gate has no rename-proof code shape; the
48
- // RED vector (a CA-asserting or constraint-omitting attestation leaf rejected) is the guard.
49
18
  function assertNotCa(cert, exts, E, code) {
50
19
  var bc = exts.decode(cert, "basicConstraints");
51
20
  if (!bc) throw new E(code, "an attestation leaf certificate MUST carry a basicConstraints extension (WebAuthn 8.2.1 / 8.3.1)");
52
21
  if (bc.value && bc.value.cA === true) throw new E(code, "an attestation leaf certificate MUST NOT be a CA (basicConstraints cA=true)");
53
22
  }
54
23
 
55
- // packedCert(cert, exts, E, code) -- the complete WebAuthn 8.2.1 packed attestation-cert
56
- // profile (v3 + non-CA + the four subject attributes).
57
24
  // @enforced-by behavioral -- the subject-attribute + OU-literal checks are WebAuthn 8.2.1
58
- // business rules with no rename-proof code shape; the RED vectors (missing C/O/CN, wrong OU,
59
- // CA=true, non-v3) are the guard.
60
25
  function packedCert(cert, exts, E, code) {
61
26
  requireV3(cert, E, code);
62
27
  assertNotCa(cert, exts, E, code);
@@ -70,18 +35,12 @@ function packedCert(cert, exts, E, code) {
70
35
  }
71
36
  }
72
37
 
73
- // aikCert(cert, exts, E, code) -- the complete WebAuthn 8.3.1 TPM AIK attestation-cert
74
- // profile (v3 + non-CA + empty subject + tcg-kp-AIKCertificate EKU + the tcg SAN attributes).
75
38
  // @enforced-by behavioral -- the empty-subject + EKU + SAN-attribute checks are WebAuthn
76
- // 8.3.1 business rules with no rename-proof code shape; the RED vectors are the guard.
77
39
  function aikCert(cert, exts, E, code) {
78
40
  requireV3(cert, E, code);
79
41
  assertNotCa(cert, exts, E, code);
80
42
  if ((cert.subject.rdns || []).length !== 0) throw new E(code, "the tpm AIK certificate subject MUST be empty (WebAuthn 8.3.1)");
81
43
  var eku = exts.decode(cert, "extKeyUsage");
82
- // The purpose test IS the rule that makes this an AIK certificate, so it is decided by comparison
83
- // rather than by a prototype method: a replaced `indexOf` reports the purpose present on any
84
- // certificate and an ordinary leaf is accepted as a TPM attestation key.
85
44
  if (!eku || !Array.isArray(eku.value) || !_asserts(eku.value, oid.byName("tcgKpAikCertificate"))) {
86
45
  throw new E(code, "the tpm AIK certificate lacks the tcg-kp-AIKCertificate extended key purpose (WebAuthn 8.3.1)");
87
46
  }
@@ -96,13 +55,10 @@ function aikCert(cert, exts, E, code) {
96
55
  }
97
56
  }
98
57
 
99
- // aaguidExt(cert, aaguid, exts, E, code, mismatchCode) -- the id-fido-gen-ce-aaguid
100
- // extension, if present, MUST NOT be critical and MUST equal the authenticatorData aaguid.
101
58
  // @enforced-by behavioral -- an optional-extension equality check has no rename-proof code
102
- // shape; the RED vectors (a critical ext, a non-matching aaguid) are the guard.
103
59
  function aaguidExt(cert, aaguid, exts, E, code, mismatchCode) {
104
60
  var ext = exts.find(cert, "idFidoGenCeAaguid");
105
- if (!ext) return; // absence is tolerated; presence must match
61
+ if (!ext) return;
106
62
  if (ext.critical) throw new E(code, "the id-fido-gen-ce-aaguid extension MUST NOT be critical (WebAuthn 8.2.1)");
107
63
  var val;
108
64
  try { val = asn1.read.octetString(asn1.decode(ext.value)); }
@@ -1,44 +1,7 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // Copyright (c) blamejs contributors
3
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 defense once, a validator owns a
12
- // decoded TYPE's COMPLETE conformance rule set once, so a format module composes the
13
- // family instead of 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, so 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 only
33
- // Ed25519; 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, per WebAuthn
35
- // sec. alg identifier: y is a full coordinate byte string, never a CBOR bool sign bit.
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.
4
+ // @internal
42
5
 
43
6
  var cbor = require("./cbor-det");
44
7
  var asn1 = require("./asn1-der");
@@ -46,56 +9,27 @@ var oid = require("./oid");
46
9
  var edwardsPoint = require("./edwards-point");
47
10
  var nodeCrypto = require("crypto");
48
11
 
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
12
+ var EC2_CRV_LEN = { 1: 32, 2: 48, 3: 66 };
52
13
  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
14
  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 only Ed25519, 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
- // The RSA credential-key bounds. 2048 bits is the floor every current FIDO authenticator and
61
- // NIST SP 800-57 agree on; nothing in the field emits less, so the floor refuses forgeable keys
62
- // without refusing real ones. The exponent bound is a work bound, not a security one.
63
15
  var RSA_MIN_MODULUS_BITS = 2048;
64
16
  var RSA_MAX_EXPONENT_BYTES = 8;
65
- // The modulus BIT length. A byte count is not one: minimally encoded, a 256-byte modulus whose
66
- // leading byte is 0x01 is 2041 bits, and would clear a floor expressed in bytes while sitting below
67
- // the floor that floor exists to state. The leading byte is non-zero by the minimal-encoding check
68
- // above, so its position fixes the total.
69
17
  function _modulusBits(n) { return (n.length - 1) * 8 + (32 - Math.clz32(n[0])); }
70
18
 
71
19
  var ALG_PROFILE = {
72
20
  "-7": { kty: 2, crv: 1 }, "-35": { kty: 2, crv: 2 }, "-36": { kty: 2, crv: 3 },
73
21
  "-9": { kty: 2, crv: 1 }, "-51": { kty: 2, crv: 2 }, "-52": { kty: 2, crv: 3 },
74
22
  "-8": { kty: 1, crv: 6 }, "-19": { kty: 1, crv: 6 }, "-53": { kty: 1, crv: 7 },
75
- // RSASSA-PSS at all three strengths. PS256 alone left PS384/PS512 refused at PARSE time on a key
76
- // that is perfectly well-formed (the same bytes accepted under -37), so the refusal blamed the
77
- // key instead of the algorithm, and a relying party migrating credential rows written by another
78
- // implementation could not tell which of its stored keys this verifier would decline, or why.
79
23
  "-257": { kty: 3 }, "-258": { kty: 3 }, "-259": { kty: 3 },
80
24
  "-37": { kty: 3 }, "-38": { kty: 3 }, "-39": { kty: 3 }, "-65535": { kty: 3 },
81
25
  };
82
26
 
83
- // credentialKey(node, E, code) -> the decoded + validated credential public key
84
- // { kty, alg, crv?, x?, y?, n?, e? }, or throws new E(code, ...). The complete COSE_Key
85
- // conformance gate for a WebAuthn credential key; a format module MUST route a credential
86
- // key through here, never re-inline the kty/alg/crv/length/canonical/profile/on-curve
87
- // checks.
88
27
  // @enforced-by validator-shape-reinlined
89
28
  // @validator-shape kty\s*===\s*2n
90
29
  // @validator-shape EC2_CRV_LEN|ALG_PROFILE
91
- // `unsupportedCode` is OPTIONAL and names the code raised when the key is well-formed but its
92
- // algorithm is not one this verifier implements, a different fact from a malformed key. Omit it
93
- // and that case keeps raising `code`, so an existing caller sees no change.
94
30
  function credentialKey(node, E, code, unsupportedCode) {
95
31
  function bad(msg, cause) { return new E(code, msg, cause); }
96
32
  if (!node || node.majorType !== 5) throw bad("a COSE_Key must be a CBOR map (RFC 9052 sec. 7)");
97
- // Every parameter read maps a wrong-type cbor/* fault to the caller's domain: a
98
- // wrong-typed COSE label (x as an integer, kty as a string) is bad input, not a leak.
99
33
  function ib(label) { var n = cbor.read.mapGet(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); } }
100
34
  function ii(label) { var n = cbor.read.mapGet(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); } }
101
35
  var ktyN = cbor.read.mapGet(node, 1), algN = cbor.read.mapGet(node, 3);
@@ -107,8 +41,6 @@ function credentialKey(node, E, code, unsupportedCode) {
107
41
  var key = { kty: Number(kty), alg: Number(algv) };
108
42
  if (kty === 2n) {
109
43
  key.crv = ii(-1) != null ? Number(ii(-1)) : null;
110
- // WebAuthn EC2 credential keys MUST use the uncompressed point form: y (-3) is the full
111
- // y-coordinate byte string, never a CBOR bool sign bit (WebAuthn sec. alg identifier).
112
44
  var yNode = cbor.read.mapGet(node, -3);
113
45
  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)");
114
46
  key.x = ib(-2); key.y = ib(-3);
@@ -122,19 +54,6 @@ function credentialKey(node, E, code, unsupportedCode) {
122
54
  } else if (kty === 3n) {
123
55
  key.n = ib(-1); key.e = ib(-2);
124
56
  if (!key.n || !key.n.length || !key.e || !key.e.length) throw bad("an RSA COSE_Key must carry n (-1) and e (-2)");
125
- // The material itself, not merely its presence: the same standard the other two key types are
126
- // held to, where EC2 pins x/y to the curve's field size and has the point validated on the curve,
127
- // and OKP pins x to an exact length. Checking only presence let a 1-byte modulus and an
128
- // exponent of 1 through as conformant credential public keys, and both reach the WebCrypto
129
- // import, so they reach real signature verification. e = 1 makes RSA the identity function:
130
- // the "signature" is the message, and it verifies under any modulus.
131
- // Both values first, before either is judged. RFC 8230 sec. 4 encodes n and e as unsigned
132
- // big-endian integers with no leading zero, and every check below reads a byte length as though
133
- // it were a magnitude: the modulus floor, the exponent bound, and the exponent's value. A
134
- // padded encoding decouples the two, so `00 01` would be read as a two-byte exponent and skip
135
- // the value check that refuses 1, the degenerate key the whole check exists to catch. (An EC2
136
- // coordinate is the opposite case, fixed-width and zero-padded by definition, which is why this
137
- // rule is stated for the RSA parameters and not for x/y.)
138
57
  if (key.n[0] === 0) throw bad("an RSA COSE_Key modulus (-1) must be minimally encoded, with no leading zero byte (RFC 8230 sec. 4)");
139
58
  if (key.e[0] === 0) throw bad("an RSA COSE_Key exponent (-2) must be minimally encoded, with no leading zero byte (RFC 8230 sec. 4)");
140
59
  var modulusBits = _modulusBits(key.n);
@@ -142,67 +61,24 @@ function credentialKey(node, E, code, unsupportedCode) {
142
61
  throw bad("an RSA COSE_Key modulus (-1) is " + modulusBits + " bits, below the " +
143
62
  RSA_MIN_MODULUS_BITS + "-bit minimum");
144
63
  }
145
- // e must be odd and greater than 1: RSA needs gcd(e, phi(n)) = 1, so an even exponent is not a
146
- // valid RSA public exponent at all, and 1 is the degenerate case above. Bounded on the way in
147
- // so a caller cannot hand over a megabyte of exponent for the modular exponentiation to chew.
148
64
  if (key.e.length > RSA_MAX_EXPONENT_BYTES) throw bad("an RSA COSE_Key exponent (-2) is longer than " + RSA_MAX_EXPONENT_BYTES + " bytes");
149
65
  if ((key.e[key.e.length - 1] & 1) === 0) throw bad("an RSA COSE_Key exponent (-2) must be odd");
150
- // Minimal encoding above makes a one-byte e the only way to express a value this small, so the
151
- // comparison is on the value and not on where it happens to sit.
152
66
  if (key.e.length === 1 && key.e[0] <= 1) throw bad("an RSA COSE_Key exponent (-2) must be greater than 1; e = 1 makes RSA the identity function");
153
67
  } else {
154
68
  throw bad("unsupported COSE_Key kty " + Number(kty));
155
69
  }
156
- // CANONICAL CTAP2 COSE_Key: exactly the type's parameters, nothing more.
157
70
  var expectedParams = kty === 2n ? 5 : 4;
158
71
  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)");
159
72
  return assertKeyMaterial(key, E, code, unsupportedCode);
160
73
  }
161
74
 
162
- // assertKeyMaterial(key, E, code, unsupportedCode) -> key | throws
163
- //
164
- // The rules about the KEY, split from the rules about its CBOR encoding, because the
165
- // toolkit accepts a stored credential key in two forms and both reach a signature
166
- // verification. `pki.webauthn.verifyAssertion` takes the COSE bytes or the object
167
- // `pki.webauthn.verify` returned; the bytes went through every check below and the
168
- // object went through none, so the same 1-byte modulus was refused as a credential key
169
- // in one form and imported for verification in the other. A caller stores whichever
170
- // form their datastore round-trips, which is not a choice about how carefully their
171
- // credential is checked.
172
- //
173
- // Everything above this line is about the encoding (a CBOR map, integer labels, byte
174
- // strings, the canonical parameter count) and can only be asked of bytes. Everything
175
- // here is about the key, and is asked of both.
176
- //
177
75
  // @enforced-by behavioral -- key-material rules have no rename-proof code shape distinct
178
- // from ordinary length and byte comparisons; the RED vectors that drive both accepted
179
- // forms of a stored credential key (the COSE bytes and the object) through
180
- // pki.webauthn.verifyAssertion with an undersized modulus, e = 1, a curve/length
181
- // mismatch and a short OKP x are the guard.
182
76
  function assertKeyMaterial(key, E, code, unsupportedCode) {
183
77
  function bad(msg, cause) { return new E(code, msg, cause); }
184
78
  if (!key || typeof key !== "object") throw bad("a credential key must be a decoded COSE_Key object");
185
- // One read of each field, into a plain object, before anything is checked or used.
186
- //
187
- // The object form comes from the caller, so any of these can be an accessor. One that throws
188
- // turns a validation into a raw fault, the thing this function exists to prevent, and one
189
- // that returns different values on successive reads makes the field that was checked and the
190
- // field that is used two different values, which defeats the check outright. Reading each
191
- // exactly once settles both, for every field, not just the ones a particular branch reaches.
192
79
  try {
193
80
  key = { kty: key.kty, alg: key.alg, crv: key.crv, x: key.x, y: key.y, n: key.n, e: key.e };
194
81
  } catch (e) { throw bad("a credential key field could not be read", e); }
195
- // Then the type and the value. BigInt() throws a raw TypeError on a Symbol and on undefined, and
196
- // a raw RangeError on a fractional or non-finite number, so the check is not "it is a number";
197
- // it is "it is an integer". Every integer label the branches below read, not the two the
198
- // dispatch happens to need first:
199
- // crv indexes a lookup table, and a Symbol thrown at a property read is the same raw fault as a
200
- // Symbol thrown at BigInt(). The decoded form gets these from the CBOR reader, which has already
201
- // established them; the object form gets them from the caller, so this is where they are settled.
202
- // A BigInt is bounded too. COSE labels are small registry integers, and an unbounded one converts
203
- // to Infinity, which then throws a raw RangeError at the next conversion: the same defeat as a
204
- // fractional number, reached by a value that is an integer. So "it is an integer" is not the
205
- // whole check either; "it is an integer this code can carry" is.
206
82
  var MAX = BigInt(Number.MAX_SAFE_INTEGER);
207
83
  function _isInt(v) {
208
84
  if (typeof v === "bigint") return v <= MAX && v >= -MAX;
@@ -211,11 +87,6 @@ function assertKeyMaterial(key, E, code, unsupportedCode) {
211
87
  if (!_isInt(key.kty)) throw bad("a COSE_Key kty (label 1) must be an integer");
212
88
  if (!_isInt(key.alg)) throw bad("a COSE_Key alg (label 3) must be an integer");
213
89
  if (key.crv !== undefined && key.crv !== null && !_isInt(key.crv)) throw bad("a COSE_Key crv (label -1) must be an integer");
214
- // One representation from here down. A label may arrive as a Number or a BigInt (a CBOR reader
215
- // hands out BigInt; an object built in JavaScript is likelier to hold Number), and everything
216
- // below compares with === against the Number-keyed profile table and the curve tables. Accepting
217
- // both forms at the gate and then comparing only one is a check that answers by how the caller
218
- // happened to spell the value; the decoded arm normalizes here too, for the same reason.
219
90
  key.kty = Number(key.kty);
220
91
  key.alg = Number(key.alg);
221
92
  if (key.crv !== undefined && key.crv !== null) key.crv = Number(key.crv);
@@ -239,41 +110,21 @@ function assertKeyMaterial(key, E, code, unsupportedCode) {
239
110
  } else {
240
111
  throw bad("unsupported COSE_Key kty " + Number(key.kty));
241
112
  }
242
- // PROFILE: the declared alg must match the key type (and, for EC2, the curve).
243
113
  var prof = ALG_PROFILE[String(key.alg)];
244
- // An algorithm this verifier does not implement is not a malformed key. The key can be perfectly
245
- // well-formed (the same bytes may parse under a neighboring algorithm id), and a relying
246
- // party migrating credential rows written elsewhere needs to tell "I cannot check this
247
- // algorithm" from "these bytes are wrong", since only one of those is fixable by re-registering.
248
- // Callers that do not distinguish the two pass one code and keep the previous behavior.
249
114
  if (!prof) throw new E(unsupportedCode || code, "unsupported credential key algorithm " + key.alg);
250
115
  if (prof.kty !== key.kty) throw bad("credential key algorithm " + key.alg + " is inconsistent with key type " + key.kty);
251
116
  if (prof.crv != null && prof.crv !== key.crv) throw bad("credential key algorithm " + key.alg + " requires a different curve");
252
- // ON-CURVE: import the SPKI so OpenSSL validates the EC point on its curve. An off-curve
253
- // x/y or the identity point fails to parse here.
254
117
  try { nodeCrypto.createPublicKey({ key: toSpki(key, E, code), format: "der", type: "spki" }); }
255
118
  catch (e) { throw bad("the credential public key point is not valid for its curve", e); }
256
- // OpenSSL does not validate an OKP (Ed25519/Ed448) point on import. An all-zeroes key
257
- // parses, and even verifies a trivial signature, so an OKP point needs an explicit
258
- // on-curve + full-order (non-low-order) check (RFC 8032 decode + the cofactor check).
259
119
  if (Number(key.kty) === 1 && !edwardsPoint.validate(key.x, key.crv)) throw bad("the OKP credential public key is not a valid, full-order Edwards point");
260
120
  return key;
261
121
  }
262
122
 
263
- // toSpki(key, E, code) -> a self-contained SubjectPublicKeyInfo DER for a validated COSE
264
- // key, so a credential key and a certificate key import/compare by one path.
265
123
  // @enforced-by behavioral -- an SPKI encoding of a validated COSE key has no rename-proof
266
- // code shape distinct from the ecPublicKey/rsaEncryption OID-name tokens that legitimately
267
- // recur in the oid registry + inspect renderer; its consumers route through
268
- // validatorCose.toSpki and credentialKey calls it on the on-curve path, so the webauthn
269
- // KAT round-trip (a real SPKI imports + verifies) is the behavioral guard.
270
124
  function toSpki(key, E, code) {
271
125
  function bad(msg) { return new E(code, msg); }
272
126
  if (key.kty === 2 && key.x && key.y) {
273
127
  var curveOid = EC2_CRV_OID[key.crv];
274
- // Coverage residual -- toSpki only receives a credentialKey-validated key; EC2 crv is
275
- // already gated to {1,2,3} by EC2_CRV_LEN, the exact EC2_CRV_OID keyset, so curveOid is
276
- // always defined.
277
128
  if (!curveOid) throw bad("unsupported EC2 curve " + key.crv);
278
129
  var b = asn1.build;
279
130
  return b.sequence([
@@ -292,8 +143,6 @@ function toSpki(key, E, code) {
292
143
  var eb = asn1.build;
293
144
  return eb.sequence([eb.sequence([eb.oid(oid.byName(OKP_CRV[key.crv].oid))]), eb.bitString(key.x)]);
294
145
  }
295
- // Coverage residual -- a credentialKey-validated key always matches one of the three forms
296
- // above; this fallthrough is defensive depth.
297
146
  throw bad("cannot build an SPKI for this COSE key type");
298
147
  }
299
148
 
@@ -1,35 +1,10 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // Copyright (c) blamejs contributors
3
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.
4
+ // @internal
28
5
 
29
6
  var asn1 = require("./asn1-der");
30
7
 
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
8
  function _alGet(seqNode, tag) {
34
9
  if (!seqNode || !seqNode.children) return null;
35
10
  for (var i = 0; i < seqNode.children.length; i++) {
@@ -53,33 +28,22 @@ function _purposeUnion(a, b, E, code) {
53
28
  return out;
54
29
  }
55
30
 
56
- // androidKeyDescription(cert, clientDataHash, exts, E, code, failCode) is 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
31
  // @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
32
  function androidKeyDescription(cert, clientDataHash, exts, E, code, failCode) {
64
33
  var ext = exts.find(cert, "keyDescription");
65
34
  if (!ext) throw new E(code, "the android-key attestation certificate is missing the key-description extension (WebAuthn 8.4.1)");
66
35
  var kd;
67
36
  try { kd = asn1.decode(ext.value); } catch (e) { throw new E(code, "the android KeyDescription is not decodable", e); }
68
37
  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
38
  var challenge;
71
39
  try { challenge = asn1.read.octetString(kd.children[4]); } catch (e) { throw new E(code, "the android attestationChallenge is not an OCTET STRING", e); }
72
40
  if (!challenge.equals(clientDataHash)) throw new E(failCode, "the android attestationChallenge does not equal clientDataHash");
73
41
  var softwareEnforced = kd.children[6], hardwareEnforced = kd.children[7];
74
- // (2) [600] allApplications MUST be ABSENT in both lists.
75
42
  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
43
  var origins = [_alGet(softwareEnforced, 702), _alGet(hardwareEnforced, 702)].filter(Boolean);
79
44
  if (!origins.length || !origins.every(function (o) { return _alInt(o, E, code) === 0n; })) {
80
45
  throw new E(failCode, "android key origin is not KM_ORIGIN_GENERATED in every authorization list that declares it (WebAuthn 8.4.1)");
81
46
  }
82
- // (4) purpose == exactly { KM_PURPOSE_SIGN (2) } over the union.
83
47
  var purposes = _purposeUnion(softwareEnforced, hardwareEnforced, E, code);
84
48
  if (purposes.length !== 1 || purposes[0] !== 2n) {
85
49
  throw new E(failCode, "android key purpose is not exactly KM_PURPOSE_SIGN (WebAuthn 8.4.1)");
@@ -1,38 +1,11 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // Copyright (c) blamejs contributors
3
3
  "use strict";
4
- //
5
- // @internal -- no operator-facing namespace. The documented surface is the verifiers
6
- // whose ECDSA signature handling composes this validator (pki.webauthn).
7
- //
8
- // validator-sig -- the SINGLE home for "is this a conformant DER ECDSA-Sig-Value", and
9
- // its conversion to the raw r||s (IEEE P1363) form a WebCrypto verify expects. Sibling to
10
- // the guard family: a validator owns a decoded type's complete conformance rule set once,
11
- // so a format module composes it instead of hand-decoding a two-INTEGER SEQUENCE into r/s
12
- // and forgetting a strict-DER check (the drift a validator exists to prevent).
13
- //
14
- // Interface mirrors the guard family: (subject, ..., E, code) where E is the caller's
15
- // typed error CONSTRUCTOR and code its domain code.
16
- //
17
- // Rule set (gap-checked verbatim against RFC 3279 sec. 2.2.3 + X.690 sec. 8.3 + SEC1):
18
- // - ECDSA-Sig-Value ::= SEQUENCE { r INTEGER, s INTEGER }, a universal SEQUENCE with
19
- // exactly two children.
20
- // - r and s are each a primitive, minimally-encoded (X.690 sec. 8.3.2, no redundant
21
- // leading 0x00/0xFF), positive integer. Reading through the strict DER integer reader
22
- // enforces the primitive and minimal-encoding rules; the value is then range-checked
23
- // positive (r,s >= 1) and bounded by the curve field size. A non-minimal, zero,
24
- // negative, or over-size coordinate fails closed, never normalized-and-accepted.
4
+ // @internal
25
5
 
26
6
  var asn1 = require("./asn1-der");
27
7
 
28
- // rawToEcdsaDer(raw, coordLen) -> the canonical DER ECDSA-Sig-Value SEQUENCE { r, s } for a raw
29
- // r||s signature (IEEE P1363, the WebCrypto sign() output). The inverse of ecdsaDerToP1363: a
30
- // signer composes this so the emitted ECDSA signature is canonical DER (minimally-encoded
31
- // INTEGERs via the build layer), never a hand-built SEQUENCE that could re-introduce a
32
- // non-minimal encoding. A config-time TypeError guards a mis-sized raw signature at entry.
33
8
  // @enforced-by behavioral -- a DER ECDSA-Sig-Value BUILD has no rename-proof code shape distinct
34
- // from generic sequence([integer,integer]); the round-trip vectors (build -> ecdsaDerToP1363
35
- // identity) and the cms.sign ECDSA KATs are the guard.
36
9
  function rawToEcdsaDer(raw, coordLen) {
37
10
  if (!Buffer.isBuffer(raw) || typeof coordLen !== "number" || coordLen <= 0 || raw.length !== coordLen * 2) {
38
11
  throw new TypeError("rawToEcdsaDer: raw signature must be a Buffer of exactly 2*coordLen bytes");
@@ -42,7 +15,6 @@ function rawToEcdsaDer(raw, coordLen) {
42
15
  return asn1.build.sequence([asn1.build.integer(r), asn1.build.integer(s)]);
43
16
  }
44
17
 
45
- // The curve group orders n -- a valid ECDSA signature has r, s in [1, n-1].
46
18
  var CURVE_FIELD_BYTES = { "P-256": 32, "P-384": 48, "P-521": 66 };
47
19
  var CURVE_ORDER = {
48
20
  "P-256": BigInt("0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551"),
@@ -50,18 +22,7 @@ var CURVE_ORDER = {
50
22
  "P-521": BigInt("0x01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA51868783BF2F966B7FCC0148F709A5D03BB5C9B8899C47AEBB6FB71E91386409"),
51
23
  };
52
24
 
53
- // ecdsaDerToP1363(der, curve, E, code) -> the DER ECDSA-Sig-Value converted to raw r||s (P1363),
54
- // each coordinate left-padded to the curve field width, rejecting r or s outside [1, n-1] against
55
- // the curve order (CVE-2022-21449 "Psychic Signatures": the r/s = 0 case and the >= n upper
56
- // bound). `curve` is a WebCrypto namedCurve (P-256/384/521). This is the single ECDSA-Sig-Value
57
- // conformance gate every curve-aware verifier routes through: it enforces the complete strict-DER
58
- // rule set (2-INTEGER SEQUENCE, minimal encoding) AND the order bound, so a verifier never
59
- // hand-decodes the SEQUENCE (skipping minimality) nor bounds r/s only by the field size (missing
60
- // the order). A non-minimal, negative, zero, over-size, or >= n coordinate fails closed.
61
25
  // @enforced-by behavioral -- a DER ECDSA-Sig-Value decode has no rename-proof code shape distinct
62
- // from generic 2-child-SEQUENCE content access; the RED conformance vectors (r/s = 0, r/s >= n,
63
- // non-minimal, negative, over-size) and the cms / ct / composite / path / webauthn ECDSA KATs are
64
- // the guard.
65
26
  function ecdsaDerToP1363(der, curve, E, code) {
66
27
  var width = CURVE_FIELD_BYTES[curve];
67
28
  var order = CURVE_ORDER[curve];
@@ -72,9 +33,6 @@ function ecdsaDerToP1363(der, curve, E, code) {
72
33
  if (n.tagClass !== "universal" || n.tagNumber !== asn1.TAGS.SEQUENCE || !n.children || n.children.length !== 2) {
73
34
  throw new E(code, "ECDSA signature must be a SEQUENCE of exactly two INTEGERs");
74
35
  }
75
- // Wrap the strict DER integer reads (they enforce primitive and minimal encoding) so a non-minimal,
76
- // constructed or empty INTEGER surfaces the caller's typed code and not a raw asn1/*. That is the
77
- // complete DER conformance rule set, gated together with the order bound below.
78
36
  var r, s;
79
37
  try { r = asn1.read.integer(n.children[0]); } catch (e) { throw new E(code, "ECDSA signature r is not a minimally-encoded DER INTEGER", e); }
80
38
  try { s = asn1.read.integer(n.children[1]); } catch (e) { throw new E(code, "ECDSA signature s is not a minimally-encoded DER INTEGER", e); }
@@ -85,9 +43,6 @@ function ecdsaDerToP1363(der, curve, E, code) {
85
43
  var hex = v.toString(16);
86
44
  if (hex.length % 2) hex = "0" + hex;
87
45
  var buf = Buffer.from(hex, "hex");
88
- // Coverage residual -- unreachable: a component needing more than `width` bytes is >=
89
- // 2^(8*width) > n (the curve order n < 2^(8*width) for P-256/384/521), so it is already
90
- // rejected by the r/s >= order check above; this width guard is a defense-in-depth backstop.
91
46
  if (buf.length > width) throw new E(code, "ECDSA signature component wider than the curve field");
92
47
  var out = Buffer.alloc(width);
93
48
  buf.copy(out, width - buf.length);