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