@blamejs/pki 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,57 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ //
5
+ // @internal -- no operator-facing namespace. The documented surface is the 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 rather than hand-decoding a 2-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 + 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.
25
+
26
+ var asn1 = require("./asn1-der");
27
+
28
+ // ecdsaSigToRaw(der, coordLen, E, code) -> the validated signature as raw r||s, each
29
+ // coordinate left-padded to coordLen bytes, or throws new E(code, ...). The complete DER
30
+ // ECDSA-Sig-Value conformance gate; a verifier MUST route an ECDSA signature through here,
31
+ // never hand-decode the SEQUENCE and read r/s content raw (which skips minimality).
32
+ // @enforced-by behavioral -- a DER ECDSA-Sig-Value decode has no rename-proof code shape
33
+ // distinct from generic 2-child-SEQUENCE content access; the RED conformance vectors
34
+ // (non-minimal / negative / zero / over-size r or s rejected) and the webauthn ECDSA KATs
35
+ // are the guard.
36
+ function ecdsaSigToRaw(der, coordLen, E, code) {
37
+ var node;
38
+ try { node = asn1.decode(der); } catch (e) { throw new E(code, "ECDSA signature is not a DER SEQUENCE", e); }
39
+ if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children || node.children.length !== 2) {
40
+ throw new E(code, "ECDSA signature must be a DER SEQUENCE { r, s }");
41
+ }
42
+ function coord(c, label) {
43
+ // The strict DER integer reader enforces PRIMITIVE + MINIMAL encoding (a constructed
44
+ // child, an empty INTEGER, or a redundant 0x00/0xFF sign octet all throw). The value
45
+ // is then range-checked: r and s MUST be positive (>= 1) and fit the curve field size.
46
+ var v;
47
+ try { v = asn1.read.integer(c); } catch (e) { throw new E(code, "ECDSA signature " + label + " is not a minimally-encoded DER INTEGER", e); }
48
+ if (v <= 0n) throw new E(code, "ECDSA signature " + label + " must be a positive integer");
49
+ var hex = v.toString(16); if (hex.length % 2) hex = "0" + hex;
50
+ var b = Buffer.from(hex, "hex");
51
+ if (b.length > coordLen) throw new E(code, "ECDSA signature " + label + " exceeds the curve field size");
52
+ var out = Buffer.alloc(coordLen); b.copy(out, coordLen - b.length); return out;
53
+ }
54
+ return Buffer.concat([coord(node.children[0], "r"), coord(node.children[1], "s")]);
55
+ }
56
+
57
+ module.exports = { ecdsaSigToRaw: ecdsaSigToRaw };
@@ -0,0 +1,135 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ //
5
+ // @internal -- no operator-facing namespace. The documented surface is the verifier whose
6
+ // tpm handling composes this validator (pki.webauthn).
7
+ //
8
+ // validator-tpm -- the SINGLE home for the TPM 2.0 structure conformance a WebAuthn tpm
9
+ // attestation carries: the TPMT_PUBLIC (pubArea) and TPMS_ATTEST (certInfo) decode, plus the
10
+ // pubArea-key == credential-key binding (WebAuthn sec. 8.3 + TCG TPM 2.0 Part 2). Sibling to
11
+ // the guard family: a validator owns a decoded TYPE's COMPLETE conformance rule set once, so
12
+ // the strict TPM2B sizing / algorithm-union walking / trailing-byte rejection cannot drift.
13
+ //
14
+ // Every field is unsigned big-endian, packed with no padding; every TPM2B_* is a UINT16 size
15
+ // followed by exactly `size` bytes. The bare certInfo and pubArea carry NO outer TPM2B size
16
+ // prefix. The caller supplies its typed error CONSTRUCTOR E + a structural `code` (a
17
+ // malformed structure is bad input) and, for the key binding, a `mismatchCode`.
18
+ //
19
+ // Rule set (gap-checked verbatim against WebAuthn sec. 8.3 + TCG TPM 2.0 Part 2 sec. 10.12 /
20
+ // 12.2):
21
+ // - parsePubArea: decode type/nameAlg, walk parameters per the algorithm union, read the
22
+ // public key in `unique`; reject trailing bytes (they perturb the TPM Name hash).
23
+ // - parseCertInfo: magic == TPM_GENERATED_VALUE, type == TPM_ST_ATTEST_CERTIFY; read
24
+ // extraData + attested.name; reject trailing bytes past the attested structure.
25
+ // - pubKeyEqualsCose: the pubArea key (EC curve+x+y, or RSA modulus+exponent) equals the
26
+ // credential COSE key, compared as unsigned magnitudes (a TPM2B may differ from a COSE
27
+ // coordinate by a leading 0x00), the RSA exponent over its full UINT32 width.
28
+
29
+ var ByteReader = require("./byte-reader");
30
+
31
+ var TPM_GENERATED_VALUE = 0xff544347;
32
+ var TPM_ST_ATTEST_CERTIFY = 0x8017;
33
+ var TPM_ALG = { RSA: 0x0001, SHA1: 0x0004, SHA256: 0x000b, SHA384: 0x000c, SHA512: 0x000d, NULL: 0x0010, ECC: 0x0023 };
34
+ var TPM_ALG_HASH = {}; TPM_ALG_HASH[TPM_ALG.SHA1] = "sha1"; TPM_ALG_HASH[TPM_ALG.SHA256] = "sha256"; TPM_ALG_HASH[TPM_ALG.SHA384] = "sha384"; TPM_ALG_HASH[TPM_ALG.SHA512] = "sha512";
35
+ // TPM_ECC_CURVE -> COSE crv (the codepoints differ; this is a mapping, not equality).
36
+ var TPM_CURVE_TO_COSE = {}; TPM_CURVE_TO_COSE[0x0003] = 1; TPM_CURVE_TO_COSE[0x0004] = 2; TPM_CURVE_TO_COSE[0x0005] = 3;
37
+
38
+ // Unsigned big-endian magnitude compare: strip leading zero octets, then byte-equal. TPM2B
39
+ // buffers and COSE fixed-length coordinates can differ by a leading 0x00.
40
+ function _ucmp(a, b) {
41
+ function strip(x) { var i = 0; while (i < x.length - 1 && x[i] === 0) i++; return x.subarray(i); }
42
+ return Buffer.compare(strip(a), strip(b)) === 0;
43
+ }
44
+ function _uintBytes(n) {
45
+ var hex = n.toString(16); if (hex.length % 2) hex = "0" + hex;
46
+ var b = Buffer.from(hex, "hex"); var i = 0; while (i < b.length - 1 && b[i] === 0) i++; return b.subarray(i);
47
+ }
48
+ // TPMT_SYM_DEF_OBJECT: algorithm UINT16; when not NULL, keyBits+mode follow. An attestation
49
+ // (restricted signing) key is NULL, so the non-NULL arm is defensive.
50
+ function _symDef(r) { if (r.u16() !== TPM_ALG.NULL) { r.u16(); r.u16(); } }
51
+ // TPMT_*_SCHEME / TPMT_KDF_SCHEME: scheme UINT16; when not NULL, a TPMS_SCHEME_HASH (a single
52
+ // UINT16 hashAlg) follows for the signing/kdf schemes in scope.
53
+ function _scheme(r) { if (r.u16() !== TPM_ALG.NULL) { r.u16(); } }
54
+
55
+ // parsePubArea(buf, E, code) -> the decoded TPMT_PUBLIC (WebAuthn 8.3 item 17-20).
56
+ // @enforced-by behavioral -- a packed TPM structure decode has no rename-proof code shape;
57
+ // the RED vectors (trailing bytes, an unsupported type, a truncated TPM2B) are the guard.
58
+ function parsePubArea(buf, E, code) {
59
+ var r = new ByteReader(buf, 0, buf.length, E, code);
60
+ var type = r.u16(), nameAlg = r.u16();
61
+ r.u32(); // objectAttributes (full policy validation deferred)
62
+ r.vector(2, 0, null); // authPolicy TPM2B_DIGEST
63
+ var pub = { type: type, nameAlg: nameAlg, nameAlgBytes: buf.subarray(2, 4) };
64
+ if (type === TPM_ALG.RSA) {
65
+ _symDef(r); // symmetric TPMT_SYM_DEF_OBJECT
66
+ _scheme(r); // scheme TPMT_RSA_SCHEME
67
+ r.u16(); // keyBits
68
+ var exp = r.u32(); // exponent (0 => default 65537)
69
+ pub.exponent = exp === 0 ? 65537 : exp;
70
+ pub.rsa = r.vector(2, 0, null); // unique TPM2B_PUBLIC_KEY_RSA (the modulus)
71
+ } else if (type === TPM_ALG.ECC) {
72
+ _symDef(r); // symmetric
73
+ _scheme(r); // scheme TPMT_ECC_SCHEME
74
+ pub.curveId = r.u16(); // curveID TPMI_ECC_CURVE
75
+ _scheme(r); // kdf TPMT_KDF_SCHEME
76
+ pub.x = r.vector(2, 0, null); // unique.ecc.x TPM2B_ECC_PARAMETER
77
+ pub.y = r.vector(2, 0, null); // unique.ecc.y TPM2B_ECC_PARAMETER
78
+ } else {
79
+ throw new E(code, "unsupported TPMT_PUBLIC type 0x" + type.toString(16));
80
+ }
81
+ // TPMT_PUBLIC ends with `unique`; trailing bytes mean a malformed pubArea (and would
82
+ // perturb the TPM Name hash), so fail closed rather than silently ignore them.
83
+ if (!r.atEnd()) throw new E(code, "pubArea has trailing bytes after the unique field (WebAuthn 8.3)");
84
+ return pub;
85
+ }
86
+
87
+ // parseCertInfo(buf, E, code) -> the decoded + magic/type-validated TPMS_ATTEST
88
+ // { extraData, attestedName } (WebAuthn 8.3 item 13-15).
89
+ // @enforced-by behavioral -- a packed TPM structure decode has no rename-proof code shape;
90
+ // the RED vectors (wrong magic, wrong type, trailing bytes) are the guard.
91
+ function parseCertInfo(buf, E, code) {
92
+ var r = new ByteReader(buf, 0, buf.length, E, code);
93
+ var magic = r.u32(), type = r.u16();
94
+ if (magic !== TPM_GENERATED_VALUE) throw new E(code, "certInfo magic is not TPM_GENERATED_VALUE (WebAuthn 8.3)");
95
+ if (type !== TPM_ST_ATTEST_CERTIFY) throw new E(code, "certInfo type is not TPM_ST_ATTEST_CERTIFY (WebAuthn 8.3)");
96
+ r.vector(2, 0, null); // qualifiedSigner TPM2B_NAME
97
+ var extraData = r.vector(2, 0, null); // extraData TPM2B_DATA
98
+ r.u64(); r.u32(); r.u32(); r.u8(); // clockInfo TPMS_CLOCK_INFO (17 bytes)
99
+ r.u64(); // firmwareVersion
100
+ var name = r.vector(2, 0, null); // attested.name TPM2B_NAME (nameAlg||H)
101
+ r.vector(2, 0, null); // attested.qualifiedName TPM2B_NAME
102
+ if (!r.atEnd()) throw new E(code, "certInfo has trailing bytes after the attested structure (WebAuthn 8.3)");
103
+ return { extraData: extraData, attestedName: name };
104
+ }
105
+
106
+ // pubKeyEqualsCose(pub, cose, E, mismatchCode, code) -- the pubArea public key MUST equal the
107
+ // credential COSE key (WebAuthn 8.3 item 22).
108
+ // @enforced-by behavioral -- a decoded-key equality check has no rename-proof code shape; the
109
+ // RED vectors (a mismatched EC curve/coordinate, a mismatched RSA modulus/exponent) are the guard.
110
+ function pubKeyEqualsCose(pub, cose, E, mismatchCode, code) {
111
+ if (pub.type === TPM_ALG.ECC) {
112
+ if (cose.kty !== 2 || TPM_CURVE_TO_COSE[pub.curveId] !== cose.crv || !_ucmp(pub.x, cose.x || Buffer.alloc(0)) || !_ucmp(pub.y, cose.y || Buffer.alloc(0))) {
113
+ throw new E(mismatchCode, "the TPM pubArea EC key does not equal the credential public key");
114
+ }
115
+ return;
116
+ }
117
+ if (pub.type === TPM_ALG.RSA) {
118
+ // Compare the exponent as an unsigned integer over its FULL width (a UINT32 up to
119
+ // 0xFFFFFFFF); a fixed 3-byte re-encode would silently truncate an exponent > 0xFFFFFF
120
+ // and let a mismatched key pass (WebAuthn 8.3 item 22).
121
+ var e = _uintBytes(pub.exponent >>> 0);
122
+ if (cose.kty !== 3 || !_ucmp(pub.rsa, cose.n || Buffer.alloc(0)) || !_ucmp(e, cose.e || Buffer.alloc(0))) {
123
+ throw new E(mismatchCode, "the TPM pubArea RSA key does not equal the credential public key");
124
+ }
125
+ return;
126
+ }
127
+ throw new E(code, "unsupported TPM pubArea key type");
128
+ }
129
+
130
+ module.exports = {
131
+ parsePubArea: parsePubArea,
132
+ parseCertInfo: parseCertInfo,
133
+ pubKeyEqualsCose: pubKeyEqualsCose,
134
+ TPM_ALG_HASH: TPM_ALG_HASH,
135
+ };