@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
package/lib/webauthn.js
CHANGED
|
@@ -29,7 +29,8 @@ var pkix = require("./schema-pkix");
|
|
|
29
29
|
var oid = require("./oid");
|
|
30
30
|
var webcrypto = require("./webcrypto");
|
|
31
31
|
var constants = require("./constants");
|
|
32
|
-
var
|
|
32
|
+
var validator = require("./validator-all");
|
|
33
|
+
var edwardsPoint = require("./edwards-point");
|
|
33
34
|
var nodeCrypto = require("crypto");
|
|
34
35
|
|
|
35
36
|
var WebauthnError = frameworkError.WebauthnError;
|
|
@@ -61,15 +62,13 @@ function _ucmp(a, b) {
|
|
|
61
62
|
// A decoded node is a primitive universal INTEGER (so `.content` is a real buffer,
|
|
62
63
|
// not null as it is for a constructed node).
|
|
63
64
|
function _isInteger(node) { return !!node && !node.constructed && node.tagClass === "universal" && node.tagNumber === asn1.TAGS.INTEGER; }
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
// the TPM extraData / apple nonce is taken under).
|
|
72
|
-
var COSE_ALG_HASH = { "-7": "sha256", "-257": "sha256", "-37": "sha256", "-35": "sha384", "-258": "sha384", "-36": "sha512", "-259": "sha512", "-65535": "sha1" };
|
|
65
|
+
// alg -> the digest a TPM attestation's certInfo.extraData is taken under (the ONLY
|
|
66
|
+
// consumer). Every ECDSA/RSA algorithm a TPM AIK may sign with -- including the RFC 9864
|
|
67
|
+
// fully-specified ECDSA ids (ESP256/384/512) -- MUST appear here, or the TPM extraData
|
|
68
|
+
// step rejects the attestation before the signature is evaluated. EdDSA (-8/-19/-53) is
|
|
69
|
+
// absent by design: a TPM 2.0 AIK never signs with EdDSA, so such an attestation is
|
|
70
|
+
// correctly refused.
|
|
71
|
+
var COSE_ALG_HASH = { "-7": "sha256", "-9": "sha256", "-257": "sha256", "-37": "sha256", "-35": "sha384", "-51": "sha384", "-258": "sha384", "-36": "sha512", "-52": "sha512", "-259": "sha512", "-65535": "sha1" };
|
|
73
72
|
function _coseAlgHash(alg, E) {
|
|
74
73
|
var h = COSE_ALG_HASH[String(alg)];
|
|
75
74
|
if (!h) throw E("webauthn/unsupported-algorithm", "no hash mapping for COSE algorithm " + alg);
|
|
@@ -89,15 +88,6 @@ function _mapGet(node, key) {
|
|
|
89
88
|
}
|
|
90
89
|
return null;
|
|
91
90
|
}
|
|
92
|
-
// An integer-keyed entry (COSE_Key labels are ints), by its signed value.
|
|
93
|
-
function _mapGetInt(node, key) {
|
|
94
|
-
if (!node || node.majorType !== 5 || !node.children) return null;
|
|
95
|
-
for (var i = 0; i < node.children.length; i++) {
|
|
96
|
-
var k = node.children[i][0];
|
|
97
|
-
if ((k.majorType === 0 || k.majorType === 1) && cbor.read.int(k) === BigInt(key)) return node.children[i][1];
|
|
98
|
-
}
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
91
|
|
|
102
92
|
// ---- authenticatorData bounded reader (WebAuthn sec. 6.1) --------------------
|
|
103
93
|
|
|
@@ -134,7 +124,7 @@ function _parseAuthData(buf, E) {
|
|
|
134
124
|
try { keyNode = cbor.decode(buf.subarray(off), { allowTrailing: true }); }
|
|
135
125
|
catch (e) { throw E("webauthn/bad-cose-key", "the credential public key is not well-formed CBOR", e); }
|
|
136
126
|
out.credentialPublicKeyBytes = buf.subarray(off, off + keyNode.bytes.length);
|
|
137
|
-
out.credentialPublicKey = _decodeCoseKey(keyNode
|
|
127
|
+
out.credentialPublicKey = _decodeCoseKey(keyNode);
|
|
138
128
|
off += keyNode.bytes.length;
|
|
139
129
|
}
|
|
140
130
|
if (out.flags.ed) {
|
|
@@ -157,71 +147,10 @@ function _parseAuthData(buf, E) {
|
|
|
157
147
|
|
|
158
148
|
// ---- COSE_Key decode (WebAuthn sec. 6.5.1, RFC 9052 sec. 7) ------------------
|
|
159
149
|
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
// COSE OKP curve (label -1) -> its RFC 8410 named-key OID + fixed public-key length.
|
|
165
|
-
// Ed25519 (crv 6, 32-byte) and Ed448 (crv 7, 57-byte) both sign under COSE alg -8.
|
|
166
|
-
var OKP_CRV = { 6: { oid: "Ed25519", len: 32 }, 7: { oid: "Ed448", len: 57 } };
|
|
167
|
-
// The WebAuthn credential-key profile: a COSE signature algorithm (label 3) pins the
|
|
168
|
-
// key type (label 1) and, for EC2, the curve -- so an EC2 key claiming EdDSA, or an
|
|
169
|
-
// OKP key claiming ES256, is a profile violation (WebAuthn sec. 6.5.1, RFC 9052).
|
|
170
|
-
var ALG_PROFILE = {
|
|
171
|
-
"-7": { kty: 2, crv: 1 }, "-35": { kty: 2, crv: 2 }, "-36": { kty: 2, crv: 3 },
|
|
172
|
-
"-8": { kty: 1 },
|
|
173
|
-
"-257": { kty: 3 }, "-258": { kty: 3 }, "-259": { kty: 3 }, "-37": { kty: 3 }, "-65535": { kty: 3 },
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
// kty 1 / alg 3; EC2 (kty 2): crv -1, x -2, y -3; OKP (kty 1): crv -1, x -2;
|
|
177
|
-
// RSA (kty 3): n -1, e -2. Surface the raw coordinate/modulus buffers.
|
|
178
|
-
function _decodeCoseKey(node, E) {
|
|
179
|
-
if (!node || node.majorType !== 5) throw E("webauthn/bad-cose-key", "a COSE_Key must be a CBOR map (RFC 9052 sec. 7)");
|
|
180
|
-
// Every parameter read maps a wrong-type cbor/* fault to the webauthn domain -- a
|
|
181
|
-
// wrong-typed COSE label (x as an integer, kty as a string) is bad-cose-key input,
|
|
182
|
-
// not a leaked codec error.
|
|
183
|
-
function ib(label) { var n = _mapGetInt(node, label); if (!n) return null; try { return cbor.read.byteString(n); } catch (e) { throw E("webauthn/bad-cose-key", "COSE_Key parameter " + label + " must be a byte string", e); } }
|
|
184
|
-
function ii(label) { var n = _mapGetInt(node, label); if (!n) return null; try { return cbor.read.int(n); } catch (e) { throw E("webauthn/bad-cose-key", "COSE_Key parameter " + label + " must be an integer", e); } }
|
|
185
|
-
var ktyN = _mapGetInt(node, 1), algN = _mapGetInt(node, 3);
|
|
186
|
-
if (!ktyN) throw E("webauthn/bad-cose-key", "a COSE_Key is missing the kty (label 1) parameter");
|
|
187
|
-
// The credential public key MUST declare its algorithm (label 3): a relying party
|
|
188
|
-
// needs it to verify the later assertion signature (WebAuthn sec. 6.5.1, COSE sec. 7).
|
|
189
|
-
if (!algN) throw E("webauthn/bad-cose-key", "a COSE_Key is missing the alg (label 3) parameter");
|
|
190
|
-
var kty, algv;
|
|
191
|
-
try { kty = cbor.read.int(ktyN); algv = cbor.read.int(algN); }
|
|
192
|
-
catch (e) { throw E("webauthn/bad-cose-key", "COSE_Key kty (label 1) and alg (label 3) must be integers", e); }
|
|
193
|
-
var key = { kty: Number(kty), alg: Number(algv) };
|
|
194
|
-
// A credential key MUST carry every parameter its key type requires; an incomplete
|
|
195
|
-
// key (or an unknown kty) is rejected at decode rather than surfaced with null
|
|
196
|
-
// material a later binding check would have to defend against.
|
|
197
|
-
if (kty === 2n) {
|
|
198
|
-
key.crv = ii(-1) != null ? Number(ii(-1)) : null; key.x = ib(-2); key.y = ib(-3);
|
|
199
|
-
if (key.crv == null || !key.x || !key.y) throw E("webauthn/bad-cose-key", "an EC2 COSE_Key must carry crv (-1), x (-2), and y (-3)");
|
|
200
|
-
var el = EC2_CRV_LEN[key.crv];
|
|
201
|
-
if (!el || key.x.length !== el || key.y.length !== el) throw E("webauthn/bad-cose-key", "an EC2 COSE_Key x/y length is inconsistent with its curve");
|
|
202
|
-
} else if (kty === 1n) {
|
|
203
|
-
key.crv = ii(-1) != null ? Number(ii(-1)) : null; key.x = ib(-2);
|
|
204
|
-
var okp = OKP_CRV[key.crv];
|
|
205
|
-
if (!okp || !key.x || key.x.length !== okp.len) throw E("webauthn/bad-cose-key", "an OKP COSE_Key must be Ed25519 (crv 6) or Ed448 (crv 7) with a matching-length x (-2)");
|
|
206
|
-
} else if (kty === 3n) {
|
|
207
|
-
key.n = ib(-1); key.e = ib(-2);
|
|
208
|
-
if (!key.n || !key.n.length || !key.e || !key.e.length) throw E("webauthn/bad-cose-key", "an RSA COSE_Key must carry n (-1) and e (-2)");
|
|
209
|
-
} else {
|
|
210
|
-
throw E("webauthn/bad-cose-key", "unsupported COSE_Key kty " + Number(kty));
|
|
211
|
-
}
|
|
212
|
-
// A WebAuthn credential key is the CANONICAL CTAP2 COSE_Key for its type -- exactly
|
|
213
|
-
// its defined parameters, nothing more: EC2 = { 1, 3, -1, -2, -3 } (5), OKP/RSA = 4.
|
|
214
|
-
// Extra parameters are rejected (a padded key is a canonicalization ambiguity).
|
|
215
|
-
var expectedParams = kty === 2n ? 5 : 4;
|
|
216
|
-
if (node.children.length !== expectedParams) throw E("webauthn/bad-cose-key", "the COSE_Key carries parameters beyond the canonical set for its key type (WebAuthn sec. 6.5.1)");
|
|
217
|
-
// Enforce the alg <-> kty/crv profile: the declared algorithm must match the key
|
|
218
|
-
// type (and, for EC2, the curve) it is used with.
|
|
219
|
-
var prof = ALG_PROFILE[String(key.alg)];
|
|
220
|
-
if (!prof) throw E("webauthn/bad-cose-key", "unsupported credential key algorithm " + key.alg);
|
|
221
|
-
if (prof.kty !== key.kty) throw E("webauthn/bad-cose-key", "credential key algorithm " + key.alg + " is inconsistent with key type " + key.kty);
|
|
222
|
-
if (prof.crv != null && prof.crv !== key.crv) throw E("webauthn/bad-cose-key", "credential key algorithm " + key.alg + " requires a different curve");
|
|
223
|
-
return key;
|
|
224
|
-
}
|
|
150
|
+
// The complete COSE credential-key conformance rule set (kty/alg/crv/length/canonical/
|
|
151
|
+
// profile/on-curve) lives in validator-cose, composed here so every credential key
|
|
152
|
+
// routes through the one home -- never a per-format re-derivation of a partial subset.
|
|
153
|
+
function _decodeCoseKey(node) { return validator.cose.credentialKey(node, WebauthnError, "webauthn/bad-cose-key"); }
|
|
225
154
|
|
|
226
155
|
// ---- signature verification bridge ------------------------------------------
|
|
227
156
|
|
|
@@ -233,6 +162,15 @@ var COSE_ALG = {
|
|
|
233
162
|
"-35": { imp: { name: "ECDSA", namedCurve: "P-384" }, verify: { name: "ECDSA", hash: "SHA-384" }, ecdsa: 48 },
|
|
234
163
|
"-36": { imp: { name: "ECDSA", namedCurve: "P-521" }, verify: { name: "ECDSA", hash: "SHA-512" }, ecdsa: 66 },
|
|
235
164
|
"-8": { imp: { name: "Ed25519" }, verify: { name: "Ed25519" }, ecdsa: 0 },
|
|
165
|
+
// RFC 9864 fully-specified ids. WebAuthn recommends against them for credential creation,
|
|
166
|
+
// but a verifier MUST evaluate an assertion signed under one. ESP256/384/512 are the
|
|
167
|
+
// curve-pinned ECDSA twins of ES256/384/512; Ed25519(-19)/Ed448(-53) are fully-specified
|
|
168
|
+
// EdDSA (Ed448 is the ONLY WebAuthn path to Ed448 -- -8 is Ed25519 only).
|
|
169
|
+
"-9": { imp: { name: "ECDSA", namedCurve: "P-256" }, verify: { name: "ECDSA", hash: "SHA-256" }, ecdsa: 32 },
|
|
170
|
+
"-51": { imp: { name: "ECDSA", namedCurve: "P-384" }, verify: { name: "ECDSA", hash: "SHA-384" }, ecdsa: 48 },
|
|
171
|
+
"-52": { imp: { name: "ECDSA", namedCurve: "P-521" }, verify: { name: "ECDSA", hash: "SHA-512" }, ecdsa: 66 },
|
|
172
|
+
"-19": { imp: { name: "Ed25519" }, verify: { name: "Ed25519" }, ecdsa: 0 },
|
|
173
|
+
"-53": { imp: { name: "Ed448" }, verify: { name: "Ed448" }, ecdsa: 0 },
|
|
236
174
|
"-257": { imp: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, verify: { name: "RSASSA-PKCS1-v1_5" }, ecdsa: 0 },
|
|
237
175
|
"-258": { imp: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-384" }, verify: { name: "RSASSA-PKCS1-v1_5" }, ecdsa: 0 },
|
|
238
176
|
"-259": { imp: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-512" }, verify: { name: "RSASSA-PKCS1-v1_5" }, ecdsa: 0 },
|
|
@@ -244,33 +182,10 @@ var COSE_ALG = {
|
|
|
244
182
|
"-65535": { imp: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-1" }, verify: { name: "RSASSA-PKCS1-v1_5" }, ecdsa: 0 },
|
|
245
183
|
};
|
|
246
184
|
|
|
247
|
-
// DER ECDSA-Sig-Value
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
try { node = asn1.decode(der); } catch (e) { throw E("webauthn/bad-signature", "ECDSA signature is not a DER SEQUENCE", e); }
|
|
252
|
-
if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children || node.children.length !== 2) {
|
|
253
|
-
throw E("webauthn/bad-signature", "ECDSA signature must be a DER SEQUENCE { r, s }");
|
|
254
|
-
}
|
|
255
|
-
function coord(c) {
|
|
256
|
-
// r and s MUST be primitive universal INTEGERs; a constructed child has a null
|
|
257
|
-
// content buffer, so assert the shape before touching `.content` (else a crafted
|
|
258
|
-
// SEQUENCE { SEQUENCE {}, SEQUENCE {} } would raw-throw instead of failing typed).
|
|
259
|
-
if (c.constructed || c.tagClass !== "universal" || c.tagNumber !== asn1.TAGS.INTEGER) {
|
|
260
|
-
throw E("webauthn/bad-signature", "ECDSA signature r/s must be a primitive INTEGER");
|
|
261
|
-
}
|
|
262
|
-
var b = c.content;
|
|
263
|
-
// r and s MUST be positive: an empty INTEGER, or a DER-negative one (top bit of
|
|
264
|
-
// the first content octet set, with no leading 0x00 sign octet), is not a valid
|
|
265
|
-
// ECDSA signature coordinate.
|
|
266
|
-
if (!b.length || (b[0] & 0x80)) throw E("webauthn/bad-signature", "ECDSA signature r/s must be a positive integer");
|
|
267
|
-
while (b.length > 1 && b[0] === 0x00) b = b.subarray(1); // strip the DER sign octet
|
|
268
|
-
if (b.length === 1 && b[0] === 0x00) throw E("webauthn/bad-signature", "ECDSA signature r/s must be a positive integer"); // reject zero
|
|
269
|
-
if (b.length > coordLen) throw E("webauthn/bad-signature", "ECDSA signature coordinate exceeds the curve field size");
|
|
270
|
-
var out = Buffer.alloc(coordLen); b.copy(out, coordLen - b.length); return out;
|
|
271
|
-
}
|
|
272
|
-
return Buffer.concat([coord(node.children[0]), coord(node.children[1])]);
|
|
273
|
-
}
|
|
185
|
+
// DER ECDSA-Sig-Value { r, s } -> raw r||s (the ieee-p1363 form WebCrypto verify expects).
|
|
186
|
+
// validator-sig owns the complete strict-DER conformance (primitive, minimal, positive,
|
|
187
|
+
// bounded r/s), composed here.
|
|
188
|
+
function _derEcdsaToRaw(der, coordLen) { return validator.sig.ecdsaSigToRaw(der, coordLen, WebauthnError, "webauthn/bad-signature"); }
|
|
274
189
|
|
|
275
190
|
// Verify `sig` over `message` with the SPKI public key `spkiBytes` under COSE `alg`.
|
|
276
191
|
// A wrong signature resolves `false` from subtle.verify without throwing (a false
|
|
@@ -283,7 +198,12 @@ function _verifySig(alg, sig, spkiBytes, message, E) {
|
|
|
283
198
|
// COSE alg -8 (EdDSA) covers Ed25519 and Ed448; the WebCrypto name follows the
|
|
284
199
|
// signing key's own SPKI algorithm OID, not the (curve-agnostic) alg id.
|
|
285
200
|
if (alg === -8) { var nm = _edName(spkiBytes, E); imp = { name: nm }; ver = { name: nm }; }
|
|
286
|
-
|
|
201
|
+
// node/OpenSSL imports any Ed25519/Ed448 SPKI without validating the point, and a
|
|
202
|
+
// low-order (e.g. all-zeroes) key verifies a trivial signature -- so validate the OKP
|
|
203
|
+
// point before verify. This covers EVERY key that signs a WebAuthn statement: the x5c
|
|
204
|
+
// attestation-certificate key (packed/tpm/apple) AND the self-attestation credential key.
|
|
205
|
+
if (imp.name === "Ed25519" || imp.name === "Ed448") _requireValidEdPoint(spkiBytes, imp.name, E);
|
|
206
|
+
var s = d.ecdsa ? _derEcdsaToRaw(sig, d.ecdsa) : sig;
|
|
287
207
|
return subtle.importKey("spki", spkiBytes, imp, false, ["verify"])
|
|
288
208
|
.then(function (key) { return subtle.verify(ver, key, s, message); })
|
|
289
209
|
.catch(function (e) { throw _err("webauthn/verify-error", "the attestation signature could not be evaluated", e); });
|
|
@@ -300,35 +220,24 @@ function _edName(spkiBytes, E) {
|
|
|
300
220
|
if (!nm) throw E("webauthn/unsupported-algorithm", "unsupported EdDSA curve OID " + algOid);
|
|
301
221
|
return nm;
|
|
302
222
|
}
|
|
303
|
-
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
function
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
b.bitString(Buffer.concat([Buffer.from([0x04]), key.x, key.y])),
|
|
314
|
-
]);
|
|
315
|
-
}
|
|
316
|
-
if (key.kty === 3 && key.n && key.n.length && key.e && key.e.length) {
|
|
317
|
-
var bb = asn1.build;
|
|
318
|
-
return bb.sequence([
|
|
319
|
-
bb.sequence([bb.oid(oid.byName("rsaEncryption")), bb.nullValue()]),
|
|
320
|
-
bb.bitString(bb.sequence([bb.integer(BigInt("0x" + key.n.toString("hex"))), bb.integer(BigInt("0x" + key.e.toString("hex")))])),
|
|
321
|
-
]);
|
|
322
|
-
}
|
|
323
|
-
if (key.kty === 1 && OKP_CRV[key.crv] && key.x && key.x.length === OKP_CRV[key.crv].len) {
|
|
324
|
-
// OKP Ed25519 / Ed448: the SPKI is AlgorithmIdentifier { id-Ed* } (no parameters)
|
|
325
|
-
// over the raw public key -- RFC 8410.
|
|
326
|
-
var eb = asn1.build;
|
|
327
|
-
return eb.sequence([eb.sequence([eb.oid(oid.byName(OKP_CRV[key.crv].oid))]), eb.bitString(key.x)]);
|
|
223
|
+
// The raw Edwards point an OKP SPKI carries (its BIT STRING body, past the unused-bits
|
|
224
|
+
// octet) MUST be a valid, full-order point -- reject an off-curve or low-order key before it
|
|
225
|
+
// verifies a signature (WebCrypto import does not check it). Curve from the WebCrypto name.
|
|
226
|
+
function _requireValidEdPoint(spkiBytes, name, E) {
|
|
227
|
+
var content;
|
|
228
|
+
try { content = asn1.decode(spkiBytes).children[1].content; }
|
|
229
|
+
catch (e) { throw E("webauthn/bad-signature", "the EdDSA public key is not a well-formed SPKI", e); }
|
|
230
|
+
var point = content && content.length ? content.subarray(1) : Buffer.alloc(0);
|
|
231
|
+
if (!edwardsPoint.validate(point, name === "Ed25519" ? 6 : 7)) {
|
|
232
|
+
throw E("webauthn/bad-signature", "the EdDSA public key is not a valid, full-order Edwards point");
|
|
328
233
|
}
|
|
329
|
-
throw E("webauthn/bad-cose-key", "cannot build an SPKI for this COSE key type");
|
|
330
234
|
}
|
|
331
235
|
|
|
236
|
+
// A validated COSE credential key -> a self-contained SPKI the WebCrypto import
|
|
237
|
+
// consumes, so a credential public key and a certificate key verify by one path.
|
|
238
|
+
// validator-cose owns the encoding (EC2 / RSA / OKP), composed here.
|
|
239
|
+
function _coseKeyToSpki(key) { return validator.cose.toSpki(key, WebauthnError, "webauthn/bad-cose-key"); }
|
|
240
|
+
|
|
332
241
|
// The attestation-certificate subject public key MUST equal the credential public
|
|
333
242
|
// key that authenticatorData carries (WebAuthn 8.4/8.8 item 30). Compare the raw
|
|
334
243
|
// key material unsigned: an EC2 uncompressed point's X/Y vs the COSE x/y; an RSA
|
|
@@ -352,7 +261,7 @@ function _certPubKeyEqualsCose(cert, cose, E) {
|
|
|
352
261
|
if (cose.kty === 2) {
|
|
353
262
|
// The certificate's declared EC curve MUST equal the credential key's curve --
|
|
354
263
|
// a curve substitution is a different key even if the coordinate bytes line up.
|
|
355
|
-
var wantCurve = EC2_CRV_OID[cose.crv];
|
|
264
|
+
var wantCurve = validator.cose.EC2_CRV_OID[cose.crv];
|
|
356
265
|
if (!wantCurve) throw E("webauthn/key-mismatch", "unsupported credential EC curve " + cose.crv);
|
|
357
266
|
if (_certEcCurveOid(cert, E) !== oid.byName(wantCurve)) throw E("webauthn/key-mismatch", "the attestation certificate EC curve does not equal the credential key curve");
|
|
358
267
|
if (raw.length < 1 || raw[0] !== 0x04) throw E("webauthn/key-mismatch", "the attestation certificate key is not an uncompressed EC point");
|
|
@@ -382,71 +291,13 @@ function _certPubKeyEqualsCose(cert, cose, E) {
|
|
|
382
291
|
throw E("webauthn/key-mismatch", "unsupported credential key type for the certificate comparison");
|
|
383
292
|
}
|
|
384
293
|
|
|
385
|
-
// ---- TPM structure
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
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";
|
|
393
|
-
// TPM_ECC_CURVE -> COSE crv (the codepoints differ; this is a mapping, not equality).
|
|
394
|
-
var TPM_CURVE_TO_COSE = {}; TPM_CURVE_TO_COSE[0x0003] = 1; TPM_CURVE_TO_COSE[0x0004] = 2; TPM_CURVE_TO_COSE[0x0005] = 3;
|
|
395
|
-
|
|
396
|
-
function _tpmReader(buf) { return new ByteReader(buf, 0, buf.length, WebauthnError, "webauthn/bad-tpm"); }
|
|
397
|
-
|
|
398
|
-
// TPMT_PUBLIC (WebAuthn 8.3 item 17-20): decode type/nameAlg + the public key in
|
|
399
|
-
// `unique`, walking past `parameters` per the algorithm union.
|
|
400
|
-
function _parseTpmPubArea(buf) {
|
|
401
|
-
var r = _tpmReader(buf);
|
|
402
|
-
var type = r.u16(), nameAlg = r.u16();
|
|
403
|
-
r.u32(); // objectAttributes (full policy validation deferred)
|
|
404
|
-
r.vector(2, 0, null); // authPolicy TPM2B_DIGEST
|
|
405
|
-
var pub = { type: type, nameAlg: nameAlg, nameAlgBytes: buf.subarray(2, 4) };
|
|
406
|
-
if (type === TPM_ALG.RSA) {
|
|
407
|
-
_tpmSymDef(r); // symmetric TPMT_SYM_DEF_OBJECT
|
|
408
|
-
_tpmScheme(r); // scheme TPMT_RSA_SCHEME
|
|
409
|
-
r.u16(); // keyBits
|
|
410
|
-
var exp = r.u32(); // exponent (0 => default 65537)
|
|
411
|
-
pub.exponent = exp === 0 ? 65537 : exp;
|
|
412
|
-
pub.rsa = r.vector(2, 0, null); // unique TPM2B_PUBLIC_KEY_RSA (the modulus)
|
|
413
|
-
} else if (type === TPM_ALG.ECC) {
|
|
414
|
-
_tpmSymDef(r); // symmetric
|
|
415
|
-
_tpmScheme(r); // scheme TPMT_ECC_SCHEME
|
|
416
|
-
pub.curveId = r.u16(); // curveID TPMI_ECC_CURVE
|
|
417
|
-
_tpmScheme(r); // kdf TPMT_KDF_SCHEME
|
|
418
|
-
pub.x = r.vector(2, 0, null); // unique.ecc.x TPM2B_ECC_PARAMETER
|
|
419
|
-
pub.y = r.vector(2, 0, null); // unique.ecc.y TPM2B_ECC_PARAMETER
|
|
420
|
-
} else {
|
|
421
|
-
throw _err("webauthn/bad-tpm", "unsupported TPMT_PUBLIC type 0x" + type.toString(16));
|
|
422
|
-
}
|
|
423
|
-
// TPMT_PUBLIC ends with `unique`; trailing bytes mean a malformed pubArea (and
|
|
424
|
-
// would perturb the TPM Name hash), so fail closed rather than silently ignore them.
|
|
425
|
-
if (!r.atEnd()) throw _err("webauthn/bad-tpm", "pubArea has trailing bytes after the unique field (WebAuthn 8.3)");
|
|
426
|
-
return pub;
|
|
427
|
-
}
|
|
428
|
-
// TPMT_SYM_DEF_OBJECT: algorithm UINT16; when not NULL, keyBits+mode follow. An
|
|
429
|
-
// attestation (restricted signing) key is NULL, so the non-NULL arm is defensive.
|
|
430
|
-
function _tpmSymDef(r) { if (r.u16() !== TPM_ALG.NULL) { r.u16(); r.u16(); } }
|
|
431
|
-
// TPMT_*_SCHEME / TPMT_KDF_SCHEME: scheme UINT16; when not NULL, a TPMS_SCHEME_HASH
|
|
432
|
-
// (a single UINT16 hashAlg) follows for the signing/kdf schemes in scope.
|
|
433
|
-
function _tpmScheme(r) { if (r.u16() !== TPM_ALG.NULL) { r.u16(); } }
|
|
434
|
-
|
|
435
|
-
// TPMS_ATTEST (WebAuthn 8.3 item 13-15): magic/type + extraData + attested.name.
|
|
436
|
-
// The whole certInfo is walked -- through qualifiedName and to the end -- so a
|
|
437
|
-
// certInfo with trailing bytes past the TPMS_CERTIFY_INFO union fails closed.
|
|
438
|
-
function _parseTpmCertInfo(buf) {
|
|
439
|
-
var r = _tpmReader(buf);
|
|
440
|
-
var magic = r.u32(), type = r.u16();
|
|
441
|
-
r.vector(2, 0, null); // qualifiedSigner TPM2B_NAME
|
|
442
|
-
var extraData = r.vector(2, 0, null); // extraData TPM2B_DATA
|
|
443
|
-
r.u64(); r.u32(); r.u32(); r.u8(); // clockInfo TPMS_CLOCK_INFO (17 bytes)
|
|
444
|
-
r.u64(); // firmwareVersion
|
|
445
|
-
var name = r.vector(2, 0, null); // attested.name TPM2B_NAME (nameAlg||H)
|
|
446
|
-
r.vector(2, 0, null); // attested.qualifiedName TPM2B_NAME
|
|
447
|
-
if (!r.atEnd()) throw _err("webauthn/bad-tpm", "certInfo has trailing bytes after the attested structure (WebAuthn 8.3)");
|
|
448
|
-
return { magic: magic, type: type, extraData: extraData, attestedName: name };
|
|
449
|
-
}
|
|
294
|
+
// ---- TPM structure conformance (WebAuthn 8.3; TCG TPM 2.0 Part 2) -------------
|
|
295
|
+
// The TPMT_PUBLIC (pubArea) + TPMS_ATTEST (certInfo) decode + the pubArea-key binding
|
|
296
|
+
// live in validator-tpm, composed here. A malformed structure is bad-tpm; a key that
|
|
297
|
+
// does not equal the credential key is key-mismatch.
|
|
298
|
+
function _parseTpmPubArea(buf) { return validator.tpm.parsePubArea(buf, WebauthnError, "webauthn/bad-tpm"); }
|
|
299
|
+
function _parseTpmCertInfo(buf) { return validator.tpm.parseCertInfo(buf, WebauthnError, "webauthn/bad-tpm"); }
|
|
300
|
+
function _tpmPubKeyEqualsCose(pub, cose) { validator.tpm.pubKeyEqualsCose(pub, cose, WebauthnError, "webauthn/key-mismatch", "webauthn/bad-tpm"); }
|
|
450
301
|
|
|
451
302
|
// ---- extension helpers -------------------------------------------------------
|
|
452
303
|
// `oidName` is a registered name (byName resolves it at call time); an unregistered
|
|
@@ -455,17 +306,6 @@ function _findExt(cert, oidName) {
|
|
|
455
306
|
var target = oid.byName(oidName);
|
|
456
307
|
return (cert.extensions || []).filter(function (e) { return e.oid === target; })[0] || null;
|
|
457
308
|
}
|
|
458
|
-
// AuthorizationList (Android key attestation) is a SEQUENCE of [tag] EXPLICIT
|
|
459
|
-
// OPTIONAL fields with non-contiguous tag numbers; return the EXPLICIT-unwrapped
|
|
460
|
-
// value node for a tag, or null when absent (a tolerant, skip-unknown scan).
|
|
461
|
-
function _alGet(seqNode, tag) {
|
|
462
|
-
if (!seqNode || !seqNode.children) return null;
|
|
463
|
-
for (var i = 0; i < seqNode.children.length; i++) {
|
|
464
|
-
var c = seqNode.children[i];
|
|
465
|
-
if (c.tagClass === "context" && c.tagNumber === tag) return c.children && c.children[0] ? c.children[0] : c;
|
|
466
|
-
}
|
|
467
|
-
return null;
|
|
468
|
-
}
|
|
469
309
|
|
|
470
310
|
// ---- public: parseAttestationObject -----------------------------------------
|
|
471
311
|
|
|
@@ -548,50 +388,15 @@ function _readX5c(attStmt) {
|
|
|
548
388
|
try { return x509.parse(der); } catch (e) { throw _err("webauthn/bad-att-stmt", "an x5c certificate is not a well-formed X.509 certificate", e); }
|
|
549
389
|
});
|
|
550
390
|
}
|
|
551
|
-
//
|
|
552
|
-
//
|
|
553
|
-
//
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
// certificate under the caller's pinned root, or a leaf that simply omits the
|
|
561
|
-
// constraint, from being repurposed as an attestation leaf; RFC 5280 path validation
|
|
562
|
-
// does not reject a CA-asserting or unconstrained leaf.
|
|
563
|
-
function _assertNotCa(cert) {
|
|
564
|
-
var bc = _decodeExt(cert, "basicConstraints");
|
|
565
|
-
if (!bc) throw _err("webauthn/bad-att-cert", "an attestation leaf certificate MUST carry a basicConstraints extension (WebAuthn 8.2.1 / 8.3.1)");
|
|
566
|
-
if (bc.value && bc.value.cA === true) throw _err("webauthn/bad-att-cert", "an attestation leaf certificate MUST NOT be a CA (basicConstraints cA=true)");
|
|
567
|
-
}
|
|
568
|
-
// Packed attestation certificate requirements (WebAuthn 8.2.1): v3, non-CA, and the
|
|
569
|
-
// subject set to C (country), O (organization), OU = "Authenticator Attestation",
|
|
570
|
-
// and CN (a vendor string).
|
|
571
|
-
function _checkPackedCert(cert) {
|
|
572
|
-
_requireV3(cert);
|
|
573
|
-
_assertNotCa(cert);
|
|
574
|
-
var have = {};
|
|
575
|
-
(cert.subject.rdns || []).forEach(function (rdn) { rdn.forEach(function (a) { have[a.type] = a.value; }); });
|
|
576
|
-
if (have[oid.byName("countryName")] == null || have[oid.byName("organizationName")] == null || have[oid.byName("commonName")] == null) {
|
|
577
|
-
throw _err("webauthn/bad-att-cert", "the packed attestation certificate subject MUST set C, O, OU, and CN (WebAuthn 8.2.1)");
|
|
578
|
-
}
|
|
579
|
-
if (have[oid.byName("organizationalUnitName")] !== "Authenticator Attestation") {
|
|
580
|
-
throw _err("webauthn/bad-att-cert", "the packed attestation certificate subject OU MUST be \"Authenticator Attestation\" (WebAuthn 8.2.1)");
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
var AAGUID_EXT_OID = oid.byName("idFidoGenCeAaguid"); // a registered name, resolved at load
|
|
584
|
-
// The id-fido-gen-ce-aaguid extension value (when present) MUST equal the
|
|
585
|
-
// attestedCredentialData aaguid, and it MUST NOT be critical (WebAuthn 8.2.1).
|
|
586
|
-
function _checkAaguidExt(cert, aaguid) {
|
|
587
|
-
var ext = (cert.extensions || []).filter(function (e) { return e.oid === AAGUID_EXT_OID; })[0];
|
|
588
|
-
if (!ext) return; // absence is tolerated; presence must match
|
|
589
|
-
if (ext.critical) throw _err("webauthn/bad-att-cert", "the id-fido-gen-ce-aaguid extension MUST NOT be critical (WebAuthn 8.2.1)");
|
|
590
|
-
var val;
|
|
591
|
-
try { val = asn1.read.octetString(asn1.decode(ext.value)); }
|
|
592
|
-
catch (e) { throw _err("webauthn/bad-att-cert", "the id-fido-gen-ce-aaguid extension value is not a valid OCTET STRING", e); }
|
|
593
|
-
if (!val.equals(aaguid)) throw _err("webauthn/aaguid-mismatch", "the id-fido-gen-ce-aaguid extension value does not equal the authenticatorData aaguid");
|
|
594
|
-
}
|
|
391
|
+
// The WebAuthn attestation-certificate profile checks (WebAuthn 8.2.1 packed / 8.3.1 TPM
|
|
392
|
+
// AIK / the id-fido-gen-ce-aaguid extension) live in validator-attcert, composed here. The
|
|
393
|
+
// extension-accessor object hands the validator this format's fail-closed extension
|
|
394
|
+
// decoders so it stays decoupled from the webauthn error namespace.
|
|
395
|
+
var _exts = { find: _findExt, decode: _decodeExt };
|
|
396
|
+
function _requireV3(cert) { validator.attcert.requireV3(cert, WebauthnError, "webauthn/bad-att-cert"); }
|
|
397
|
+
function _checkPackedCert(cert) { validator.attcert.packedCert(cert, _exts, WebauthnError, "webauthn/bad-att-cert"); }
|
|
398
|
+
function _checkAikCert(cert) { validator.attcert.aikCert(cert, _exts, WebauthnError, "webauthn/bad-att-cert"); }
|
|
399
|
+
function _checkAaguidExt(cert, aaguid) { validator.attcert.aaguidExt(cert, aaguid, _exts, WebauthnError, "webauthn/bad-att-cert", "webauthn/aaguid-mismatch"); }
|
|
595
400
|
|
|
596
401
|
var VERIFIERS = {
|
|
597
402
|
// packed (WebAuthn 8.2): the x5c arm (Basic/AttCA) or self-attestation.
|
|
@@ -612,7 +417,7 @@ var VERIFIERS = {
|
|
|
612
417
|
// Self-attestation: the statement alg MUST equal the credential key's own alg
|
|
613
418
|
// (WebAuthn 8.2), then sig verifies under the credential key itself.
|
|
614
419
|
if (alg !== att.authData.credentialPublicKey.alg) throw _err("webauthn/bad-att-stmt", "the packed self-attestation alg does not match the credential public key algorithm (WebAuthn 8.2)");
|
|
615
|
-
var spki = _coseKeyToSpki(att.authData.credentialPublicKey
|
|
420
|
+
var spki = _coseKeyToSpki(att.authData.credentialPublicKey);
|
|
616
421
|
return _verifySig(alg, sig, spki, message, _err).then(function (ok) {
|
|
617
422
|
if (!ok) throw _err("webauthn/verify-failed", "the packed self-attestation signature does not verify under the credential key");
|
|
618
423
|
return _result("packed", "Self", [], att);
|
|
@@ -630,8 +435,10 @@ var VERIFIERS = {
|
|
|
630
435
|
var leaf = chain[0];
|
|
631
436
|
var sig = _sigOf(att.attStmt);
|
|
632
437
|
var key = att.authData.credentialPublicKey;
|
|
633
|
-
|
|
634
|
-
|
|
438
|
+
// WebAuthn 8.6: the fido-u2f credential public key MUST be alg -7 (ES256) on EC2 P-256 --
|
|
439
|
+
// the newer ESP256 (-9) id, though the same curve, is not a valid fido-u2f credential.
|
|
440
|
+
if (key.alg !== -7 || key.kty !== 2 || key.crv !== 1 || !key.x || !key.y || key.x.length !== 32 || key.y.length !== 32) {
|
|
441
|
+
throw _err("webauthn/bad-att-stmt", "fido-u2f requires an ES256 (-7) EC2 P-256 credential public key (WebAuthn 8.6)");
|
|
635
442
|
}
|
|
636
443
|
var publicKeyU2F = Buffer.concat([Buffer.from([0x04]), key.x, key.y]);
|
|
637
444
|
var verificationData = Buffer.concat([Buffer.from([0x00]), att.authData.rpIdHash, clientDataHash, att.authData.credentialId, publicKeyU2F]);
|
|
@@ -682,21 +489,20 @@ var VERIFIERS = {
|
|
|
682
489
|
var pubAreaBytes = _attRead(att.attStmt, "pubArea", cbor.read.byteString, "a byte string");
|
|
683
490
|
var certInfoBytes = _attRead(att.attStmt, "certInfo", cbor.read.byteString, "a byte string");
|
|
684
491
|
var chain = _readX5c(att.attStmt), aik = chain[0];
|
|
685
|
-
_requireV3(aik);
|
|
686
492
|
|
|
687
493
|
var pub = _parseTpmPubArea(pubAreaBytes);
|
|
688
494
|
_tpmPubKeyEqualsCose(pub, att.authData.credentialPublicKey); // 8.3 item 22
|
|
689
495
|
|
|
496
|
+
// validator-tpm decodes certInfo AND validates magic == TPM_GENERATED_VALUE / type ==
|
|
497
|
+
// TPM_ST_ATTEST_CERTIFY, so the returned structure is already self-consistent.
|
|
690
498
|
var certInfo = _parseTpmCertInfo(certInfoBytes);
|
|
691
|
-
if (certInfo.magic !== TPM_GENERATED_VALUE) throw _err("webauthn/bad-tpm", "certInfo magic is not TPM_GENERATED_VALUE (WebAuthn 8.3)");
|
|
692
|
-
if (certInfo.type !== TPM_ST_ATTEST_CERTIFY) throw _err("webauthn/bad-tpm", "certInfo type is not TPM_ST_ATTEST_CERTIFY (WebAuthn 8.3)");
|
|
693
499
|
|
|
694
500
|
// extraData == hash_alg(authData || clientDataHash) (bare digest, no method id).
|
|
695
501
|
var attToBeSigned = Buffer.concat([att.authDataBytes, clientDataHash]);
|
|
696
502
|
if (!certInfo.extraData.equals(_sha(_coseAlgHash(alg, _err), attToBeSigned))) throw _err("webauthn/verify-failed", "certInfo extraData does not equal the hash of authData || clientDataHash");
|
|
697
503
|
|
|
698
504
|
// attested.name == nameAlg || H_nameAlg(pubArea).
|
|
699
|
-
var nameHash = TPM_ALG_HASH[pub.nameAlg];
|
|
505
|
+
var nameHash = validator.tpm.TPM_ALG_HASH[pub.nameAlg];
|
|
700
506
|
if (!nameHash) throw _err("webauthn/bad-tpm", "unsupported TPM nameAlg 0x" + pub.nameAlg.toString(16));
|
|
701
507
|
var computedName = Buffer.concat([pub.nameAlgBytes, _sha(nameHash, pubAreaBytes)]);
|
|
702
508
|
if (!certInfo.attestedName.equals(computedName)) throw _err("webauthn/verify-failed", "certInfo attested Name does not match the pubArea TPM Name");
|
|
@@ -736,27 +542,6 @@ function _result(fmt, attestationType, chain, att) {
|
|
|
736
542
|
return { verified: true, fmt: fmt, attestationType: attestationType, trustPath: (chain || []).slice().reverse(), aaguid: att.authData.aaguid, credentialPublicKey: att.authData.credentialPublicKey };
|
|
737
543
|
}
|
|
738
544
|
|
|
739
|
-
// pubArea public key == credentialPublicKey (unsigned compare; WebAuthn 8.3 item 22).
|
|
740
|
-
function _tpmPubKeyEqualsCose(pub, cose) {
|
|
741
|
-
if (pub.type === TPM_ALG.ECC) {
|
|
742
|
-
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))) {
|
|
743
|
-
throw _err("webauthn/key-mismatch", "the TPM pubArea EC key does not equal the credential public key");
|
|
744
|
-
}
|
|
745
|
-
return;
|
|
746
|
-
}
|
|
747
|
-
if (pub.type === TPM_ALG.RSA) {
|
|
748
|
-
// Compare the exponent as an unsigned integer over its FULL width (a UINT32 up to
|
|
749
|
-
// 0xFFFFFFFF); a fixed 3-byte re-encode would silently truncate an exponent
|
|
750
|
-
// > 0xFFFFFF and let a mismatched key pass (WebAuthn 8.3 item 22).
|
|
751
|
-
var e = _uintBytes(pub.exponent >>> 0);
|
|
752
|
-
if (cose.kty !== 3 || !_ucmp(pub.rsa, cose.n || Buffer.alloc(0)) || !_ucmp(e, cose.e || Buffer.alloc(0))) {
|
|
753
|
-
throw _err("webauthn/key-mismatch", "the TPM pubArea RSA key does not equal the credential public key");
|
|
754
|
-
}
|
|
755
|
-
return;
|
|
756
|
-
}
|
|
757
|
-
throw _err("webauthn/bad-tpm", "unsupported TPM pubArea key type");
|
|
758
|
-
}
|
|
759
|
-
|
|
760
545
|
// Decode the Apple extension AppleAnonymousAttestation ::= SEQUENCE { nonce [1]
|
|
761
546
|
// EXPLICIT OCTET STRING } (WebAuthn 8.8 item 29) and return the 32-byte nonce.
|
|
762
547
|
function _appleNonce(extValue) {
|
|
@@ -769,73 +554,13 @@ function _appleNonce(extValue) {
|
|
|
769
554
|
try { return asn1.read.octetString(tagged.children[0]); } catch (e) { throw _err("webauthn/bad-att-cert", "the apple attestation nonce is not an OCTET STRING", e); }
|
|
770
555
|
}
|
|
771
556
|
|
|
772
|
-
// The
|
|
557
|
+
// The complete Android Key Attestation KeyDescription conformance (AOSP schema + WebAuthn
|
|
558
|
+
// 8.4.1) lives in validator-keydesc, composed here. A structural fault is bad-att-cert; a
|
|
559
|
+
// well-formed description that fails an 8.4.1 MUST is verify-failed.
|
|
773
560
|
function _checkAndroidKeyDescription(cert, clientDataHash) {
|
|
774
|
-
|
|
775
|
-
if (!ext) throw _err("webauthn/bad-att-cert", "the android-key attestation certificate is missing the key-description extension (WebAuthn 8.4.1)");
|
|
776
|
-
var kd;
|
|
777
|
-
try { kd = asn1.decode(ext.value); } catch (e) { throw _err("webauthn/bad-att-cert", "the android KeyDescription is not decodable", e); }
|
|
778
|
-
if (!kd.children || kd.children.length < 8) throw _err("webauthn/bad-att-cert", "the android KeyDescription is not a positional 8-field SEQUENCE");
|
|
779
|
-
// (1) attestationChallenge (position 4) == clientDataHash.
|
|
780
|
-
var challenge;
|
|
781
|
-
try { challenge = asn1.read.octetString(kd.children[4]); } catch (e) { throw _err("webauthn/bad-att-cert", "the android attestationChallenge is not an OCTET STRING", e); }
|
|
782
|
-
if (!challenge.equals(clientDataHash)) throw _err("webauthn/verify-failed", "the android attestationChallenge does not equal clientDataHash");
|
|
783
|
-
var softwareEnforced = kd.children[6], hardwareEnforced = kd.children[7];
|
|
784
|
-
// (2) [600] allApplications MUST be ABSENT in both lists.
|
|
785
|
-
if (_alGet(softwareEnforced, 600) || _alGet(hardwareEnforced, 600)) throw _err("webauthn/verify-failed", "android allApplications MUST be absent (WebAuthn 8.4.1)");
|
|
786
|
-
// (3)+(4) origin == KM_ORIGIN_GENERATED (0) and purpose == exactly {KM_PURPOSE_SIGN}
|
|
787
|
-
// (2), evaluated over the UNION of the teeEnforced and softwareEnforced lists --
|
|
788
|
-
// the WebAuthn 8.4.1 default (a teeEnforced-only policy is the caller's opt-in).
|
|
789
|
-
// Origin: at least one list MUST declare it, and EVERY list that declares one MUST
|
|
790
|
-
// say GENERATED -- a mixed teeEnforced=IMPORTED / softwareEnforced=GENERATED key is
|
|
791
|
-
// contradictory and rejected, not accepted on the strength of one list.
|
|
792
|
-
var origins = [_alGet(softwareEnforced, 702), _alGet(hardwareEnforced, 702)].filter(Boolean);
|
|
793
|
-
if (!origins.length || !origins.every(function (o) { return _alInt(o) === 0n; })) {
|
|
794
|
-
throw _err("webauthn/verify-failed", "android key origin is not KM_ORIGIN_GENERATED in every authorization list that declares it (WebAuthn 8.4.1)");
|
|
795
|
-
}
|
|
796
|
-
var purposes = _purposeUnion(softwareEnforced, hardwareEnforced);
|
|
797
|
-
if (purposes.length !== 1 || purposes[0] !== 2n) {
|
|
798
|
-
throw _err("webauthn/verify-failed", "android key purpose is not exactly KM_PURPOSE_SIGN (WebAuthn 8.4.1)");
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
// Read an android KeyDescription authorization INTEGER, mapping a wrong-type asn1/*
|
|
802
|
-
// fault to the webauthn domain (a malformed authorization value is bad-att-cert input
|
|
803
|
-
// to this layer, not a leaked codec error).
|
|
804
|
-
function _alInt(node) {
|
|
805
|
-
try { return asn1.read.integer(node); }
|
|
806
|
-
catch (e) { throw _err("webauthn/bad-att-cert", "an android KeyDescription authorization value is not an INTEGER", e); }
|
|
807
|
-
}
|
|
808
|
-
// The union of the [1] purpose (SET OF INTEGER) values across both authorization
|
|
809
|
-
// lists -- an attestation key MUST be usable for SIGN and nothing else.
|
|
810
|
-
function _purposeUnion(a, b) {
|
|
811
|
-
var out = [];
|
|
812
|
-
[a, b].forEach(function (list) {
|
|
813
|
-
var p = _alGet(list, 1);
|
|
814
|
-
if (p && p.children) p.children.forEach(function (c) { var v = _alInt(c); if (out.indexOf(v) === -1) out.push(v); });
|
|
815
|
-
});
|
|
816
|
-
return out;
|
|
561
|
+
validator.keydesc.androidKeyDescription(cert, clientDataHash, _exts, WebauthnError, "webauthn/bad-att-cert", "webauthn/verify-failed");
|
|
817
562
|
}
|
|
818
563
|
|
|
819
|
-
// AIK-certificate requirements (WebAuthn 8.3.1): non-CA, an empty subject, the tcg
|
|
820
|
-
// AIK-certificate EKU, and the tcg tpmManufacturer/tpmModel/tpmVersion subjectAltName
|
|
821
|
-
// directory attributes.
|
|
822
|
-
function _checkAikCert(cert) {
|
|
823
|
-
_assertNotCa(cert);
|
|
824
|
-
if ((cert.subject.rdns || []).length !== 0) throw _err("webauthn/bad-att-cert", "the tpm AIK certificate subject MUST be empty (WebAuthn 8.3.1)");
|
|
825
|
-
var eku = _decodeExt(cert, "extKeyUsage");
|
|
826
|
-
if (!eku || !Array.isArray(eku.value) || eku.value.indexOf(oid.byName("tcgKpAikCertificate")) === -1) {
|
|
827
|
-
throw _err("webauthn/bad-att-cert", "the tpm AIK certificate lacks the tcg-kp-AIKCertificate extended key purpose (WebAuthn 8.3.1)");
|
|
828
|
-
}
|
|
829
|
-
var san = _decodeExt(cert, "subjectAltName");
|
|
830
|
-
var dirName = san && san.value && san.value.names && san.value.names.filter(function (n) { return n.tagNumber === 4; })[0];
|
|
831
|
-
var types = {};
|
|
832
|
-
if (dirName && dirName.value && dirName.value.rdns) {
|
|
833
|
-
dirName.value.rdns.forEach(function (rdn) { rdn.forEach(function (a) { types[a.type] = true; }); });
|
|
834
|
-
}
|
|
835
|
-
if (!types[oid.byName("tpmManufacturer")] || !types[oid.byName("tpmModel")] || !types[oid.byName("tpmVersion")]) {
|
|
836
|
-
throw _err("webauthn/bad-att-cert", "the tpm AIK certificate subjectAltName lacks the required tcg attributes (WebAuthn 8.3.1)");
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
564
|
|
|
840
565
|
/**
|
|
841
566
|
* @primitive pki.webauthn.verify
|
package/lib/webcrypto.js
CHANGED
|
@@ -658,7 +658,11 @@ function _algFromImport(name, alg, keyObject) {
|
|
|
658
658
|
}
|
|
659
659
|
|
|
660
660
|
function _curveFromKey(ko) {
|
|
661
|
-
try {
|
|
661
|
+
try {
|
|
662
|
+
var jwk = ko.export({ format: "jwk" });
|
|
663
|
+
for (var k in CURVE_NODE) { if (jwk.crv === k) return k; }
|
|
664
|
+
}
|
|
665
|
+
catch (_e) { /* best-effort */ }
|
|
662
666
|
return undefined;
|
|
663
667
|
}
|
|
664
668
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blamejs/pki",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "blamejs contributors",
|
|
@@ -66,6 +66,8 @@
|
|
|
66
66
|
"fuzz": "npm ci --prefix fuzz && npx --prefix fuzz jazzer fuzz/asn1-der.fuzz.js -- -max_total_time=60",
|
|
67
67
|
"gates": "node test/layer-0-primitives/codebase-patterns.test.js && node scripts/validate-source-comment-blocks.js && node scripts/check-api-snapshot.js",
|
|
68
68
|
"coverage": "c8 --include=lib/** --include=index.js --reporter=text-summary --reporter=lcov node test/smoke.js",
|
|
69
|
+
"check:swallows": "node scripts/check-swallow-coverage.js",
|
|
70
|
+
"coverage:gated": "npm run coverage && npm run check:swallows",
|
|
69
71
|
"prepack": "node scripts/check-pack-against-gitignore.js",
|
|
70
72
|
"check:vendor-currency": "node scripts/check-vendor-currency.js"
|
|
71
73
|
},
|