@blamejs/pki 0.2.5 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +1 -1
- package/lib/acme.js +2 -1
- package/lib/edwards-point.js +91 -0
- package/lib/inspect.js +13 -3
- package/lib/path-validate.js +3 -1
- package/lib/validator-all.js +47 -0
- package/lib/validator-attcert.js +110 -0
- package/lib/validator-cose.js +176 -0
- package/lib/validator-keydesc.js +89 -0
- package/lib/validator-sig.js +57 -0
- package/lib/validator-tpm.js +135 -0
- package/lib/webauthn.js +77 -352
- package/lib/webcrypto.js +5 -1
- package/package.json +3 -1
- package/sbom.cdx.json +6 -6
|
@@ -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
|
+
};
|