@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.
- package/CHANGELOG.md +22 -1
- package/README.md +1 -1
- package/index.js +1 -1
- package/lib/acme.js +59 -815
- package/lib/asn1-der.js +7 -245
- package/lib/attrcert-sign.js +12 -260
- package/lib/attrcert-verify.js +1 -7
- package/lib/byte-reader.js +0 -8
- package/lib/byte-writer.js +0 -11
- package/lib/cbor-det.js +0 -54
- package/lib/cmc-build.js +15 -357
- package/lib/cmc-verify.js +6 -239
- package/lib/cmp-build.js +41 -317
- package/lib/cmp-session.js +73 -760
- package/lib/cmp-verify.js +32 -488
- package/lib/cms-compress.js +3 -32
- package/lib/cms-decrypt.js +29 -294
- package/lib/cms-encrypt.js +14 -234
- package/lib/cms-sign.js +29 -324
- package/lib/cms-verify.js +9 -500
- package/lib/composite-kem.js +2 -102
- package/lib/composite-sig.js +1 -77
- package/lib/constants.js +0 -249
- package/lib/crl-sign.js +9 -237
- package/lib/crl-verify.js +1 -10
- package/lib/crmf-sign.js +19 -198
- package/lib/crmf-verify.js +1 -12
- package/lib/csr-sign.js +3 -34
- package/lib/csr-verify.js +1 -12
- package/lib/ct.js +28 -253
- package/lib/edwards-point.js +10 -44
- package/lib/est.js +28 -764
- package/lib/framework-error.js +4 -182
- package/lib/guard-all.js +1 -58
- package/lib/guard-async.js +1 -36
- package/lib/guard-bytes.js +6 -650
- package/lib/guard-compress.js +5 -102
- package/lib/guard-crypto.js +1 -40
- package/lib/guard-der.js +2 -93
- package/lib/guard-encoding.js +2 -60
- package/lib/guard-header.js +1 -36
- package/lib/guard-identifier.js +7 -531
- package/lib/guard-intrinsic.js +1 -150
- package/lib/guard-json.js +10 -73
- package/lib/guard-limits.js +1 -77
- package/lib/guard-list.js +1 -49
- package/lib/guard-name.js +3 -209
- package/lib/guard-parsed.js +7 -335
- package/lib/guard-range.js +1 -71
- package/lib/guard-secret.js +1 -102
- package/lib/guard-text.js +1 -64
- package/lib/guard-time.js +1 -97
- package/lib/hpke.js +3 -105
- package/lib/http-digest.js +14 -142
- package/lib/http-retry-after.js +1 -47
- package/lib/http-transport.js +21 -131
- package/lib/inspect.js +30 -284
- package/lib/ip-utils.js +6 -47
- package/lib/jose.js +7 -142
- package/lib/key.js +2 -67
- package/lib/lint.js +9 -194
- package/lib/merkle.js +1 -27
- package/lib/mime.js +9 -82
- package/lib/ocsp-request-verify.js +1 -14
- package/lib/ocsp-verify.js +1 -82
- package/lib/ocsp.js +11 -169
- package/lib/oid.js +5 -337
- package/lib/path-validate.js +103 -1492
- package/lib/pbes2.js +2 -89
- package/lib/pkcs12-build.js +31 -379
- package/lib/pki-build.js +64 -290
- package/lib/rc2.js +2 -18
- package/lib/rfc3339.js +4 -27
- package/lib/scep.js +59 -233
- package/lib/schema-all.js +0 -122
- package/lib/schema-attrcert.js +4 -146
- package/lib/schema-c509.js +153 -802
- package/lib/schema-cmc.js +5 -220
- package/lib/schema-cmp.js +2 -223
- package/lib/schema-cms.js +13 -427
- package/lib/schema-crl.js +5 -68
- package/lib/schema-crmf.js +6 -167
- package/lib/schema-csr.js +1 -54
- package/lib/schema-csrattrs.js +2 -87
- package/lib/schema-engine.js +0 -152
- package/lib/schema-ocsp.js +0 -117
- package/lib/schema-pkcs12.js +2 -93
- package/lib/schema-pkcs8.js +3 -35
- package/lib/schema-pkix.js +24 -655
- package/lib/schema-smime.js +0 -54
- package/lib/schema-tsp.js +3 -99
- package/lib/schema-x509.js +4 -63
- package/lib/shbs.js +11 -95
- package/lib/sign-scheme.js +2 -97
- package/lib/sigstore.js +9 -169
- package/lib/sleep.js +2 -8
- package/lib/smime.js +15 -408
- package/lib/tls-cert-compress.js +0 -62
- package/lib/trust.js +19 -185
- package/lib/tsp-sign.js +16 -270
- package/lib/validator-all.js +1 -30
- package/lib/validator-attcert.js +2 -46
- package/lib/validator-cose.js +2 -153
- package/lib/validator-keydesc.js +1 -37
- package/lib/validator-sig.js +1 -46
- package/lib/validator-tls.js +1 -88
- package/lib/validator-tpm.js +19 -122
- package/lib/verify-seam.js +1 -24
- package/lib/webauthn-mds.js +3 -400
- package/lib/webauthn.js +12 -833
- package/lib/webcrypto.js +5 -485
- package/lib/x509-sign.js +4 -108
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/validator-all.js
CHANGED
|
@@ -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");
|
package/lib/validator-attcert.js
CHANGED
|
@@ -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;
|
|
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)); }
|
package/lib/validator-cose.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/lib/validator-keydesc.js
CHANGED
|
@@ -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)");
|
package/lib/validator-sig.js
CHANGED
|
@@ -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);
|