diff options
Diffstat (limited to 'testing/web-platform/tests/webauthn')
42 files changed, 3720 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webauthn/META.yml b/testing/web-platform/tests/webauthn/META.yml new file mode 100644 index 0000000000..a52d978f0e --- /dev/null +++ b/testing/web-platform/tests/webauthn/META.yml @@ -0,0 +1,5 @@ +spec: https://w3c.github.io/webauthn/ +suggested_reviewers: + - apowers313 + - jcjones + - nsatragno diff --git a/testing/web-platform/tests/webauthn/conditional-mediation.https.html b/testing/web-platform/tests/webauthn/conditional-mediation.https.html new file mode 100644 index 0000000000..0bec08ce45 --- /dev/null +++ b/testing/web-platform/tests/webauthn/conditional-mediation.https.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Conditional Mediation tests</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src=helpers.js></script> +<body></body> +<script> +"use strict"; + +// Test that a configuration that must support conditional mediation reports +// supporting it. +virtualAuthenticatorPromiseTest(async t => { + assert_own_property(window.PublicKeyCredential, "isConditionalMediationAvailable"); + assert_true(await window.PublicKeyCredential.isConditionalMediationAvailable()); +}, { + protocol: "ctap2", + hasResidentKey: true, + hasUserVerification: true, + transport: "internal", +}, "Conditional mediation supported"); + +// Test that a configuration that cannot possibly support conditional mediation +// does not report supporting it. +virtualAuthenticatorPromiseTest(async t => { + assert_own_property(window.PublicKeyCredential, "isConditionalMediationAvailable"); + assert_false(await window.PublicKeyCredential.isConditionalMediationAvailable()); +}, { + protocol: "ctap2", + hasResidentKey: false, + hasUserVerification: false, + transport: "nfc", +}, "Conditional mediation not supported"); + +</script> diff --git a/testing/web-platform/tests/webauthn/createcredential-abort.https.html b/testing/web-platform/tests/webauthn/createcredential-abort.https.html new file mode 100644 index 0000000000..d175e660e7 --- /dev/null +++ b/testing/web-platform/tests/webauthn/createcredential-abort.https.html @@ -0,0 +1,109 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>WebAuthn navigator.credentials.create() abort Tests</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src=helpers.js></script> +<body></body> +<script> +"use strict"; + +virtualAuthenticatorPromiseTest(async t => { + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + const promise = createCredential({ + options: { + signal: signal, + } + }); + return promise_rejects_dom(t, "AbortError", promise); +}, { + protocol: "ctap1/u2f", + transport: "usb", + isUserConsenting: false, +}, "navigator.credentials.create() after abort without reason"); + +virtualAuthenticatorPromiseTest(async t => { + const abortController = new AbortController(); + const signal = abortController.signal; + const promise = createCredential({ + options: { + signal: signal, + } + }); + abortController.abort(); + return promise_rejects_dom(t, "AbortError", promise); +}, { + protocol: "ctap1/u2f", + transport: "usb", + isUserConsenting: false, +}, "navigator.credentials.create() before abort without reason"); + +virtualAuthenticatorPromiseTest(async t => { + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort("CustomError"); + const promise = createCredential({ + options: { + signal: signal, + } + }); + return promise_rejects_exactly(t, "CustomError", promise); +}, { + protocol: "ctap1/u2f", + transport: "usb", + isUserConsenting: false, +}, "navigator.credentials.create() after abort reason"); + +virtualAuthenticatorPromiseTest(async t => { + const abortController = new AbortController(); + const signal = abortController.signal; + const promise = createCredential({ + options: { + signal: signal, + } + }); + abortController.abort("CustomError"); + return promise_rejects_exactly(t, "CustomError", promise); +}, { + protocol: "ctap1/u2f", + transport: "usb", + isUserConsenting: false, +}, "navigator.credentials.create() before abort reason"); + +virtualAuthenticatorPromiseTest(async t => { + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(new Error('error')); + const promise = createCredential({ + options: { + signal: signal, + } + }); + return promise_rejects_js(t, Error, promise); +}, { + protocol: "ctap1/u2f", + transport: "usb", + isUserConsenting: false, +}, "navigator.credentials.create() after abort reason with Error"); + +virtualAuthenticatorPromiseTest(async t => { + const abortController = new AbortController(); + const signal = abortController.signal; + const promise = createCredential({ + options: { + signal: signal, + } + }); + abortController.abort(new Error('error')); + return promise_rejects_js(t, Error, promise); +}, { + protocol: "ctap1/u2f", + transport: "usb", + isUserConsenting: false, +}, "navigator.credentials.create() before abort reason with Error"); +</script> diff --git a/testing/web-platform/tests/webauthn/createcredential-attachment.https.html b/testing/web-platform/tests/webauthn/createcredential-attachment.https.html new file mode 100644 index 0000000000..e9458ad560 --- /dev/null +++ b/testing/web-platform/tests/webauthn/createcredential-attachment.https.html @@ -0,0 +1,79 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src=helpers.js></script> +<body></body> +<script> + "use strict"; + // usb transport + virtualAuthenticatorPromiseTest(async function() { + const credential = await createCredential({ + options: { + publicKey: { + authenticatorSelection: { + userVerification: "preferred" + }, + }, + }, + }); + assert_equals(credential.authenticatorAttachment, "cross-platform"); + }, { + protocol: "ctap2", + transport: "usb" + }, "navigator.credentials.create() with usb authenticator, attachment as cross-platform"); + + // ble transport + virtualAuthenticatorPromiseTest(async function() { + const credential = await createCredential({ + options: { + publicKey: { + authenticatorSelection: { + userVerification: "preferred" + }, + }, + }, + }); + assert_equals(credential.authenticatorAttachment, "cross-platform"); + }, { + protocol: "ctap2", + transport: "ble" + }, "navigator.credentials.create() with ble authenticator, attachment as cross-platform"); + + // nfc transport + virtualAuthenticatorPromiseTest(async function() { + const credential = await createCredential({ + options: { + publicKey: { + authenticatorSelection: { + userVerification: "preferred" + }, + }, + }, + }); + assert_equals(credential.authenticatorAttachment, "cross-platform"); + }, { + protocol: "ctap2", + transport: "nfc" + }, "navigator.credentials.create() with nfc authenticator, attachment as cross-platform"); + + // internal transport + virtualAuthenticatorPromiseTest(async function() { + const credential = await createCredential({ + options: { + publicKey: { + authenticatorSelection: { + userVerification: "preferred" + }, + }, + }, + }); + assert_equals(credential.authenticatorAttachment, "platform"); + }, { + protocol: "ctap2", + transport: "internal" + }, "navigator.credentials.create() with internal authenticator, attachment as platform"); +</script> diff --git a/testing/web-platform/tests/webauthn/createcredential-badargs-authnrselection.https.html b/testing/web-platform/tests/webauthn/createcredential-badargs-authnrselection.https.html new file mode 100644 index 0000000000..85b0f90538 --- /dev/null +++ b/testing/web-platform/tests/webauthn/createcredential-badargs-authnrselection.https.html @@ -0,0 +1,60 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>WebAuthn navigator.credentials.create() authenticator selection Tests</title> +<meta name="timeout" content="long"> +<link rel="author" title="Adam Powers" href="mailto:adam@fidoalliance.org"> +<link rel="help" href="https://w3c.github.io/webauthn/#iface-credential"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src=helpers.js></script> +<body></body> +<script> +standardSetup(function() { + "use strict"; + + var defaultAuthnrSel = { + authenticatorAttachment: "cross-platform", + requireResidentKey: false, + userVerification: "preferred" + }; + // attachment + var authnrSelAttachPlatform = cloneObject(defaultAuthnrSel); + authnrSelAttachPlatform.authenticatorAttachment = "platform"; + // resident key + var authnrSelRkTrue = cloneObject(defaultAuthnrSel); + authnrSelRkTrue.requireResidentKey = true; + var authnrSelRkBadString = cloneObject(defaultAuthnrSel); + authnrSelRkBadString.requireResidentKey = "foo"; + // user verification + var authnrSelUvRequired = cloneObject(defaultAuthnrSel); + authnrSelUvRequired.userVerification = "required"; + + // authenticatorSelection bad values + new CreateCredentialsTest("options.publicKey.authenticatorSelection", "").runTest("Bad AuthenticatorSelectionCriteria: authenticatorSelection is empty string", TypeError); + new CreateCredentialsTest("options.publicKey.authenticatorSelection", "none").runTest("Bad AuthenticatorSelectionCriteria: authenticatorSelection is string", TypeError); + + // authenticatorSelection bad attachment values + // the physically plugged-in or virtual authenticator should be a cross-platform authenticator. + new CreateCredentialsTest("options.publicKey.authenticatorSelection", authnrSelAttachPlatform) + .modify("options.publicKey.timeout", 300) + .runTest("Bad AuthenticatorSelectionCriteria: authenticatorSelection attachment platform", "NotAllowedError"); + + // authenticatorSelection bad requireResidentKey values + // the physically plugged-in or virtual authenticator should not support resident keys + new CreateCredentialsTest("options.publicKey.authenticatorSelection", authnrSelRkTrue) + .modify("options.publicKey.timeout", 300) + .runTest("Bad AuthenticatorSelectionCriteria: authenticatorSelection residentKey true", "NotAllowedError"); + + // authenticatorSelection bad userVerification values + // the physically plugged-in or virtual authenticator should not support user verification + new CreateCredentialsTest("options.publicKey.authenticatorSelection", authnrSelUvRequired) + // this assertion will time out the test under default parameters since the browser will wait for a platform authenticator + .modify("options.publicKey.timeout", 300) + .runTest("Bad AuthenticatorSelectionCriteria: authenticatorSelection userVerification required", "NotAllowedError"); +}); + +/* JSHINT */ +/* globals standardSetup, CreateCredentialsTest, cloneObject */ +</script> diff --git a/testing/web-platform/tests/webauthn/createcredential-badargs-challenge.https.html b/testing/web-platform/tests/webauthn/createcredential-badargs-challenge.https.html new file mode 100644 index 0000000000..4fa41df11d --- /dev/null +++ b/testing/web-platform/tests/webauthn/createcredential-badargs-challenge.https.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>WebAuthn navigator.credentials.create() challenge Tests</title> +<meta name="timeout" content="long"> +<link rel="author" title="Adam Powers" href="mailto:adam@fidoalliance.org"> +<link rel="help" href="https://w3c.github.io/webauthn/#iface-credential"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src=helpers.js></script> +<body></body> +<script> +standardSetup(function() { + "use strict"; + + // bad challenge values + new CreateCredentialsTest({path: "options.publicKey.challenge", value: undefined}).runTest("Bad challenge: challenge missing", TypeError); + new CreateCredentialsTest("options.publicKey.challenge", "hi mom").runTest("Bad challenge: challenge is string", TypeError); + new CreateCredentialsTest("options.publicKey.challenge", null).runTest("Bad challenge: challenge is null", TypeError); + new CreateCredentialsTest("options.publicKey.challenge", {}).runTest("Bad challenge: challenge is empty object", TypeError); + new CreateCredentialsTest("options.publicKey.challenge", new Array()).runTest("Bad challenge: challenge is empty Array", TypeError); +}); + +/* JSHINT */ +/* globals standardSetup, CreateCredentialsTest */ +</script> diff --git a/testing/web-platform/tests/webauthn/createcredential-badargs-rp.https.html b/testing/web-platform/tests/webauthn/createcredential-badargs-rp.https.html new file mode 100644 index 0000000000..5e3c38821b --- /dev/null +++ b/testing/web-platform/tests/webauthn/createcredential-badargs-rp.https.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>WebAuthn navigator.credentials.create() rp Tests</title> +<meta name="timeout" content="long"> +<link rel="author" title="Adam Powers" href="mailto:adam@fidoalliance.org"> +<link rel="help" href="https://w3c.github.io/webauthn/#iface-credential"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src=helpers.js></script> +<body></body> +<script> +standardSetup(function() { + "use strict"; + + // rp bad values + new CreateCredentialsTest({path: "options.publicKey.rp", value: undefined}).runTest("Bad rp: rp missing", TypeError); + new CreateCredentialsTest({ path: "options.publicKey.rp", value: null }).runTest("Bad rp: rp null", TypeError); + new CreateCredentialsTest("options.publicKey.rp", "hi mom").runTest("Bad rp: rp is string", TypeError); + new CreateCredentialsTest("options.publicKey.rp", {}).runTest("Bad rp: rp is empty object", TypeError); + + // // rp.id + new CreateCredentialsTest("options.publicKey.rp.id", null).runTest("Bad rp: id is null", "SecurityError"); + new CreateCredentialsTest("options.publicKey.rp.id", "").runTest("Bad rp: id is empty String", "SecurityError"); + new CreateCredentialsTest("options.publicKey.rp.id", "invalid domain.com").runTest("Bad rp: id is invalid domain (has space)", "SecurityError"); + new CreateCredentialsTest("options.publicKey.rp.id", "-invaliddomain.com").runTest("Bad rp: id is invalid domain (starts with dash)", "SecurityError"); + new CreateCredentialsTest("options.publicKey.rp.id", "0invaliddomain.com").runTest("Bad rp: id is invalid domain (starts with number)", "SecurityError"); + + let hostAndPort = window.location.host; + if (!hostAndPort.match(/:\d+$/)) { + hostAndPort += ":443"; + } + new CreateCredentialsTest("options.publicKey.rp.id", hostAndPort).runTest("Bad rp id: id is host + port", "SecurityError"); + + // // rp.name + new CreateCredentialsTest({path: "options.publicKey.rp.name", value: undefined}).runTest("rp missing name", TypeError); +}); + +/* JSHINT */ +/* globals standardSetup, CreateCredentialsTest */ +</script> diff --git a/testing/web-platform/tests/webauthn/createcredential-badargs-user.https.html b/testing/web-platform/tests/webauthn/createcredential-badargs-user.https.html new file mode 100644 index 0000000000..e487242571 --- /dev/null +++ b/testing/web-platform/tests/webauthn/createcredential-badargs-user.https.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>WebAuthn navigator.credentials.create() user Tests</title> +<meta name="timeout" content="long"> +<link rel="author" title="Adam Powers" href="mailto:adam@fidoalliance.org"> +<link rel="help" href="https://w3c.github.io/webauthn/#iface-credential"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src=helpers.js></script> +<body></body> +<script> +standardSetup(function() { + "use strict"; + + // user bad values + new CreateCredentialsTest({path: "options.publicKey.user", value: undefined}).runTest("Bad user: user missing", TypeError); + new CreateCredentialsTest("options.publicKey.user", "hi mom").runTest("Bad user: user is string", TypeError); + new CreateCredentialsTest("options.publicKey.user", {}).runTest("Bad user: user is empty object", TypeError); + + // // user.id + new CreateCredentialsTest({path: "options.publicKey.user.id", value: undefined}).runTest("Bad user: id is undefined", TypeError); + new CreateCredentialsTest("options.publicKey.user.id", {}).runTest("Bad user: id is object", TypeError); + new CreateCredentialsTest("options.publicKey.user.id", null).runTest("Bad user: id is null", TypeError); + new CreateCredentialsTest("options.publicKey.user.id", "").runTest("Bad user: id is empty String", TypeError); + new CreateCredentialsTest("options.publicKey.user.id", new Array()).runTest("Bad user: id is empty Array", TypeError); + new CreateCredentialsTest("options.publicKey.user.id", new ArrayBuffer(65)).runTest("Bad user: ArrayBuffer id is too long (65 bytes)", TypeError); + new CreateCredentialsTest("options.publicKey.user.id", new Int16Array(33)).runTest("Bad user: Int16Array id is too long (66 bytes)", TypeError); + new CreateCredentialsTest("options.publicKey.user.id", new Int32Array(17)).runTest("Bad user: Int32Array id is too long (68 bytes)", TypeError); + new CreateCredentialsTest("options.publicKey.user.id", new Float32Array(17)).runTest("Bad user: Float32Array id is too long (68 bytes)", TypeError); + new CreateCredentialsTest("options.publicKey.user.id", new Float64Array(9)).runTest("Bad user: Float64Array id is too long (72 bytes)", TypeError); + var buf = new ArrayBuffer(65); + new CreateCredentialsTest("options.publicKey.user.id", new DataView(buf)).runTest("Bad user: id is too long (65 bytes)", TypeError); + + // // user.name + new CreateCredentialsTest({path: "options.publicKey.user.name", value: undefined}).runTest("user missing name", TypeError); + + // // user.displayName + new CreateCredentialsTest({path: "options.publicKey.user.displayName", value: undefined}).runTest("Bad user: displayName is undefined", TypeError); +}); + +/* JSHINT */ +/* globals standardSetup, CreateCredentialsTest */ +</script> diff --git a/testing/web-platform/tests/webauthn/createcredential-excludecredentials.https.html b/testing/web-platform/tests/webauthn/createcredential-excludecredentials.https.html new file mode 100644 index 0000000000..2b1eec19b7 --- /dev/null +++ b/testing/web-platform/tests/webauthn/createcredential-excludecredentials.https.html @@ -0,0 +1,82 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>WebAuthn navigator.credentials.create() excludeCredentials Tests</title> +<meta name="timeout" content="long"> +<link rel="author" title="Adam Powers" href="mailto:adam@fidoalliance.org"> +<link rel="help" href="https://w3c.github.io/webauthn/#iface-credential"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src=helpers.js></script> +<body></body> +<script> +standardSetup(function() { + "use strict"; + + // bad excludeCredentials values + new CreateCredentialsTest("options.publicKey.excludeCredentials", "hi mom").runTest("Bad excludeCredentials: string", TypeError); + new CreateCredentialsTest("options.publicKey.excludeCredentials", {}).runTest("Bad excludeCredentials: empty object", TypeError); + // TODO: bad excludeCredentials with [{.type}] or [{.id}] or [{.transports}] wrong + + // good excludeCredentials values + new CreateCredentialsTest({path: "options.publicKey.excludeCredentials", value: undefined}).runTest("excludeCredentials missing"); + new CreateCredentialsTest("options.publicKey.excludeCredentials", []).runTest("excludeCredentials empty array"); + + // proper excludeCredentials behavior + // should error on excluding existing credential + promise_test((t) => { + var cred1; + return Promise.resolve() + .then(() => { + return createCredential(); + }) + .then((cred) => { + cred1 = cred; + var excludeCred = { + id: cred.rawId, + type: "public-key" + }; + var args = { + options: { + publicKey: { + excludeCredentials: [excludeCred] + } + } + }; + var p = createCredential(args); + return promise_rejects_dom(t, "InvalidStateError", p, "expected to fail on excluded credential"); + }); + }, "exclude existing credential"); + + // should not error on excluding random credential + promise_test(() => { + return Promise.resolve() + .then(() => { + return createCredential(); + }) + .then(() => { + var randomCredId = new Uint8Array(162); + window.crypto.getRandomValues(randomCredId); + + var excludeCred = { + id: randomCredId, + type: "public-key" + }; + var args = { + options: { + publicKey: { + excludeCredentials: [excludeCred] + } + } + }; + return createCredential(args); + }); + }, "exclude random (non-existing) credential"); + + // TODO: exclude including transport type (USB, BLE, NFC) +}); + +/* JSHINT */ +/* globals standardSetup, CreateCredentialsTest, createCredential, promise_test, promise_rejects_dom */ +</script> diff --git a/testing/web-platform/tests/webauthn/createcredential-extensions.https.html b/testing/web-platform/tests/webauthn/createcredential-extensions.https.html new file mode 100644 index 0000000000..5a55a8d860 --- /dev/null +++ b/testing/web-platform/tests/webauthn/createcredential-extensions.https.html @@ -0,0 +1,51 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>WebAuthn navigator.credentials.create() extensions Tests</title> +<meta name="timeout" content="long"> +<link rel="author" title="Adam Powers" href="mailto:adam@fidoalliance.org"> +<link rel="help" href="https://w3c.github.io/webauthn/#iface-credential"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src=helpers.js></script> +<body></body> +<script> +standardSetup(function() { + "use strict"; + + var dummyExtension = { + foo: true, + bar: "yup" + }; + + // bad extension values + new CreateCredentialsTest("options.publicKey.extensions", "hi mom").runTest("Bad extensions: extensions is string", TypeError); + + // phony extensions + var randomExtId = {}; + randomExtId[createRandomString(64)] = dummyExtension; + new CreateCredentialsTest("options.publicKey.extensions", {foo: JSON.stringify(randomExtId)}).runTest("extensions is a nonsensical JSON string"); + + // appid + new CreateCredentialsTest("options.publicKey.extensions", {appid: ""}).runTest("empty appid in create request", "NotSupportedError"); + new CreateCredentialsTest("options.publicKey.extensions", {appid: null}).runTest("null appid in create request", "NotSupportedError"); + new CreateCredentialsTest("options.publicKey.extensions", {appid: "anything"}).runTest("appid in create request", "NotSupportedError"); + + promise_test(async t => { + const credential = await createCredential({ + options: { + publicKey: { + extensions: { + prf: {}, + }, + }, + }, + }); + assert_false(credential.getClientExtensionResults().prf.enabled); + }, "navigator.credentials.create() with prf requested but no support in authenticator"); +}); + +/* JSHINT */ +/* globals standardSetup, CreateCredentialsTest, createRandomString */ +</script> diff --git a/testing/web-platform/tests/webauthn/createcredential-getpublickey.https.html b/testing/web-platform/tests/webauthn/createcredential-getpublickey.https.html new file mode 100644 index 0000000000..215519913d --- /dev/null +++ b/testing/web-platform/tests/webauthn/createcredential-getpublickey.https.html @@ -0,0 +1,52 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>WebAuthn getPublicKey</title> +<meta name="timeout" content="long"> +<link rel="help" href="https://w3c.github.io/webauthn/#sctn-public-key-easy"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="helpers.js"></script> +<script src="resources/utils.js"></script> +<script> +function testGetPublicKey() { + standardSetup(function() { + promise_test(async t => { + let cred = await createCredential(); + const response = cred.response; + assert_equals(response.getPublicKeyAlgorithm(), + cose_alg_ECDSA_w_SHA256); + + const attestationObject = + new Cbor(response.attestationObject).getCBOR(); + const claimedAuthDataHex = uint8ArrayToHex( + new Uint8Array(response.getAuthenticatorData())); + const actualAuthDataHex = uint8ArrayToHex(attestationObject.authData); + assert_equals(actualAuthDataHex, claimedAuthDataHex); + + // Check that the x and y coordinates of the public key appear in + // the claimed SPKI, at least. + const spkiHex = uint8ArrayToHex( + new Uint8Array(response.getPublicKey())); + const authData = parseAuthenticatorData(attestationObject.authData); + const pubKey = authData.attestedCredentialData.credentialPublicKey; + const xHex = uint8ArrayToHex(pubKey.x); + const yHex = uint8ArrayToHex(pubKey.y); + assert_not_equals(-1, spkiHex.indexOf(xHex)); + assert_not_equals(-1, spkiHex.indexOf(yHex)); + + t.done(); + }); + }); +} + +testGetPublicKey(); +/* JSHINT */ +/* globals standardSetup, createCredential */ +</script> +</head> +<body></body> +</html> diff --git a/testing/web-platform/tests/webauthn/createcredential-large-blob-not-supported.https.html b/testing/web-platform/tests/webauthn/createcredential-large-blob-not-supported.https.html new file mode 100644 index 0000000000..167a65b922 --- /dev/null +++ b/testing/web-platform/tests/webauthn/createcredential-large-blob-not-supported.https.html @@ -0,0 +1,78 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>navigator.credentials.create() largeBlob extension tests with no authenticator support</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src=helpers.js></script> +<body></body> +<script> +standardSetup(function() { + "use strict"; + + new CreateCredentialsTest("options.publicKey.extensions", { + largeBlob: { + write: new ArrayBuffer(), + }, + }).runTest("navigator.credentials.create() with largeBlob.write set", "NotSupportedError"); + + new CreateCredentialsTest("options.publicKey.extensions", { + largeBlob: { + read: true, + }, + }).runTest("navigator.credentials.create() with largeBlob.read set", "NotSupportedError"); + + promise_test(async t => { + const credential = await createCredential({ + options: { + publicKey: { + authenticatorSelection: { + requireResidentKey: true, + }, + extensions: { + largeBlob: { + support: "preferred", + }, + }, + }, + }, + }); + assert_own_property(credential.getClientExtensionResults(), "largeBlob"); + assert_false(credential.getClientExtensionResults().largeBlob.supported); + assert_not_own_property(credential.getClientExtensionResults().largeBlob, "blob"); + assert_not_own_property(credential.getClientExtensionResults().largeBlob, "written"); + }, "navigator.credentials.create() with largeBlob.support set to preferred and not supported by authenticator"); + + promise_test(async t => { + const credential = await createCredential({ + options: { + publicKey: { + authenticatorSelection: { + requireResidentKey: true, + }, + extensions: { + largeBlob: {}, + }, + }, + }, + }); + assert_own_property(credential.getClientExtensionResults(), "largeBlob"); + assert_false(credential.getClientExtensionResults().largeBlob.supported); + assert_not_own_property(credential.getClientExtensionResults().largeBlob, "blob"); + assert_not_own_property(credential.getClientExtensionResults().largeBlob, "written"); + }, "navigator.credentials.create() with largeBlob.support not set and not supported by authenticator"); + + new CreateCredentialsTest("options.publicKey.extensions", { + largeBlob: { + support: "required" + }, + }).runTest("navigator.credentials.create() with largeBlob.support set to required and not supported by authenticator", "NotAllowedError"); +}, { + protocol: "ctap2_1", + hasResidentKey: true, + hasUserVerification: true, + isUserVerified: true, +}); +</script> diff --git a/testing/web-platform/tests/webauthn/createcredential-large-blob-supported.https.html b/testing/web-platform/tests/webauthn/createcredential-large-blob-supported.https.html new file mode 100644 index 0000000000..5c07745a49 --- /dev/null +++ b/testing/web-platform/tests/webauthn/createcredential-large-blob-supported.https.html @@ -0,0 +1,82 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>navigator.credentials.create() largeBlob extension tests with authenticator support</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src=helpers.js></script> +<body></body> +<script> +standardSetup(function() { + "use strict"; + + promise_test(async t => { + const credential = await createCredential({ + options: { + publicKey: { + authenticatorSelection: { + requireResidentKey: true, + }, + extensions: { + largeBlob: { + support: "preferred", + }, + }, + }, + }, + }); + assert_own_property(credential.getClientExtensionResults(), "largeBlob"); + assert_true(credential.getClientExtensionResults().largeBlob.supported); + assert_not_own_property(credential.getClientExtensionResults().largeBlob, "blob"); + assert_not_own_property(credential.getClientExtensionResults().largeBlob, "written"); + }, "navigator.credentials.create() with largeBlob.support set to preferred and supported by authenticator"); + + promise_test(async t => { + const credential = await createCredential({ + options: { + publicKey: { + authenticatorSelection: { + requireResidentKey: true, + }, + extensions: { + largeBlob: {}, + }, + }, + }, + }); + assert_own_property(credential.getClientExtensionResults(), "largeBlob"); + assert_true(credential.getClientExtensionResults().largeBlob.supported); + assert_not_own_property(credential.getClientExtensionResults().largeBlob, "blob"); + assert_not_own_property(credential.getClientExtensionResults().largeBlob, "written"); + }, "navigator.credentials.create() with largeBlob.support not set and supported by authenticator"); + + promise_test(async t => { + const credential = await createCredential({ + options: { + publicKey: { + authenticatorSelection: { + requireResidentKey: true, + }, + extensions: { + largeBlob: { + support: "required" + }, + }, + }, + }, + }); + assert_own_property(credential.getClientExtensionResults(), "largeBlob"); + assert_true(credential.getClientExtensionResults().largeBlob.supported); + assert_not_own_property(credential.getClientExtensionResults().largeBlob, "blob"); + assert_not_own_property(credential.getClientExtensionResults().largeBlob, "written"); + }, "navigator.credentials.create() with largeBlob.support set to required and supported by authenticator"); +}, { + protocol: "ctap2_1", + hasResidentKey: true, + hasUserVerification: true, + isUserVerified: true, + extensions: ["largeBlob"], +}); +</script> diff --git a/testing/web-platform/tests/webauthn/createcredential-minpinlength.https.html b/testing/web-platform/tests/webauthn/createcredential-minpinlength.https.html new file mode 100644 index 0000000000..a92898c848 --- /dev/null +++ b/testing/web-platform/tests/webauthn/createcredential-minpinlength.https.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>navigator.credentials.create() largeBlob extension tests with authenticator support</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src=helpers.js></script> +<body></body> +<script> +standardSetup(function() { + "use strict"; + + promise_test(async t => { + const credential = await createCredential({ + options: { + publicKey: { + extensions: { + minPinLength: true, + }, + }, + }, + }); + // The extension results will be in the authenticator outputs. + assert_true(new Uint8Array(credential.response.getAuthenticatorData()) + .toString() + .includes(new TextEncoder() + .encode("minPinLength") + .toString())); + }, "navigator.credentials.create() with minPinLength requested"); +}, { + protocol: "ctap2_1", + extensions: ["minPinLength"], +}); +</script> diff --git a/testing/web-platform/tests/webauthn/createcredential-passing.https.html b/testing/web-platform/tests/webauthn/createcredential-passing.https.html new file mode 100644 index 0000000000..f64a4ff039 --- /dev/null +++ b/testing/web-platform/tests/webauthn/createcredential-passing.https.html @@ -0,0 +1,145 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>WebAuthn credential.create() Passing Tests</title> +<meta name="timeout" content="long"> +<link rel="author" title="Adam Powers" href="mailto:adam@fidoalliance.org"> +<link rel="help" href="https://w3c.github.io/webauthn/#iface-credential"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src=helpers.js></script> +<body></body> +<script> +standardSetup(function() { + "use strict"; + + // CreateCredentialTest passing tests + + // default arguments + new CreateCredentialsTest().runTest("passing credentials.create() with default arguments"); + + // rp + new CreateCredentialsTest({path: "options.publicKey.rp.id", value: window.location.hostname}).runTest("passing credentials.create() with rpId (hostname)"); + + // user + new CreateCredentialsTest("options.publicKey.user.id", new ArrayBuffer(1)).runTest("very short user id"); + new CreateCredentialsTest("options.publicKey.user.id", new ArrayBuffer(64)).runTest("max length user id"); + new CreateCredentialsTest("options.publicKey.user.id", new Uint8Array(64)).runTest("Uint8Array user id"); + new CreateCredentialsTest("options.publicKey.user.id", new Int8Array(64)).runTest("Int8Array user id"); + new CreateCredentialsTest("options.publicKey.user.id", new Int16Array(32)).runTest("Int16Array user id"); + new CreateCredentialsTest("options.publicKey.user.id", new Int32Array(16)).runTest("Int32Array user id"); + new CreateCredentialsTest("options.publicKey.user.id", new Float32Array(16)).runTest("Float32Array user id"); + var dvBuf1 = new ArrayBuffer(16); + new CreateCredentialsTest("options.publicKey.user.id", new DataView(dvBuf1)).runTest("DataView user id"); + + // good challenge values + // all these challenges are zero-filled buffers... think anyone will complain? + new CreateCredentialsTest("options.publicKey.challenge", new Int16Array(33)).runTest("Int16Array challenge"); + new CreateCredentialsTest("options.publicKey.challenge", new Int32Array(17)).runTest("Int32Array challenge"); + new CreateCredentialsTest("options.publicKey.challenge", new Float32Array(17)).runTest("Float32Array challenge"); + new CreateCredentialsTest("options.publicKey.challenge", new Float64Array(9)).runTest("Float64Array challenge"); + var dvBuf2 = new ArrayBuffer(65); + new CreateCredentialsTest("options.publicKey.challenge", new DataView(dvBuf2)).runTest("DataView challenge"); + new CreateCredentialsTest("options.publicKey.challenge", new ArrayBuffer(8192)).runTest("Absurdly large challenge"); + + // good pubKeyCredParams values + // empty pubKeyCredParams should default to EC256 and RS256 + new CreateCredentialsTest("options.publicKey.pubKeyCredParams", []).runTest("pubKeyCredParams is empty Array"); + const pkParamEC256 = { + type: "public-key", + alg: cose_alg_ECDSA_w_SHA256 + }; + const pkParamEC512 = { + type: "public-key", + alg: cose_alg_ECDSA_w_SHA512 + }; + new CreateCredentialsTest("options.publicKey.pubKeyCredParams", [pkParamEC256]).runTest("EC256 pubKeyCredParams"); + new CreateCredentialsTest("options.publicKey.pubKeyCredParams", [pkParamEC512, pkParamEC256]) + .runTest("SelectEC256 pubKeyCredParams from a list"); + + // timeout + new CreateCredentialsTest({path: "options.publicKey.timeout", value: undefined}).runTest("passing credentials.create() with no timeout"); + + // valid authenticatorSelection values + var defaultAuthnrSel = { + authenticatorAttachment: "cross-platform", + requireResidentKey: false, + userVerification: "preferred" + }; + // attachment + var authnrSelAttachUndef = cloneObject(defaultAuthnrSel); + authnrSelAttachUndef.authenticatorAttachment = undefined; + var authnrSelAttachEmptyStr = cloneObject(defaultAuthnrSel); + authnrSelAttachEmptyStr.authenticatorAttachment = ""; + var authnrSelAttachEmptyObj = cloneObject(defaultAuthnrSel); + authnrSelAttachEmptyObj.authenticatorAttachment = {}; + var authnrSelAttachNull = cloneObject(defaultAuthnrSel); + authnrSelAttachNull.authenticatorAttachment = null; + var authnrSelAttachUnknownValue = cloneObject(defaultAuthnrSel); + authnrSelAttachUnknownValue.authenticatorAttachment = "unknown-value"; + // resident key + var authnrSelRkUndef = cloneObject(defaultAuthnrSel); + authnrSelRkUndef.requireResidentKey = undefined; + var authnrSelRkFalse = cloneObject(defaultAuthnrSel); + authnrSelRkFalse.requireResidentKey = false; + // user verification + var authnrSelUvUndef = cloneObject(defaultAuthnrSel); + authnrSelUvUndef.userVerification = undefined; + var authnrSelUvDiscouraged = cloneObject(defaultAuthnrSel); + authnrSelUvDiscouraged.userVerification = "discouraged"; + var authnrSelUvEmptyStr = cloneObject(defaultAuthnrSel); + authnrSelUvEmptyStr.userVerification = ""; + var authnrSelUvEmptyObj = cloneObject(defaultAuthnrSel); + authnrSelUvEmptyObj.userVerification = {}; + var authnrSelUvStr = cloneObject(defaultAuthnrSel); + authnrSelUvStr.userVerification = "requiredshirtshoestshirt"; + var authnrSelUvNull = cloneObject(defaultAuthnrSel); + authnrSelUvNull.userVerification = null; + + new CreateCredentialsTest({path: "options.publicKey.authenticatorSelection", value: undefined}).runTest("authenticatorSelection is undefined"); + new CreateCredentialsTest("options.publicKey.authenticatorSelection", {}).runTest("authenticatorSelection is empty object"); + new CreateCredentialsTest("options.publicKey.authenticatorSelection", cloneObject(defaultAuthnrSel)).runTest("authenticatorSelection default values"); + + // authnr selection attachment + new CreateCredentialsTest("options.publicKey.authenticatorSelection", authnrSelAttachUndef).runTest("authenticatorSelection attachment undefined"); + new CreateCredentialsTest("options.publicKey.authenticatorSelection", authnrSelAttachEmptyStr).runTest("authenticatorSelection attachment empty string"); + new CreateCredentialsTest("options.publicKey.authenticatorSelection", authnrSelAttachEmptyObj).runTest("authenticatorSelection attachment empty object"); + new CreateCredentialsTest("options.publicKey.authenticatorSelection", authnrSelAttachNull).runTest("authenticatorSelection attachment null"); + new CreateCredentialsTest("options.publicKey.authenticatorSelection", authnrSelAttachUnknownValue).runTest("authenticatorSelection attachment unknown value"); + + // authnr selection resident key + new CreateCredentialsTest("options.publicKey.authenticatorSelection", authnrSelRkUndef).runTest("authenticatorSelection residentKey undefined"); + // XXX: assumes authnr is behaving like most U2F authnrs; really depends on the authnr or mock configuration + new CreateCredentialsTest("options.publicKey.authenticatorSelection", authnrSelRkFalse).runTest("authenticatorSelection residentKey false"); + + // authnr selection user verification + new CreateCredentialsTest("options.publicKey.authenticatorSelection", authnrSelUvUndef).runTest("authenticatorSelection userVerification undefined"); + new CreateCredentialsTest("options.publicKey.authenticatorSelection", authnrSelUvDiscouraged).runTest("authenticatorSelection userVerification discouraged"); + new CreateCredentialsTest("options.publicKey.authenticatorSelection", authnrSelUvEmptyStr).runTest("authenticatorSelection userVerification empty string"); + new CreateCredentialsTest("options.publicKey.authenticatorSelection", authnrSelUvEmptyObj).runTest("authenticatorSelection userVerification empty object"); + new CreateCredentialsTest("options.publicKey.authenticatorSelection", authnrSelUvStr).runTest("authenticatorSelection userVerification unknown value"); + new CreateCredentialsTest("options.publicKey.authenticatorSelection", authnrSelUvNull).runTest("authenticatorSelection userVerification null"); + + // good attestation values + new CreateCredentialsTest("options.publicKey.attestation", "none").runTest("attestation parameter: attestation is \"none\""); + new CreateCredentialsTest("options.publicKey.attestation", "indirect").runTest("attestation parameter: attestation is \"indirect\""); + new CreateCredentialsTest("options.publicKey.attestation", "direct").runTest("attestation parameter: attestation is \"direct\""); + new CreateCredentialsTest({path: "options.publicKey.attestation", value: undefined}).runTest("attestation parameter: attestation is undefined"); + // attestation unknown values + new CreateCredentialsTest("options.publicKey.attestation", {}).runTest("attestation parameter: attestation is empty object"); + new CreateCredentialsTest("options.publicKey.attestation", []).runTest("attestation parameter: attestation is empty array"); + new CreateCredentialsTest("options.publicKey.attestation", null).runTest("attestation parameter: attestation is null"); + new CreateCredentialsTest("options.publicKey.attestation", "noneofyourbusiness").runTest("attestation parameter: attestation is \"noneofyourbusiness\""); + new CreateCredentialsTest("options.publicKey.attestation", "").runTest("attestation parameter: attestation is empty string"); + // TODO: test this with multiple mock authenticators to make sure that the right options are chosen when available? + + // good extension values + new CreateCredentialsTest({path: "options.publicKey.extensions", value: undefined}).runTest("extensions undefined"); + new CreateCredentialsTest("options.publicKey.extensions", {}).runTest("extensions are empty object"); + new CreateCredentialsTest("options.publicKey.extensions", {foo: "", bar: "", bat: ""}).runTest("extensions are dict of empty strings"); +}); + +/* JSHINT */ +/* globals standardSetup, CreateCredentialsTest, cose_alg_ECDSA_w_SHA256, cose_alg_ECDSA_w_SHA512, cloneObject */ +</script> diff --git a/testing/web-platform/tests/webauthn/createcredential-prf.https.html b/testing/web-platform/tests/webauthn/createcredential-prf.https.html new file mode 100644 index 0000000000..7243e088d4 --- /dev/null +++ b/testing/web-platform/tests/webauthn/createcredential-prf.https.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>navigator.credentials.create() prf extension tests with authenticator support</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src=helpers.js></script> +<body></body> +<script> +standardSetup(function() { + "use strict"; + + promise_test(async t => { + const credential = await createCredential({ + options: { + publicKey: { + extensions: { + prf: {}, + }, + }, + }, + }); + assert_true(credential.getClientExtensionResults().prf.enabled); + }, "navigator.credentials.create() with prf requested"); + + promise_test(async t => { + const promise = createCredential({ + options: { + publicKey: { + extensions: { + prf: {evalByCredential: {"Zm9v": {first: new Uint8Array([1,2,3,4]).buffer}}}, + }, + }, + }, + }); + return promise_rejects_dom(t, "NotSupportedError", promise); + }, "navigator.credentials.create() with nonsensical evalByCredential"); +}, { + protocol: "ctap2_1", + extensions: ["prf"], +}); +</script> diff --git a/testing/web-platform/tests/webauthn/createcredential-pubkeycredparams.https.html b/testing/web-platform/tests/webauthn/createcredential-pubkeycredparams.https.html new file mode 100644 index 0000000000..d1df7952d6 --- /dev/null +++ b/testing/web-platform/tests/webauthn/createcredential-pubkeycredparams.https.html @@ -0,0 +1,73 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>WebAuthn navigator.credentials.create() pubKeyCredParams Tests</title> +<meta name="timeout" content="long"> +<link rel="author" title="Adam Powers" href="mailto:adam@fidoalliance.org"> +<link rel="help" href="https://w3c.github.io/webauthn/#iface-credential"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src=helpers.js></script> +<body></body> +<script> +standardSetup(function() { + "use strict"; + + var badType = { + type: "something-else", + alg: cose_alg_ECDSA_w_SHA512 + }; + var badTypeEmptyString = cloneObject(badType); + badTypeEmptyString.type = ""; + var badTypeNull = cloneObject(badType); + badTypeNull.type = null; + var badTypeEmptyObj = cloneObject(badType); + badTypeEmptyObj.type = {}; + + var badAlg = { + type: "public-key", + alg: 42 + }; + var badAlgZero = cloneObject(badAlg); + badAlgZero.alg = 0; + + // bad pubKeyCredParams values + new CreateCredentialsTest({path: "options.publicKey.pubKeyCredParams", value: undefined}).runTest("Bad pubKeyCredParams: pubKeyCredParams is undefined", TypeError); + new CreateCredentialsTest("options.publicKey.pubKeyCredParams", "hi mom").runTest("Bad pubKeyCredParams: pubKeyCredParams is string", TypeError); + new CreateCredentialsTest("options.publicKey.pubKeyCredParams", null).runTest("Bad pubKeyCredParams: pubKeyCredParams is null", TypeError); + new CreateCredentialsTest("options.publicKey.pubKeyCredParams", [badType]).runTest("Bad pubKeyCredParams: first param has bad type (\"something-else\")", TypeError); + new CreateCredentialsTest("options.publicKey.pubKeyCredParams", [badTypeEmptyString]).runTest("Bad pubKeyCredParams: first param has bad type (\"\")", TypeError); + new CreateCredentialsTest("options.publicKey.pubKeyCredParams", [badTypeNull]).runTest("Bad pubKeyCredParams: first param has bad type (null)", TypeError); + new CreateCredentialsTest("options.publicKey.pubKeyCredParams", [badTypeEmptyObj]).runTest("Bad pubKeyCredParams: first param has bad type (empty object)", TypeError); + new CreateCredentialsTest("options.publicKey.pubKeyCredParams", [badAlg]) + .modify("options.publicKey.timeout", 300) + .runTest("Bad pubKeyCredParams: first param has bad alg (42)", "NotAllowedError"); + new CreateCredentialsTest("options.publicKey.pubKeyCredParams", [badAlgZero]) + .modify("options.publicKey.timeout", 300) + .runTest("Bad pubKeyCredParams: first param has bad alg (0)", "NotAllowedError"); + + // TODO: come back to this when mock authenticators support multiple cryptos so that we can test the preference ranking + // function verifyEC256(res) { + // debug ("verifyEC256 got", res); + // debug ("client data JSON", ab2str(res.response.clientDataJSON)); + // parseAuthenticatorData(res.response.attestationObject); + // } + // new CreateCredentialsTest("options.publicKey.pubKeyCredParams", [pkParamEC256, pkParamEC512]) + // .afterTest(verifyEC256) + // .runTest("EC256, EC512 pubKeyCredParams"); + // function verifyEC512(res) { + // debug ("verifyEC512 got", res); + // debug ("client data JSON", ab2str(res.response.clientDataJSON)); + // // parseAuthenticatorData(res.response.attestationObject); + // printHex ("clientDataJSON", res.response.clientDataJSON); + // printHex ("attestationObject", res.response.attestationObject); + // } + // new CreateCredentialsTest("options.publicKey.pubKeyCredParams", [pkParamEC512, pkParamEC256]) + // .afterTest(verifyEC512) + // .runTest("EC512, EC256 pubKeyCredParams"); +}); + +/* JSHINT */ +/* globals standardSetup, CreateCredentialsTest, cose_alg_ECDSA_w_SHA512, cloneObject */ +</script> diff --git a/testing/web-platform/tests/webauthn/createcredential-resident-key.https.html b/testing/web-platform/tests/webauthn/createcredential-resident-key.https.html new file mode 100644 index 0000000000..d64ec14c00 --- /dev/null +++ b/testing/web-platform/tests/webauthn/createcredential-resident-key.https.html @@ -0,0 +1,178 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>navigator.credentials.create() test with residentKey and credProps</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src=helpers.js></script> +<script> + +"use strict"; +const credPropsTests = [ + { + name: "U2F", + authenticatorArgs: { + protocol: "ctap1/u2f", + }, + expected: { + discouraged: { + success: true, + hasRk: true, + rk: false, + }, + preferred: { + success: true, + hasRk: true, + rk: false, + }, + required: { + success: false, + }, + }, + }, + { + name: "CTAP 2.0 without resident key support", + authenticatorArgs: { + protocol: "ctap2", + hasResidentKey: false, + hasUserVerification: true, + isUserVerified: true, + }, + expected: { + discouraged: { + success: true, + hasRk: true, + rk: false, + }, + preferred: { + success: true, + hasRk: true, + rk: false, + }, + required: { + success: false, + }, + }, + }, + { + name: "CTAP 2.0 with resident key support", + authenticatorArgs: { + protocol: "ctap2", + hasResidentKey: true, + hasUserVerification: true, + isUserVerified: true, + }, + expected: { + discouraged: { + success: true, + // CTAP2.0 authenticators may treat all credentials as discoverable, + // thus Chrome omits 'rk' in this case. + hasRk: false, + }, + preferred: { + success: true, + hasRk: true, + rk: true, + }, + required: { + success: true, + hasRk: true, + rk: true, + }, + }, + }, + { + name: "CTAP 2.1 without resident key support", + authenticatorArgs: { + protocol: "ctap2_1", + hasResidentKey: false, + hasUserVerification: true, + isUserVerified: true, + }, + expected: { + discouraged: { + success: true, + hasRk: true, + rk: false, + }, + preferred: { + success: true, + hasRk: true, + rk: false, + }, + required: { + success: false, + }, + }, + }, + { + name: "CTAP 2.1 with resident key support", + authenticatorArgs: { + protocol: "ctap2_1", + hasResidentKey: true, + hasUserVerification: true, + isUserVerified: true, + }, + expected: { + discouraged: { + success: true, + hasRk: true, + rk: false, + }, + preferred: { + success: true, + hasRk: true, + rk: true, + }, + required: { + success: true, + hasRk: true, + rk: true, + }, + }, + }, +]; + +for (const fixture of credPropsTests) { + for (const rkRequirement of ["discouraged", "preferred", "required"]) { + virtualAuthenticatorPromiseTest(async t => { + const promise = createCredential({ + options: { + publicKey: { + authenticatorSelection: { + residentKey: rkRequirement, + }, + extensions: { + credProps: true, + }, + }, + }, + }); + + assert_true(rkRequirement in fixture.expected); + const expected = fixture.expected[rkRequirement]; + assert_true('success' in expected); + if (!expected.success) { + return promise_rejects_dom(t, "NotAllowedError", promise); + } + + const cred = await promise; + assert_true('credProps' in cred.getClientExtensionResults()); + const credProps = cred.getClientExtensionResults().credProps; + assert_equals('rk' in credProps, expected.hasRk, "hasRk"); + if (expected.hasRk) { + assert_equals(credProps.rk, expected.rk, "rk"); + } + }, fixture.authenticatorArgs, fixture.name + + ": navigator.credentials.create() with credProps extension, rk=" + + rkRequirement); + } +} +</script> +</head> +<body></body> +</html> diff --git a/testing/web-platform/tests/webauthn/createcredential-timeout.https.html b/testing/web-platform/tests/webauthn/createcredential-timeout.https.html new file mode 100644 index 0000000000..fc35a6e72f --- /dev/null +++ b/testing/web-platform/tests/webauthn/createcredential-timeout.https.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>WebAuthn navigator.credentials.create() timeout Tests</title> +<meta name="timeout" content="long"> +<link rel="author" title="Adam Powers" href="mailto:adam@fidoalliance.org"> +<link rel="help" href="https://w3c.github.io/webauthn/#iface-credential"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src=helpers.js></script> +<body></body> +<script> +"use strict"; + +// bad timeout values +// TODO: there is some debate as to whether MAX_UNSIGNED_LONG + 1 and / or -1 should be disallowed since they get converted to valid values internally +// new CreateCredentialsTest({path: "options.publicKey.timeout", value: -1}).runTest("Bad timeout: negative", TypeError); +// new CreateCredentialsTest({path: "options.publicKey.timeout", value: 4294967295 + 1}).runTest("Bad timeout: too big", TypeError); + +// timeout test +promise_test(async t => { + // if available, configure a mock authenticator that does not respond to user input + try { + let authenticator = await window.test_driver.add_virtual_authenticator({ + protocol: "ctap1/u2f", + transport: "usb", + isUserConsenting: false, + }); + t.add_cleanup(() => window.test_driver.remove_virtual_authenticator(authenticator)); + } catch (error) { + if (error !== "error: Action add_virtual_authenticator not implemented") { + throw error; + } + } + + var args = { + options: { + publicKey: { + // browsers may set an arbitrary minimum timeout and not respect this value + timeout: 1 + } + } + }; + + return promise_rejects_dom(t, "NotAllowedError", createCredential(args)); +}, "ensure create credential times out"); +// TODO: createCredential.timeout > 1s && setTimeout < 1s +// TODO: createCredential.timeout < 5s && setTimeout > 5s + +/* JSHINT */ +/* globals standardSetup, CreateCredentialsTest, createCredential, promise_test, promise_rejects_dom*/ +</script> diff --git a/testing/web-platform/tests/webauthn/credblob-not-supported.https.html b/testing/web-platform/tests/webauthn/credblob-not-supported.https.html new file mode 100644 index 0000000000..0f9514ba5e --- /dev/null +++ b/testing/web-platform/tests/webauthn/credblob-not-supported.https.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>credBlob extension tests</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src=helpers.js></script> +<body></body> +<script> +"use strict"; + +const blobu8 = new Uint8Array(16); +window.crypto.getRandomValues(blobu8); +const blob = blobu8.buffer; + +virtualAuthenticatorPromiseTest(async t => { + const cred = await createCredential({ + options: { + publicKey: { + extensions: { + credBlob: blob, + }, + }, + }, + }); + + const createExtensions = cred.getClientExtensionResults(); + assert_own_property(createExtensions, "credBlob"); + assert_equals(createExtensions.credBlob, false, "extension supported at create time"); +}, { + protocol: "ctap2_1", + extensions: [], +}, "creation requesting credBlob without authenticator support"); +</script> diff --git a/testing/web-platform/tests/webauthn/credblob-supported.https.html b/testing/web-platform/tests/webauthn/credblob-supported.https.html new file mode 100644 index 0000000000..c69091fbc4 --- /dev/null +++ b/testing/web-platform/tests/webauthn/credblob-supported.https.html @@ -0,0 +1,93 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>credBlob extension tests</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src=helpers.js></script> +<body></body> +<script> +"use strict"; + +const blobu8 = new Uint8Array(16); +window.crypto.getRandomValues(blobu8); +const blob = blobu8.buffer; +const authenticatorOptions = { + protocol: "ctap2_1", + extensions: ["credBlob"], +}; + +function compareBuffers(a, b) { + if (a.byteLength != b.byteLength) { + return false; + } + const a8 = new Uint8Array(a); + const b8 = new Uint8Array(b); + for (let i = 0; i < a8.length; i++) { + if (a8[i] != b8[i]) { + return false; + } + } + return true; +} + +virtualAuthenticatorPromiseTest(async t => { + const cred = await createCredential({ + options: { + publicKey: {}, + }, + }); + + const createExtensions = cred.getClientExtensionResults(); + assert_not_own_property(createExtensions, "credBlob"); + + const assertion = await navigator.credentials.get({publicKey: { + challenge: new Uint8Array(), + allowCredentials: [{ + id: cred.rawId, + type: "public-key", + }], + extensions: { + getCredBlob: true, + }, + }}); + + const getExtensions = assertion.getClientExtensionResults(); + assert_own_property(getExtensions, "getCredBlob"); + const emptyBuffer = new Uint8Array(); + assert_true(compareBuffers(getExtensions.getCredBlob, emptyBuffer)); +}, authenticatorOptions, "assertion without credBlob"); + +virtualAuthenticatorPromiseTest(async t => { + const cred = await createCredential({ + options: { + publicKey: { + extensions: { + credBlob: blob, + }, + }, + }, + }); + + const createExtensions = cred.getClientExtensionResults(); + assert_own_property(createExtensions, "credBlob"); + assert_equals(createExtensions.credBlob, true, "extension supported at create time"); + + const assertion = await navigator.credentials.get({publicKey: { + challenge: new Uint8Array(), + allowCredentials: [{ + id: cred.rawId, + type: "public-key", + }], + extensions: { + getCredBlob: true, + }, + }}); + + const getExtensions = assertion.getClientExtensionResults(); + assert_own_property(getExtensions, "getCredBlob"); + assert_true(compareBuffers(getExtensions.getCredBlob, blob)); +}, authenticatorOptions, "assertion with credBlob"); +</script> diff --git a/testing/web-platform/tests/webauthn/getcredential-abort.https.html b/testing/web-platform/tests/webauthn/getcredential-abort.https.html new file mode 100644 index 0000000000..958f65daf1 --- /dev/null +++ b/testing/web-platform/tests/webauthn/getcredential-abort.https.html @@ -0,0 +1,66 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>WebAuthn navigator.credentials.get() abort Tests</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src=helpers.js></script> +<body></body> +<script> +"use strict"; + +const getParams = { + publicKey: { + id: 'id', + challenge: new Uint8Array(), + } +}; + +promise_test(async t => { + const abortController = new AbortController(); + abortController.abort(); + getParams.signal = abortController.signal; + const promise = navigator.credentials.get(getParams); + return promise_rejects_dom(t, "AbortError", promise); +}, "navigator.credentials.get() after abort without reason"); + +promise_test(async t => { + const abortController = new AbortController(); + getParams.signal = abortController.signal; + const promise = navigator.credentials.get(getParams); + abortController.abort(); + return promise_rejects_dom(t, "AbortError", promise); +}, "navigator.credentials.get() before abort without reason"); + +promise_test(async t => { + const abortController = new AbortController(); + abortController.abort("CustomError"); + getParams.signal = abortController.signal; + const promise = navigator.credentials.get(getParams); + return promise_rejects_exactly(t, "CustomError", promise); +}, "navigator.credentials.get() after abort reason"); + +promise_test(async t => { + const abortController = new AbortController(); + getParams.signal = abortController.signal; + const promise = navigator.credentials.get(getParams); + abortController.abort("CustomError"); + return promise_rejects_exactly(t, "CustomError", promise); +}, "navigator.credentials.get() before abort reason"); + +promise_test(async t => { + const abortController = new AbortController(); + abortController.abort(new Error('error')); + getParams.signal = abortController.signal; + const promise = navigator.credentials.get(getParams); + return promise_rejects_js(t, Error, promise); +}, "navigator.credentials.get() after abort reason with Error"); + +promise_test(async t => { + const abortController = new AbortController(); + getParams.signal = abortController.signal; + const promise = navigator.credentials.get(getParams); + abortController.abort(new Error('error')); + return promise_rejects_js(t, Error, promise); +}, "navigator.credentials.get() before abort reason with Error"); +</script> diff --git a/testing/web-platform/tests/webauthn/getcredential-attachment.https.html b/testing/web-platform/tests/webauthn/getcredential-attachment.https.html new file mode 100644 index 0000000000..7ab7235af5 --- /dev/null +++ b/testing/web-platform/tests/webauthn/getcredential-attachment.https.html @@ -0,0 +1,71 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src=helpers.js></script> +<body></body> +<script> + "use strict"; + // usb transport + virtualAuthenticatorPromiseTest(async function() { + const assertion = await navigator.credentials.get({publicKey: { + challenge: new Uint8Array(), + allowCredentials: [{ + id: (await createCredential()).rawId, + type: "public-key", + }], + }}); + assert_equals(assertion.authenticatorAttachment, "cross-platform"); + }, { + protocol: "ctap2", + transport: "usb" + }, "navigator.credentials.get() with usb authenticator, attachment as cross-platform"); + + // ble transport + virtualAuthenticatorPromiseTest(async function() { + const assertion = await navigator.credentials.get({publicKey: { + challenge: new Uint8Array(), + allowCredentials: [{ + id: (await createCredential()).rawId, + type: "public-key", + }], + }}); + assert_equals(assertion.authenticatorAttachment, "cross-platform"); + }, { + protocol: "ctap2", + transport: "ble" + }, "navigator.credentials.get() with ble authenticator, attachment as cross-platform"); + + // nfc transport + virtualAuthenticatorPromiseTest(async function() { + const assertion = await navigator.credentials.get({publicKey: { + challenge: new Uint8Array(), + allowCredentials: [{ + id: (await createCredential()).rawId, + type: "public-key", + }], + }}); + assert_equals(assertion.authenticatorAttachment, "cross-platform"); + }, { + protocol: "ctap2", + transport: "nfc" + }, "navigator.credentials.get() with nfc authenticator, attachment as cross-platform"); + + // internal transport + virtualAuthenticatorPromiseTest(async function() { + const assertion = await navigator.credentials.get({publicKey: { + challenge: new Uint8Array(), + allowCredentials: [{ + id: (await createCredential()).rawId, + type: "public-key", + }], + }}); + assert_equals(assertion.authenticatorAttachment, "platform"); + }, { + protocol: "ctap2", + transport: "internal" + }, "navigator.credentials.get() with internal authenticator, attachment as platform"); +</script> diff --git a/testing/web-platform/tests/webauthn/getcredential-badargs-rpid.https.html b/testing/web-platform/tests/webauthn/getcredential-badargs-rpid.https.html new file mode 100644 index 0000000000..3f9d3f2177 --- /dev/null +++ b/testing/web-platform/tests/webauthn/getcredential-badargs-rpid.https.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>WebAuthn credential.get() rpId Tests</title> +<meta name="timeout" content="long"> +<link rel="author" title="Adam Powers" href="mailto:adam@fidoalliance.org"> +<link rel="help" href="https://w3c.github.io/webauthn/#iface-credential"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src=helpers.js></script> +<body></body> +<script> +standardSetup(function() { + "use strict"; + + var credPromise = createCredential(); + + new GetCredentialsTest("options.publicKey.rpId", "") + .addCredential(credPromise) + .runTest("Bad rpId: empty string", "SecurityError"); + new GetCredentialsTest("options.publicKey.rpId", null) + .addCredential(credPromise) + .runTest("Bad rpId: null", "SecurityError"); + new GetCredentialsTest("options.publicKey.rpId", "invalid domain.com") + .addCredential(credPromise) + .runTest("Bad rpId: invalid domain (has space)", "SecurityError"); + new GetCredentialsTest("options.publicKey.rpId", "-invaliddomain.com") + .addCredential(credPromise) + .runTest("Bad rpId: invalid domain (starts with dash)", "SecurityError"); + new GetCredentialsTest("options.publicKey.rpId", "0invaliddomain.com") + .addCredential(credPromise) + .runTest("Bad rpId: invalid domain (starts with number)", "SecurityError"); + + let hostAndPort = window.location.host; + if (!hostAndPort.match(/:\d+$/)) { + hostAndPort += ":443"; + } + new GetCredentialsTest({path: "options.publicKey.rpId", value: hostAndPort}) + .addCredential(credPromise) + .runTest("Bad rpId: host + port", "SecurityError"); +}); + +/* JSHINT */ +/* globals standardSetup, GetCredentialsTest, createCredential */ +</script> diff --git a/testing/web-platform/tests/webauthn/getcredential-badargs-userverification.https.html b/testing/web-platform/tests/webauthn/getcredential-badargs-userverification.https.html new file mode 100644 index 0000000000..8c15a76403 --- /dev/null +++ b/testing/web-platform/tests/webauthn/getcredential-badargs-userverification.https.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>WebAuthn navigator.credentials.get() user verification Tests</title> +<meta name="timeout" content="long"> +<link rel="author" title="Adam Powers" href="mailto:adam@fidoalliance.org"> +<link rel="help" href="https://w3c.github.io/webauthn/#iface-credential"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src=helpers.js></script> +<body></body> +<script> +standardSetup(function() { + "use strict"; + + var credPromise = createCredential(); + + // authenticatorSelection bad userVerification values + // mock authenticator does not support user verification + new GetCredentialsTest("options.publicKey.userVerification", "required") + .addCredential(credPromise) + .runTest("Bad userVerification: \"required\"", "NotAllowedError"); +}); + +/* JSHINT */ +/* globals standardSetup, GetCredentialsTest, createCredential */ +</script> diff --git a/testing/web-platform/tests/webauthn/getcredential-extensions.https.html b/testing/web-platform/tests/webauthn/getcredential-extensions.https.html new file mode 100644 index 0000000000..16c1e57457 --- /dev/null +++ b/testing/web-platform/tests/webauthn/getcredential-extensions.https.html @@ -0,0 +1,80 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>WebAuthn navigator.credentials.get() extensions Tests</title> +<meta name="timeout" content="long"> +<link rel="author" title="Adam Powers" href="mailto:adam@fidoalliance.org"> +<link rel="help" href="https://w3c.github.io/webauthn/#iface-credential"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src=helpers.js></script> +<body></body> +<script> +standardSetup(function() { + "use strict"; + + var credPromise = createCredential(); + var dummyExtension = { + foo: true, + bar: "yup" + }; + var badExtId = {}; + badExtId[createRandomString(65)] = dummyExtension; + + // bad extension values + new GetCredentialsTest("options.publicKey.extensions", "hi mom") + .addCredential(credPromise) + .runTest("Bad extensions: extensions is string", TypeError); + + // empty extensions + new GetCredentialsTest("options.publicKey.extensions", null) + .addCredential(credPromise) + .runTest("extensions is null"); + new GetCredentialsTest("options.publicKey.extensions", []) + .addCredential(credPromise) + .runTest("extensions is empty Array"); + new GetCredentialsTest("options.publicKey.extensions", new ArrayBuffer(0)) + .addCredential(credPromise) + .runTest("extensions is empty ArrayBuffer"); + + // unknown extensions should be ignored + new GetCredentialsTest("options.publicKey.extensions", {foo: dummyExtension}) + .addCredential(credPromise) + .runTest("ignored extension"); + new GetCredentialsTest("options.publicKey.extensions", {badExtId: dummyExtension}) + .addCredential(credPromise) + .runTest("extension ID too long"); + + new GetCredentialsTest("options.publicKey.extensions", {credProps: true}) + .addCredential(credPromise) + .runTest("credProps is only supported at registration", "NotSupportedError"); + + new GetCredentialsTest("options.publicKey.extensions", {payment: {isPayment:true}}) + .addCredential(credPromise) + .runTest("Payment extension is only supported at registration", "NotAllowedError"); + + promise_test(async t => { + const id = (await credPromise).rawId; + const assertion = await navigator.credentials.get({publicKey: { + challenge: new Uint8Array(), + allowCredentials: [{ + id: id, + type: "public-key", + }], + extensions: { + prf: { + eval: { + first: new Uint8Array([1,2,3,4]).buffer, + }, + }, + }, + }}); + + assert_not_own_property(assertion.getClientExtensionResults().prf, 'results'); + }, "navigator.credentials.get() with prf requested but no support in authenticator"); +}); + +/* JSHINT */ +/* globals standardSetup, GetCredentialsTest, createRandomString, createCredential */ +</script> diff --git a/testing/web-platform/tests/webauthn/getcredential-large-blob-not-supported.https.html b/testing/web-platform/tests/webauthn/getcredential-large-blob-not-supported.https.html new file mode 100644 index 0000000000..97cea30f27 --- /dev/null +++ b/testing/web-platform/tests/webauthn/getcredential-large-blob-not-supported.https.html @@ -0,0 +1,91 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>navigator.credentials.get() largeBlob extension tests with no authenticator support</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src=helpers.js></script> +<body></body> +<script> +standardSetup(async function() { + "use strict"; + + const credential = createCredential(); + + promise_test(async t => { + return promise_rejects_dom(t, "NotSupportedError", + navigator.credentials.get({publicKey: { + challenge: new Uint8Array(), + allowCredentials: [{ + id: (await credential).rawId, + type: "public-key", + }], + extensions: { + largeBlob: { + support: "preferred", + }, + }, + }})); + }, "navigator.credentials.get() with largeBlob.support set"); + + promise_test(async t => { + return promise_rejects_dom(t, "NotSupportedError", + navigator.credentials.get({publicKey: { + challenge: new Uint8Array(), + allowCredentials: [{ + id: (await credential).rawId, + type: "public-key", + }], + extensions: { + largeBlob: { + read: true, + write: new ArrayBuffer(), + }, + }, + }})); + }, "navigator.credentials.get() with largeBlob.read and largeBlob.write set"); + + promise_test(async t => { + const assertion = await navigator.credentials.get({publicKey: { + challenge: new Uint8Array(), + allowCredentials: [{ + id: (await credential).rawId, + type: "public-key", + }], + extensions: { + largeBlob: { + read: true, + }, + }, + }}); + assert_not_own_property(assertion.getClientExtensionResults().largeBlob, "supported"); + assert_not_own_property(assertion.getClientExtensionResults().largeBlob, "blob"); + assert_not_own_property(assertion.getClientExtensionResults().largeBlob, "written"); + }, "navigator.credentials.get() with largeBlob.read set without authenticator support"); + + promise_test(async t => { + const assertion = await navigator.credentials.get({publicKey: { + challenge: new Uint8Array(), + allowCredentials: [{ + id: (await credential).rawId, + type: "public-key", + }], + extensions: { + largeBlob: { + write: new TextEncoder().encode("Don't call me Shirley"), + }, + }, + }}); + assert_not_own_property(assertion.getClientExtensionResults().largeBlob, "supported"); + assert_not_own_property(assertion.getClientExtensionResults().largeBlob, "blob"); + assert_false(assertion.getClientExtensionResults().largeBlob.written); + }, "navigator.credentials.get() with largeBlob.write set without authenticator support"); +}, { + protocol: "ctap2_1", + hasResidentKey: true, + hasUserVerification: true, + isUserVerified: true, +}); +</script> diff --git a/testing/web-platform/tests/webauthn/getcredential-large-blob-supported.https.html b/testing/web-platform/tests/webauthn/getcredential-large-blob-supported.https.html new file mode 100644 index 0000000000..c7cc0dfc4a --- /dev/null +++ b/testing/web-platform/tests/webauthn/getcredential-large-blob-supported.https.html @@ -0,0 +1,90 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>navigator.credentials.get() largeBlob extension tests with authenticator support</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src=helpers.js></script> +<body></body> +<script> +standardSetup(async function(authenticator) { + "use strict"; + + const credential = createCredential({ + options: { + publicKey: { + authenticatorSelection: { + requireResidentKey: true, + }, + extensions: { + largeBlob: { + support: "required", + }, + }, + }, + }, + }); + + promise_test(async t => { + const assertion = await navigator.credentials.get({publicKey: { + challenge: new Uint8Array(), + allowCredentials: [{ + id: (await credential).rawId, + type: "public-key", + }], + extensions: { + largeBlob: { + read: true, + }, + }, + }}); + assert_not_own_property(assertion.getClientExtensionResults().largeBlob, "supported"); + assert_not_own_property(assertion.getClientExtensionResults().largeBlob, "blob"); + assert_not_own_property(assertion.getClientExtensionResults().largeBlob, "written"); + }, "navigator.credentials.get() with largeBlob.read set with no blob on authenticator"); + + promise_test(async t => { + const blob = new TextEncoder().encode("According to all known laws of aviation, " + + "there is no way a bee should be able to fly"); + let assertion = await navigator.credentials.get({publicKey: { + challenge: new Uint8Array(), + allowCredentials: [{ + id: (await credential).rawId, + type: "public-key", + }], + extensions: { + largeBlob: { + write: blob, + }, + }, + }}); + assert_not_own_property(assertion.getClientExtensionResults().largeBlob, "blob"); + assert_not_own_property(assertion.getClientExtensionResults().largeBlob, "supported"); + assert_true(assertion.getClientExtensionResults().largeBlob.written); + + assertion = await navigator.credentials.get({publicKey: { + challenge: new Uint8Array(), + allowCredentials: [{ + id: (await credential).rawId, + type: "public-key", + }], + extensions: { + largeBlob: { + read: true, + }, + }, + }}); + assert_array_equals(new Uint8Array(assertion.getClientExtensionResults().largeBlob.blob), blob); + assert_not_own_property(assertion.getClientExtensionResults().largeBlob, "supported"); + assert_not_own_property(assertion.getClientExtensionResults().largeBlob, "written"); + }, "navigator.credentials.get() read and write blob"); +}, { + protocol: "ctap2_1", + hasResidentKey: true, + hasUserVerification: true, + extensions: ["largeBlob"], + isUserVerified: true, +}); +</script> diff --git a/testing/web-platform/tests/webauthn/getcredential-passing.https.html b/testing/web-platform/tests/webauthn/getcredential-passing.https.html new file mode 100644 index 0000000000..7c730d7183 --- /dev/null +++ b/testing/web-platform/tests/webauthn/getcredential-passing.https.html @@ -0,0 +1,74 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>WebAuthn credential.get() Passing Tests</title> +<meta name="timeout" content="long"> +<link rel="author" title="Adam Powers" href="mailto:adam@fidoalliance.org"> +<link rel="help" href="https://w3c.github.io/webauthn/#iface-credential"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src=helpers.js></script> +<body></body> +<script> +standardSetup(function() { + "use strict"; + + var credPromise = createCredential(); + + // GetCredentialsTest with default args + new GetCredentialsTest() + .addCredential(credPromise) + .runTest("passing credentials.get() with default args"); + + // timeout + new GetCredentialsTest({path: "options.publicKey.timeout", value: undefined}) + .addCredential(credPromise) + .runTest("passing credentials.create() with no timeout"); + + // rpId + new GetCredentialsTest({path: "options.publicKey.rpId", value: undefined}) + .addCredential(credPromise) + .runTest("rpId undefined"); + new GetCredentialsTest({path: "options.publicKey.rpId", value: window.location.hostname}) + .addCredential(credPromise) + .runTest("passing credentials.get() with rpId (hostname)"); + + // authnr selection user verification + new GetCredentialsTest({path: "options.publicKey.userVerification", value: undefined}) + .addCredential(credPromise) + .runTest("authenticatorSelection userVerification undefined"); + new GetCredentialsTest("options.publicKey.userVerification", "preferred") + .addCredential(credPromise) + .runTest("authenticatorSelection userVerification preferred"); + new GetCredentialsTest("options.publicKey.userVerification", "discouraged") + .addCredential(credPromise) + .runTest("authenticatorSelection userVerification discouraged"); + new GetCredentialsTest("options.publicKey.userVerification", "") + .addCredential(credPromise) + .runTest("authenticatorSelection userVerification empty string"); + new GetCredentialsTest("options.publicKey.userVerification", {}) + .addCredential(credPromise) + .runTest("authenticatorSelection userVerification empty object"); + new GetCredentialsTest("options.publicKey.userVerification", "requiredshirtshoestshirt") + .addCredential(credPromise) + .runTest("authenticatorSelection userVerification unknown value"); + new GetCredentialsTest("options.publicKey.userVerification", null) + .addCredential(credPromise) + .runTest("authenticatorSelection userVerification null"); + + // good extension values + new GetCredentialsTest({path: "options.publicKey.extensions", value: undefined}) + .addCredential(credPromise) + .runTest("extensions undefined"); + new GetCredentialsTest("options.publicKey.extensions", {}) + .addCredential(credPromise) + .runTest("extensions are empty object"); + new GetCredentialsTest("options.publicKey.extensions", {foo: "", bar: "", bat: ""}) + .addCredential(credPromise) + .runTest("extensions are dict of empty strings"); +}); + +/* JSHINT */ +/* globals standardSetup, GetCredentialsTest, createCredential */ +</script> diff --git a/testing/web-platform/tests/webauthn/getcredential-prf.https.html b/testing/web-platform/tests/webauthn/getcredential-prf.https.html new file mode 100644 index 0000000000..40dfc5cad3 --- /dev/null +++ b/testing/web-platform/tests/webauthn/getcredential-prf.https.html @@ -0,0 +1,165 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>navigator.credentials.get() prf extension tests with authenticator support</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src=helpers.js></script> +<body></body> +<script> +standardSetup(async function(authenticator) { + "use strict"; + + const b64 = buf => btoa(String.fromCharCode.apply(null, new Uint8Array(buf))); + const b64url = buf => b64(buf). + replace(/\+/g, '-'). + replace(/\//g, '_'). + replace(/=+$/, ''); + + const credential = createCredential({ + options: { + publicKey: { + extensions: { + prf: {}, + }, + }, + }, + }); + + const assert = (id, prfExt) => + navigator.credentials.get({publicKey: { + challenge: new Uint8Array(), + allowCredentials: [{ + id: id, + type: "public-key", + }], + extensions: { + prf: prfExt, + }, + }}); + + promise_test(async t => { + const id = (await credential).rawId; + const assertion = await assert(id, { + eval: { + first: new Uint8Array([1,2,3,4]).buffer, + }, + }); + const results = assertion.getClientExtensionResults().prf.results; + assert_equals(results.first.byteLength, 32) + assert_not_own_property(results, 'second'); + }, "navigator.credentials.get() with single evaluation point"); + + promise_test(async t => { + const id = (await credential).rawId; + const assertion = await assert(id, { + eval: { + first: new Uint8Array([1,2,3,4]).buffer, + second: new Uint8Array([1,2,3,4]).buffer, + }, + }); + const results = assertion.getClientExtensionResults().prf.results; + assert_equals(results.first.byteLength, 32) + assert_equals(results.second.byteLength, 32) + assert_equals(b64(results.first), b64(results.second)); + }, "navigator.credentials.get() with two equal evaluation points"); + + promise_test(async t => { + const id = (await credential).rawId; + const assertion = await assert(id, { + eval: { + first: new Uint8Array([1,2,3,4]).buffer, + second: new Uint8Array([1,2,3,5]).buffer, + }, + }); + const results = assertion.getClientExtensionResults().prf.results; + assert_equals(results.first.byteLength, 32) + assert_equals(results.second.byteLength, 32) + assert_not_equals(b64(results.first), b64(results.second)); + }, "navigator.credentials.get() with two distinct evaluation points"); + + promise_test(async t => { + const id = (await credential).rawId; + const byCred = {}; + byCred[b64url(id)] = { + first: new Uint8Array([1,2,3,4]).buffer, + }; + const assertion = await assert(id, { + evalByCredential: byCred, + }); + const results = assertion.getClientExtensionResults().prf.results; + assert_equals(results.first.byteLength, 32) + assert_not_own_property(results, 'second'); + }, "navigator.credentials.get() using credential ID with one evaluation point"); + + promise_test(async t => { + const id = (await credential).rawId; + const byCred = {}; + byCred[b64url(id)] = { + first: new Uint8Array([1,2,3,4]).buffer, + second: new Uint8Array([1,2,3,4]).buffer, + }; + const assertion = await assert(id, { + evalByCredential: byCred, + }); + const results = assertion.getClientExtensionResults().prf.results; + assert_equals(results.first.byteLength, 32) + assert_equals(results.second.byteLength, 32) + assert_equals(b64(results.first), b64(results.second)); + }, "navigator.credentials.get() using credential ID with two evaluation points"); + + promise_test(async t => { + const id = (await credential).rawId; + const byCred = {}; + byCred["Zm9v"] = { + first: new Uint8Array([1,2,3,4]).buffer, + }; + return promise_rejects_dom(t, "SyntaxError", assert(id, { + evalByCredential: byCred, + })); + }, "navigator.credentials.get() with credential ID not in allowedCredentials"); + + promise_test(async t => { + const id = (await credential).rawId; + const byCred = {}; + byCred["Zm9v"] = { + first: new Uint8Array([1,2,3,4]), + }; + return promise_rejects_dom(t, "SyntaxError", assert(id, { + evalByCredential: byCred, + })); + }, "navigator.credentials.get() with Uint8Array credential ID not in allowedCredentials"); + + promise_test(async t => { + const id = (await credential).rawId; + const byCred = {}; + byCred["Zm9v="] = { + first: new Uint8Array([1,2,3,4]).buffer, + }; + return promise_rejects_dom( + t, "SyntaxError", assert(id, {evalByCredential: byCred })); + }, "navigator.credentials.get() using invalid base64url credential ID"); + + promise_test(async t => { + const id = (await credential).rawId; + const byCred = {}; + byCred["Zm9v"] = { + first: new Uint8Array([1,2,3,4]).buffer, + }; + const promise = navigator.credentials.get({publicKey: { + challenge: new Uint8Array(), + extensions: { + prf: {evalByCredential: byCred }, + }, + }}); + return promise_rejects_dom(t, "NotSupportedError", promise); + }, "navigator.credentials.get() with an empty allow list but also using evalByCredential"); +}, { + protocol: "ctap2_1", + extensions: ["prf"], + hasUserVerification: true, + isUserVerified: true, +}); +</script> diff --git a/testing/web-platform/tests/webauthn/getcredential-rk-passing.https.html b/testing/web-platform/tests/webauthn/getcredential-rk-passing.https.html new file mode 100644 index 0000000000..8c0254fee4 --- /dev/null +++ b/testing/web-platform/tests/webauthn/getcredential-rk-passing.https.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>WebAuthn credential.get() Resident Key Passing Tests</title> +<meta name="timeout" content="long"> +<link rel="help" href="hhttps://w3c.github.io/webauthn/#resident-credential"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src=helpers.js></script> +<body></body> +<script> +standardSetup(function() { + "use strict"; + + // create a resident key credential + var credPromise = createCredential({ + options: { + publicKey: { + authenticatorSelection: { + requireResidentKey: true, + } + } + } + }); + + // empty allowCredential should find the requireResidentKey: true credential + new GetCredentialsTest({path: "options.publicKey.allowCredentials", value: []}) + .addCredential(credPromise) + .setIsResidentKeyTest(true) + .runTest("empty allowCredentials"); + + // undefined allowCredential should be equivalent to empty + new GetCredentialsTest({path: "options.publicKey.allowCredentials", value: undefined}) + .addCredential(credPromise) + .setIsResidentKeyTest(true) + .runTest("undefined allowCredentials"); +}, { + // browsers may not allow resident key credential creation without uv + protocol: "ctap2", + hasResidentKey: true, + hasUserVerification: true, + isUserVerified: true, +}); + +/* JSHINT */ +/* globals standardSetup, GetCredentialsTest, createCredential */ +</script> diff --git a/testing/web-platform/tests/webauthn/getcredential-timeout.https.html b/testing/web-platform/tests/webauthn/getcredential-timeout.https.html new file mode 100644 index 0000000000..c4d8aed38c --- /dev/null +++ b/testing/web-platform/tests/webauthn/getcredential-timeout.https.html @@ -0,0 +1,72 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>WebAuthn navigator.credentials.get() timeout Tests</title> +<meta name="timeout" content="long"> +<link rel="author" title="Adam Powers" href="mailto:adam@fidoalliance.org"> +<link rel="help" href="https://w3c.github.io/webauthn/#iface-credential"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src=helpers.js></script> +<body></body> +<script> +promise_test(async t => { + "use strict"; + + let credentialId; + try { + // if available, set up a mock authenticator that does not respond to user input with a credential + let authenticator = await window.test_driver.add_virtual_authenticator({ + protocol: "ctap1/u2f", + transport: "usb", + isUserConsenting: false, + }); + t.add_cleanup(() => window.test_driver.remove_virtual_authenticator(authenticator)); + const private_key = + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg8_zMDQDYAxlU-Q" + + "hk1Dwkf0v18GZca1DMF3SaJ9HPdmShRANCAASNYX5lyVCOZLzFZzrIKmeZ2jwU" + + "RmgsJYxGP__fWN_S-j5sN4tT15XEpN_7QZnt14YvI6uvAgO0uJEboFaZlOEB"; + credentialId = new Uint8Array([..."cred-1"].map(c => c.charCodeAt(0))); + await window.test_driver.add_credential(authenticator, { + credentialId: btoa("cred-1"), + rpId: window.location.hostname, + privateKey: private_key, + signCount: 0, + isResidentCredential: false, + }); + } catch (error) { + if (error !== "error: Action add_virtual_authenticator not implemented") { + throw error; + } + // configure a manual authenticator by creating a credential. + credentialId = (await createCredential()).rawId; + } + + // bad timeout values + // TODO: there is some debate as to whether MAX_UNSIGNED_LONG + 1 and / or -1 should be disallowed since they get converted to valid values internally + // new GetCredentialsTest({path: "options.publicKey.timeout", value: -1}) + // .addCredential(credPromise) + // .runTest("Bad timeout: negative", TypeError); + // new GetCredentialsTest({path: "options.publicKey.timeout", value: 4294967295 + 1}) + // .addCredential(credPromise) + // .runTest("Bad timeout: too big", TypeError); + + // timeout test + return promise_rejects_dom(t, "NotAllowedError", navigator.credentials.get({ + publicKey: { + challenge: new Uint8Array([1, 2, 3]), + allowCredentials: [{ + id: credentialId, + type: "public-key", + }], + timeout: 1, + }, + })); + // TODO: createCredential.timeout > 1s && setTimeout < 1s + // TODO: createCredential.timeout < 5s && setTimeout > 5s +}); + +/* JSHINT */ +/* globals standardSetup, GetCredentialsTest, createCredential, promise_rejects_dom */ +</script> diff --git a/testing/web-platform/tests/webauthn/helpers.js b/testing/web-platform/tests/webauthn/helpers.js new file mode 100644 index 0000000000..56d941a3ed --- /dev/null +++ b/testing/web-platform/tests/webauthn/helpers.js @@ -0,0 +1,628 @@ +// Useful constants for working with COSE key objects +const cose_kty = 1; +const cose_kty_ec2 = 2; +const cose_alg = 3; +const cose_alg_ECDSA_w_SHA256 = -7; +const cose_alg_ECDSA_w_SHA512 = -36; +const cose_crv = -1; +const cose_crv_P256 = 1; +const cose_crv_x = -2; +const cose_crv_y = -3; + +/** + * These are the default arguments that will be passed to navigator.credentials.create() + * unless modified by a specific test case + */ +var createCredentialDefaultArgs = { + options: { + publicKey: { + // Relying Party: + rp: { + name: "Acme", + }, + + // User: + user: { + id: new Uint8Array(16), // Won't survive the copy, must be rebuilt + name: "john.p.smith@example.com", + displayName: "John P. Smith", + }, + + pubKeyCredParams: [{ + type: "public-key", + alg: cose_alg_ECDSA_w_SHA256, + }], + + authenticatorSelection: { + requireResidentKey: false, + }, + + timeout: 60000, // 1 minute + excludeCredentials: [] // No excludeList + } + } +}; + +/** + * These are the default arguments that will be passed to navigator.credentials.get() + * unless modified by a specific test case + */ +var getCredentialDefaultArgs = { + options: { + publicKey: { + timeout: 60000 + // allowCredentials: [newCredential] + } + } +}; + +function createCredential(opts) { + opts = opts || {}; + + // set the default options + var createArgs = cloneObject(createCredentialDefaultArgs); + let challengeBytes = new Uint8Array(16); + window.crypto.getRandomValues(challengeBytes); + createArgs.options.publicKey.challenge = challengeBytes; + createArgs.options.publicKey.user.id = new Uint8Array(16); + + // change the defaults with any options that were passed in + extendObject(createArgs, opts); + + // create the credential, return the Promise + return navigator.credentials.create(createArgs.options); +} + +function assertCredential(credential) { + var options = cloneObject(getCredentialDefaultArgs); + let challengeBytes = new Uint8Array(16); + window.crypto.getRandomValues(challengeBytes); + options.challenge = challengeBytes; + options.allowCredentials = [{type: 'public-key', id: credential.rawId}]; + return navigator.credentials.get({publicKey: options}); +} + +function createRandomString(len) { + var text = ""; + var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for(var i = 0; i < len; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +} + + +function ab2str(buf) { + return String.fromCharCode.apply(null, new Uint8Array(buf)); +} + +// Useful constants for working with attestation data +const authenticator_data_user_present = 0x01; +const authenticator_data_user_verified = 0x04; +const authenticator_data_attested_cred_data = 0x40; +const authenticator_data_extension_data = 0x80; + +function parseAuthenticatorData(buf) { + if (buf.byteLength < 37) { + throw new TypeError ("parseAuthenticatorData: buffer must be at least 37 bytes"); + } + + printHex ("authnrData", buf); + + var authnrData = new DataView(buf); + var authnrDataObj = {}; + authnrDataObj.length = buf.byteLength; + + authnrDataObj.rpIdHash = new Uint8Array (buf.slice (0,32)); + authnrDataObj.rawFlags = authnrData.getUint8(32); + authnrDataObj.counter = authnrData.getUint32(33, false); + authnrDataObj.rawCounter = []; + authnrDataObj.rawCounter[0] = authnrData.getUint8(33); + authnrDataObj.rawCounter[1] = authnrData.getUint8(34); + authnrDataObj.rawCounter[2] = authnrData.getUint8(35); + authnrDataObj.rawCounter[3] = authnrData.getUint8(36); + authnrDataObj.flags = {}; + + authnrDataObj.flags.userPresent = (authnrDataObj.rawFlags&authenticator_data_user_present)?true:false; + authnrDataObj.flags.userVerified = (authnrDataObj.rawFlags&authenticator_data_user_verified)?true:false; + authnrDataObj.flags.attestedCredentialData = (authnrDataObj.rawFlags&authenticator_data_attested_cred_data)?true:false; + authnrDataObj.flags.extensionData = (authnrDataObj.rawFlags&authenticator_data_extension_data)?true:false; + + return authnrDataObj; +} + +/** + * TestCase + * + * A generic template for test cases + * Is intended to be overloaded with subclasses that override testObject, testFunction and argOrder + * The testObject is the default arguments for the testFunction + * The default testObject can be modified with the modify() method, making it easy to create new tests based on the default + * The testFunction is the target of the test and is called by the doIt() method. doIt() applies the testObject as arguments via toArgs() + * toArgs() uses argOrder to make sure the resulting array is in the right order of the arguments for the testFunction + */ +class TestCase { + constructor() { + this.testFunction = function() { + throw new Error("Test Function not implemented"); + }; + this.testObject = {}; + this.argOrder = []; + this.ctx = null; + } + + /** + * toObject + * + * return a copy of the testObject + */ + toObject() { + return JSON.parse(JSON.stringify(this.testObject)); // cheap clone + } + + /** + * toArgs + * + * converts test object to an array that is ordered in the same way as the arguments to the test function + */ + toArgs() { + var ret = []; + // XXX, TODO: this won't necessarily produce the args in the right order + for (let idx of this.argOrder) { + ret.push(this.testObject[idx]); + } + return ret; + } + + /** + * modify + * + * update the internal object by a path / value combination + * e.g. : + * modify ("foo.bar", 3) + * accepts three types of args: + * "foo.bar", 3 + * {path: "foo.bar", value: 3} + * [{path: "foo.bar", value: 3}, ...] + */ + modify(arg1, arg2) { + var mods; + + // check for the two argument scenario + if (typeof arg1 === "string" && arg2 !== undefined) { + mods = { + path: arg1, + value: arg2 + }; + } else { + mods = arg1; + } + + // accept a single modification object instead of an array + if (!Array.isArray(mods) && typeof mods === "object") { + mods = [mods]; + } + + // iterate through each of the desired modifications, and call recursiveSetObject on them + for (let idx in mods) { + var mod = mods[idx]; + let paths = mod.path.split("."); + recursiveSetObject(this.testObject, paths, mod.value); + } + + // iterates through nested `obj` using the `pathArray`, creating the path if it doesn't exist + // when the final leaf of the path is found, it is assigned the specified value + function recursiveSetObject(obj, pathArray, value) { + var currPath = pathArray.shift(); + if (typeof obj[currPath] !== "object") { + obj[currPath] = {}; + } + if (pathArray.length > 0) { + return recursiveSetObject(obj[currPath], pathArray, value); + } + obj[currPath] = value; + } + + return this; + } + + /** + * actually runs the test function with the supplied arguments + */ + doIt() { + if (typeof this.testFunction !== "function") { + throw new Error("Test function not found"); + } + + return this.testFunction.call(this.ctx, ...this.toArgs()); + } + + /** + * run the test function with the top-level properties of the test object applied as arguments + * expects the test to pass, and then validates the results + */ + testPasses(desc) { + return this.doIt() + .then((ret) => { + // check the result + this.validateRet(ret); + return ret; + }); + } + + /** + * run the test function with the top-level properties of the test object applied as arguments + * expects the test to fail + */ + testFails(t, testDesc, expectedErr) { + if (typeof expectedErr == "string") { + return promise_rejects_dom(t, expectedErr, this.doIt(), "Expected bad parameters to fail"); + } + + return promise_rejects_js(t, expectedErr, this.doIt(), "Expected bad parameters to fail"); + } + + /** + * Runs the test that's implemented by the class by calling the doIt() function + * @param {String} desc A description of the test being run + * @param [Error|String] expectedErr A string matching an error type, such as "SecurityError" or an object with a .name value that is an error type string + */ + runTest(desc, expectedErr) { + promise_test((t) => { + return Promise.resolve().then(() => { + return this.testSetup(); + }).then(() => { + if (expectedErr === undefined) { + return this.testPasses(desc); + } else { + return this.testFails(t, desc, expectedErr); + } + }).then((res) => { + return this.testTeardown(res); + }) + }, desc) + } + + /** + * called before runTest + * virtual method expected to be overridden by child class if needed + */ + testSetup() { + if (this.beforeTestFn) { + this.beforeTestFn.call(this); + } + + return Promise.resolve(); + } + + /** + * Adds a callback function that gets called in the TestCase context + * and within the testing process. + */ + beforeTest(fn) { + if (typeof fn !== "function") { + throw new Error ("Tried to call non-function before test"); + } + + this.beforeTestFn = fn; + + return this; + } + + /** + * called after runTest + * virtual method expected to be overridden by child class if needed + */ + testTeardown(res) { + if (this.afterTestFn) { + this.afterTestFn.call(this, res); + } + + return Promise.resolve(); + } + + /** + * Adds a callback function that gets called in the TestCase context + * and within the testing process. Good for validating results. + */ + afterTest(fn) { + if (typeof fn !== "function") { + throw new Error ("Tried to call non-function after test"); + } + + this.afterTestFn = fn; + + return this; + } + + /** + * validates the value returned from the test function + * virtual method expected to be overridden by child class + */ + validateRet() { + throw new Error("Not implemented"); + } +} + +function cloneObject(o) { + return JSON.parse(JSON.stringify(o)); +} + +function extendObject(dst, src) { + Object.keys(src).forEach(function(key) { + if (isSimpleObject(src[key]) && !isAbortSignal(src[key])) { + dst[key] ||= {}; + extendObject(dst[key], src[key]); + } else { + dst[key] = src[key]; + } + }); +} + +function isSimpleObject(o) { + return (typeof o === "object" && + !Array.isArray(o) && + !(o instanceof ArrayBuffer)); +} + +function isAbortSignal(o) { + return (o instanceof AbortSignal); +} + +/** + * CreateCredentialTest + * + * tests the WebAuthn navigator.credentials.create() interface + */ +class CreateCredentialsTest extends TestCase { + constructor() { + // initialize the parent class + super(); + + // the function to be tested + this.testFunction = navigator.credentials.create; + // the context to call the test function with (i.e. - the 'this' object for the function) + this.ctx = navigator.credentials; + + // the default object to pass to makeCredential, to be modified with modify() for various tests + let challengeBytes = new Uint8Array(16); + window.crypto.getRandomValues(challengeBytes); + this.testObject = cloneObject(createCredentialDefaultArgs); + // cloneObject can't clone the BufferSource in user.id, so let's recreate it. + this.testObject.options.publicKey.user.id = new Uint8Array(16); + this.testObject.options.publicKey.challenge = challengeBytes; + + // how to order the properties of testObject when passing them to makeCredential + this.argOrder = [ + "options" + ]; + + // enable the constructor to modify the default testObject + // would prefer to do this in the super class, but have to call super() before using `this.*` + if (arguments.length) this.modify(...arguments); + } + + validateRet(ret) { + validatePublicKeyCredential(ret); + validateAuthenticatorAttestationResponse(ret.response); + } +} + +/** + * GetCredentialsTest + * + * tests the WebAuthn navigator.credentials.get() interface + */ +class GetCredentialsTest extends TestCase { + constructor(...args) { + // initialize the parent class + super(); + + // the function to be tested + this.testFunction = navigator.credentials.get; + // the context to call the test function with (i.e. - the 'this' object for the function) + this.ctx = navigator.credentials; + + // default arguments + let challengeBytes = new Uint8Array(16); + window.crypto.getRandomValues(challengeBytes); + this.testObject = cloneObject(getCredentialDefaultArgs); + this.testObject.options.publicKey.challenge = challengeBytes; + + // how to order the properties of testObject when passing them to makeCredential + this.argOrder = [ + "options" + ]; + + this.credentialPromiseList = []; + + // set to true to pass an empty allowCredentials list to credentials.get + this.isResidentKeyTest = false; + + // enable the constructor to modify the default testObject + // would prefer to do this in the super class, but have to call super() before using `this.*` + if (arguments.length) { + if (args.cred instanceof Promise) this.credPromise = args.cred; + else if (typeof args.cred === "object") this.credPromise = Promise.resolve(args.cred); + delete args.cred; + this.modify(...arguments); + } + } + + addCredential(arg) { + // if a Promise was passed in, add it to the list + if (arg instanceof Promise) { + this.credentialPromiseList.push(arg); + return this; + } + + // if a credential object was passed in, convert it to a Promise for consistency + if (typeof arg === "object") { + this.credentialPromiseList.push(Promise.resolve(arg)); + return this; + } + + // if no credential specified then create one + var p = createCredential(); + this.credentialPromiseList.push(p); + + return this; + } + + testSetup(desc) { + if (!this.credentialPromiseList.length) { + throw new Error("Attempting list without defining credential to test"); + } + + return Promise.all(this.credentialPromiseList) + .then((credList) => { + var idList = credList.map((cred) => { + return { + id: cred.rawId, + transports: ["usb", "nfc", "ble"], + type: "public-key" + }; + }); + if (!this.isResidentKeyTest) { + this.testObject.options.publicKey.allowCredentials = idList; + } + // return super.test(desc); + }) + .catch((err) => { + throw Error(err); + }); + } + + validateRet(ret) { + validatePublicKeyCredential(ret); + validateAuthenticatorAssertionResponse(ret.response); + } + + setIsResidentKeyTest(isResidentKeyTest) { + this.isResidentKeyTest = isResidentKeyTest; + return this; + } +} + +/** + * converts a uint8array to base64 url-safe encoding + * based on similar function in resources/utils.js + */ +function base64urlEncode(array) { + let string = String.fromCharCode.apply(null, array); + let result = btoa(string); + return result.replace(/=+$/g, '').replace(/\+/g, "-").replace(/\//g, "_"); +} +/** + * runs assertions against a PublicKeyCredential object to ensure it is properly formatted + */ +function validatePublicKeyCredential(cred) { + // class + assert_class_string(cred, "PublicKeyCredential", "Expected return to be instance of 'PublicKeyCredential' class"); + // id + assert_idl_attribute(cred, "id", "should return PublicKeyCredential with id attribute"); + assert_readonly(cred, "id", "should return PublicKeyCredential with readonly id attribute"); + // rawId + assert_idl_attribute(cred, "rawId", "should return PublicKeyCredential with rawId attribute"); + assert_readonly(cred, "rawId", "should return PublicKeyCredential with readonly rawId attribute"); + assert_equals(cred.id, base64urlEncode(new Uint8Array(cred.rawId)), "should return PublicKeyCredential with id attribute set to base64 encoding of rawId attribute"); + + // type + assert_idl_attribute(cred, "type", "should return PublicKeyCredential with type attribute"); + assert_equals(cred.type, "public-key", "should return PublicKeyCredential with type 'public-key'"); +} + +/** + * runs assertions against a AuthenticatorAttestationResponse object to ensure it is properly formatted + */ +function validateAuthenticatorAttestationResponse(attr) { + // class + assert_class_string(attr, "AuthenticatorAttestationResponse", "Expected credentials.create() to return instance of 'AuthenticatorAttestationResponse' class"); + + // clientDataJSON + assert_idl_attribute(attr, "clientDataJSON", "credentials.create() should return AuthenticatorAttestationResponse with clientDataJSON attribute"); + assert_readonly(attr, "clientDataJSON", "credentials.create() should return AuthenticatorAttestationResponse with readonly clientDataJSON attribute"); + // TODO: clientDataJSON() and make sure fields are correct + + // attestationObject + assert_idl_attribute(attr, "attestationObject", "credentials.create() should return AuthenticatorAttestationResponse with attestationObject attribute"); + assert_readonly(attr, "attestationObject", "credentials.create() should return AuthenticatorAttestationResponse with readonly attestationObject attribute"); + // TODO: parseAuthenticatorData() and make sure flags are correct +} + +/** + * runs assertions against a AuthenticatorAssertionResponse object to ensure it is properly formatted + */ +function validateAuthenticatorAssertionResponse(assert) { + // class + assert_class_string(assert, "AuthenticatorAssertionResponse", "Expected credentials.create() to return instance of 'AuthenticatorAssertionResponse' class"); + + // clientDataJSON + assert_idl_attribute(assert, "clientDataJSON", "credentials.get() should return AuthenticatorAssertionResponse with clientDataJSON attribute"); + assert_readonly(assert, "clientDataJSON", "credentials.get() should return AuthenticatorAssertionResponse with readonly clientDataJSON attribute"); + // TODO: clientDataJSON() and make sure fields are correct + + // signature + assert_idl_attribute(assert, "signature", "credentials.get() should return AuthenticatorAssertionResponse with signature attribute"); + assert_readonly(assert, "signature", "credentials.get() should return AuthenticatorAssertionResponse with readonly signature attribute"); + + // authenticatorData + assert_idl_attribute(assert, "authenticatorData", "credentials.get() should return AuthenticatorAssertionResponse with authenticatorData attribute"); + assert_readonly(assert, "authenticatorData", "credentials.get() should return AuthenticatorAssertionResponse with readonly authenticatorData attribute"); + // TODO: parseAuthenticatorData() and make sure flags are correct +} + +function defaultAuthenticatorArgs() { + return { + protocol: 'ctap1/u2f', + transport: 'usb', + hasResidentKey: false, + hasUserVerification: false, + isUserVerified: false, + }; +} + +function standardSetup(cb, options = {}) { + // Setup an automated testing environment if available. + let authenticatorArgs = Object.assign(defaultAuthenticatorArgs(), options); + window.test_driver.add_virtual_authenticator(authenticatorArgs) + .then(authenticator => { + cb(); + // XXX add a subtest to clean up the virtual authenticator since + // testharness does not support waiting for promises on cleanup. + promise_test( + () => + window.test_driver.remove_virtual_authenticator(authenticator), + 'Clean up the test environment'); + }) + .catch(error => { + if (error !== + 'error: Action add_virtual_authenticator not implemented') { + throw error; + } + // The protocol is not available. Continue manually. + cb(); + }); +} + +// virtualAuthenticatorPromiseTest runs |testCb| in a promise_test with a +// virtual authenticator set up before and destroyed after the test, if the +// virtual testing API is available. In manual tests, setup and teardown is +// skipped. +function virtualAuthenticatorPromiseTest( + testCb, options = {}, name = 'Virtual Authenticator Test') { + let authenticatorArgs = Object.assign(defaultAuthenticatorArgs(), options); + promise_test(async t => { + try { + let authenticator = + await window.test_driver.add_virtual_authenticator(authenticatorArgs); + t.add_cleanup( + () => window.test_driver.remove_virtual_authenticator(authenticator)); + } catch (error) { + if (error !== 'error: Action add_virtual_authenticator not implemented') { + throw error; + } + } + return testCb(t); + }, name); +} diff --git a/testing/web-platform/tests/webauthn/idlharness-manual.https.window.js b/testing/web-platform/tests/webauthn/idlharness-manual.https.window.js new file mode 100644 index 0000000000..884702753d --- /dev/null +++ b/testing/web-platform/tests/webauthn/idlharness-manual.https.window.js @@ -0,0 +1,53 @@ +// META: timeout=long +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js +// META: script=helpers.js + +// https://w3c.github.io/webauthn/ + +'use strict'; + +idl_test( + ['webauthn'], + ['credential-management'], + async idlArray => { + idlArray.add_untested_idls("[Exposed=(Window,Worker)] interface ArrayBuffer {};"); + + idlArray.add_objects({ + PublicKeyCredential: ['cred', 'assertion'], + AuthenticatorAttestationResponse: ['cred.response'], + AuthenticatorAssertionResponse: ['assertion.response'] + }); + + const challengeBytes = new Uint8Array(16); + window.crypto.getRandomValues(challengeBytes); + + self.cred = await Promise.race([ + new Promise((_, reject) => window.setTimeout(() => { + reject('Timed out waiting for user to touch security key') + }, 3000)), + createCredential({ + options: { + publicKey: { + timeout: 3000, + user: { + id: new Uint8Array(16), + }, + } + } + }), + ]); + + self.assertion = await navigator.credentials.get({ + publicKey: { + timeout: 3000, + allowCredentials: [{ + id: cred.rawId, + transports: ["usb", "nfc", "ble"], + type: "public-key" + }], + challenge: challengeBytes, + } + }); + } +); diff --git a/testing/web-platform/tests/webauthn/idlharness.https.window.js b/testing/web-platform/tests/webauthn/idlharness.https.window.js new file mode 100644 index 0000000000..ff0efcb656 --- /dev/null +++ b/testing/web-platform/tests/webauthn/idlharness.https.window.js @@ -0,0 +1,21 @@ +// META: timeout=long +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js +// META: script=helpers.js + +// https://w3c.github.io/webauthn/ + +'use strict'; + +idl_test( + ['webauthn'], + ['credential-management'], + async idlArray => { + // NOTE: The following are tested in idlharness-manual.https.window.js: + // idlArray.add_objects({ + // PublicKeyCredential: ['cred', 'assertion'], + // AuthenticatorAttestationResponse: ['cred.response'], + // AuthenticatorAssertionResponse: ['assertion.response'] + // }); + } +); diff --git a/testing/web-platform/tests/webauthn/public-key-credential-to-json.https.window.js b/testing/web-platform/tests/webauthn/public-key-credential-to-json.https.window.js new file mode 100644 index 0000000000..8de3b8c3cd --- /dev/null +++ b/testing/web-platform/tests/webauthn/public-key-credential-to-json.https.window.js @@ -0,0 +1,157 @@ +// META: script=/resources/testharness.js +// META: script=/resources/testharnessreport.js +// META: script=/resources/testdriver.js +// META: script=/resources/testdriver-vendor.js +// META: script=/resources/utils.js +// META: script=helpers.js + +function assertObjectKeysEq(a, b) { + let a_keys = new Set(Object.keys(a)); + let b_keys = new Set(Object.keys(b)); + assert_true( + a_keys.length == b_keys.length && [...a_keys].every(k => b_keys.has(k)), + `keys differ: ${a_keys} != ${b_keys}`); +} + +// Returns the JSON encoding for `value`. If `value` is a function, `optParent` +// is the object to which execution should be bound. +function convertValue(value, optParent) { + switch (typeof value) { + case 'undefined': + case 'boolean': + case 'number': + case 'bigint': + case 'string': + case 'symbol': + return value; + case 'function': + return value.apply(optParent); + case 'object': + if (value.__proto__.constructor === Object) { + var result = {}; + Object.entries(value).map((k, v) => { + result[k] = convertValue(k, v); + }); + return result; + } + if (value instanceof Array) { + return value.map(convertValue); + } + if (value instanceof ArrayBuffer) { + return base64urlEncode(new Uint8Array(value)); + } + throw `can't convert value ${value} in ${parent}`; + default: + throw `${value} has unexpected type`; + } +} + +// Conversion spec for a single attribute. +// @typedef {Object} ConvertParam +// @property {string} name - The name of the attribute to convert from +// @property {string=} target - The name of the attribute to convert to, if +// different from `name` +// @property {function=} func - Method to convert this property. Defaults to +// convertValue(). + +// Returns the JSON object for `obj`. +// +// @param obj +// @param {Array<(string|ConvertParam)>} keys - The names of parameters in +// `obj` to convert, or instances of ConvertParam for complex cases. +function convertObject(obj, params) { + let result = {}; + params.forEach((param) => { + switch (typeof (param)) { + case 'string': + assert_true(param in obj, `missing ${param}`); + if (obj[param] !== null) { + result[param] = convertValue(obj[param], obj); + } + break; + case 'object': + assert_true(param.name in obj, `missing ${param.name}`); + const val = obj[param.name]; + const target_key = param.target || param.name; + const convert_func = param.func || convertValue; + try { + result[target_key] = + convert_func(((typeof val) == 'function' ? val.apply(obj) : val)); + } catch (e) { + throw `failed to convert ${param.name}: ${e}` + } + break; + default: + throw `invalid key ${param}`; + } + }); + return result; +} + +// Converts an AuthenticatorResponse instance into a JSON object. +// @param {!AuthenticatorResponse} +function authenticatorResponseToJson(response) { + assert_true( + (response instanceof AuthenticatorAttestationResponse) || + (response instanceof AuthenticatorAssertionResponse)); + const isAttestation = (response instanceof AuthenticatorAttestationResponse); + const keys = + (isAttestation ? + [ + 'clientDataJSON', 'attestationObject', + {name: 'getTransports', target: 'transports'} + ] : + ['clientDataJSON', 'authenticatorData', 'signature', 'userHandle']); + return convertObject(response, keys); +} + +// Converts a PublicKeyCredential instance to a JSON object. +// @param {!PublicKeyCredential} +function publicKeyCredentialToJson(cred) { + const keys = [ + 'id', 'rawId', {name: 'response', func: authenticatorResponseToJson}, + 'authenticatorAttachment', + {name: 'getClientExtensionResults', target: 'clientExtensionResults'}, + 'type' + ]; + return convertObject(cred, keys); +} + +// Returns a copy of `jsonObj`, which must be a JSON type, with object keys +// recursively sorted in lexicographic order; or simply `jsonObj` if it is not +// an instance of Object. +function deepSortKeys(jsonObj) { + if (typeof jsonObj !== 'object' || jsonObj === null || + jsonObj.__proto__.constructor !== Object || + Object.keys(jsonObj).length === 0) { + return jsonObj; + } + return Object.keys(jsonObj).sort().reduce((acc, key) => { + acc[key] = deepSortKeys(jsonObj[key]); + return acc; + }, {}); +} + +// Asserts that `actual` and `expected`, which are both JSON types, are equal. +// The object key order is ignored for comparison. +function assertJsonEquals(actual, expected, optMsg) { + assert_equals( + JSON.stringify(deepSortKeys(actual)), + JSON.stringify(deepSortKeys(expected)), optMsg); +} + +virtualAuthenticatorPromiseTest( + async t => { + let credential = await createCredential(); + assertJsonEquals( + credential.toJSON(), publicKeyCredentialToJson(credential)); + + let assertion = await assertCredential(credential); + assertJsonEquals( + assertion.toJSON(), publicKeyCredentialToJson(assertion)); + }, + { + protocol: 'ctap2_1', + transport: 'usb', + }, + 'toJSON()'); diff --git a/testing/web-platform/tests/webauthn/remote-desktop-client-override.tentative.https.html b/testing/web-platform/tests/webauthn/remote-desktop-client-override.tentative.https.html new file mode 100644 index 0000000000..888dca2e9c --- /dev/null +++ b/testing/web-platform/tests/webauthn/remote-desktop-client-override.tentative.https.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>remoteDesktopClientOverride</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src=helpers.js></script> +<body></body> +<script> +"use strict"; + +const remoteDesktopClientOverride = { + origin: "https://acme.com", + sameOriginWithAncestors: false, +}; + +virtualAuthenticatorPromiseTest(async t => { + let promise = createCredential({ + options: { + publicKey: { + extensions: { + remoteDesktopClientOverride: remoteDesktopClientOverride, + }, + }, + }, + }); + // Site isn't authorized to use the extension. + return promise_rejects_dom(t, "NotAllowedError", promise); +}, { + protocol: "ctap2_1", + transport: "usb", +}, "create() with remoteDesktopClientOverride"); + +virtualAuthenticatorPromiseTest(async t => { + let promise = navigator.credentials.get({publicKey: { + challenge: new Uint8Array(), + allowCredentials: [{ + id: (await createCredential()).rawId, + type: "public-key", + }], + extensions: { + remoteDesktopClientOverride: remoteDesktopClientOverride, + }, + }}); + // Site isn't authorized to use the extension. + return promise_rejects_dom(t, "NotAllowedError", promise); +}, { + protocol: "ctap2_1", + transport: "usb", +}, "get() with remoteDesktopClientOverride on an unauthorized site"); +</script> diff --git a/testing/web-platform/tests/webauthn/resources/common-inputs.js b/testing/web-platform/tests/webauthn/resources/common-inputs.js new file mode 100644 index 0000000000..e60fed45e8 --- /dev/null +++ b/testing/web-platform/tests/webauthn/resources/common-inputs.js @@ -0,0 +1,33 @@ +const ES256_ID = -7; +const CHALLENGE = "climb the mountain"; + +const PUBLIC_KEY_RP = { + id: window.location.hostname, + name: "Example RP", +}; + +const PUBLIC_KEY_USER = { + id: new TextEncoder().encode("123456789"), + name: "madeline@example.com", + displayName: "Madeline", +}; + +// ES256. +const PUBLIC_KEY_PARAMETERS = [{ + type: "public-key", + alg: ES256_ID, +}]; + +const AUTHENTICATOR_SELECTION_CRITERIA = { + requireResidentKey: false, + userVerification: "discouraged", +}; + +const MAKE_CREDENTIAL_OPTIONS = { + challenge: new TextEncoder().encode(CHALLENGE), + rp: PUBLIC_KEY_RP, + user: PUBLIC_KEY_USER, + pubKeyCredParams: PUBLIC_KEY_PARAMETERS, + authenticatorSelection: AUTHENTICATOR_SELECTION_CRITERIA, + excludeCredentials: [], +}; diff --git a/testing/web-platform/tests/webauthn/resources/utils.js b/testing/web-platform/tests/webauthn/resources/utils.js new file mode 100644 index 0000000000..50c39605a1 --- /dev/null +++ b/testing/web-platform/tests/webauthn/resources/utils.js @@ -0,0 +1,340 @@ +"use strict"; + +// Encodes |data| into base64url string. There is no '=' padding, and the +// characters '-' and '_' must be used instead of '+' and '/', respectively. +function base64urlEncode(data) { + let result = btoa(data); + return result.replace(/=+$/g, '').replace(/\+/g, "-").replace(/\//g, "_"); +} + +// Decode |encoded| using base64url decoding. +function base64urlDecode(encoded) { + return atob(encoded.replace(/\-/g, "+").replace(/\_/g, "/")); +} + +// Encodes a Uint8Array as a base64url string. +function uint8ArrayToBase64url(array) { + return base64urlEncode(String.fromCharCode.apply(null, array)); +} + +// Encodes a Uint8Array to lowercase hex. +function uint8ArrayToHex(array) { + const hexTable = '0123456789abcdef'; + let s = ''; + for (let i = 0; i < array.length; i++) { + s += hexTable.charAt(array[i] >> 4); + s += hexTable.charAt(array[i] & 15); + } + return s; +} + +// Convert a EC signature from DER to a concatenation of the r and s parameters, +// as expected by the subtle crypto API. +function convertDERSignatureToSubtle(der) { + let index = -1; + const SEQUENCE = 0x30; + const INTEGER = 0x02; + assert_equals(der[++index], SEQUENCE); + + let size = der[++index]; + assert_equals(size + 2, der.length); + + assert_equals(der[++index], INTEGER); + let rSize = der[++index]; + ++index; + while (der[index] == 0) { + ++index; + --rSize; + } + let r = der.slice(index, index + rSize); + index += rSize; + + assert_equals(der[index], INTEGER); + let sSize = der[++index]; + ++index; + while (der[index] == 0) { + ++index; + --sSize; + } + let s = der.slice(index, index + sSize); + assert_equals(index + sSize, der.length); + + let result = new Uint8Array(64); + result.set(r, 32 - rSize); + result.set(s, 64 - sSize); + return result; +}; + +function coseObjectToJWK(cose) { + // Convert an object representing a COSE_Key encoded public key into a JSON + // Web Key object. + // https://tools.ietf.org/html/rfc7517 + + // The example used on the test is a ES256 key, so we only implement that. + let jwk = {}; + if (cose.type != 2) + assert_unreached("Unknown type: " + cose.type); + + jwk.kty = "EC"; + if (cose.alg != ES256_ID) + assert_unreached("Unknown alg: " + cose.alg); + + if (cose.crv != 1) + assert_unreached("Unknown curve: " + jwk.crv); + + jwk.crv = "P-256"; + jwk.x = uint8ArrayToBase64url(cose.x); + jwk.y = uint8ArrayToBase64url(cose.y); + return jwk; +} + +function parseCosePublicKey(coseKey) { + // Parse a CTAP2 canonical CBOR encoding form key. + // https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-client-to-authenticator-protocol-v2.0-id-20180227.html#ctap2-canonical-cbor-encoding-form + let parsed = new Cbor(coseKey); + let cbor = parsed.getCBOR(); + let key = { + type: cbor[1], + alg: cbor[3], + }; + if (key.type != 2) + assert_unreached("Unknown key type: " + key.type); + + key.crv = cbor[-1]; + key.x = new Uint8Array(cbor[-2]); + key.y = new Uint8Array(cbor[-3]); + return key; +} + +function parseAttestedCredentialData(attestedCredentialData) { + // Parse the attested credential data according to + // https://w3c.github.io/webauthn/#attested-credential-data + let aaguid = attestedCredentialData.slice(0, 16); + let credentialIdLength = (attestedCredentialData[16] << 8) + + attestedCredentialData[17]; + let credentialId = + attestedCredentialData.slice(18, 18 + credentialIdLength); + let credentialPublicKey = parseCosePublicKey( + attestedCredentialData.slice(18 + credentialIdLength, + attestedCredentialData.length)); + + return { aaguid, credentialIdLength, credentialId, credentialPublicKey }; +} + +function parseAuthenticatorData(authenticatorData) { + // Parse the authenticator data according to + // https://w3c.github.io/webauthn/#sctn-authenticator-data + assert_greater_than_equal(authenticatorData.length, 37); + let flags = authenticatorData[32]; + let counter = authenticatorData.slice(33, 37); + + let attestedCredentialData = authenticatorData.length > 37 ? + parseAttestedCredentialData(authenticatorData.slice(37)) : null; + let extensions = null; + if (attestedCredentialData && + authenticatorData.length > 37 + attestedCredentialData.length) { + extensions = authenticatorData.slice(37 + attestedCredentialData.length); + } + + return { + rpIdHash: authenticatorData.slice(0, 32), + flags: { + up: !!(flags & 0x01), + uv: !!(flags & 0x04), + at: !!(flags & 0x40), + ed: !!(flags & 0x80), + }, + counter: (counter[0] << 24) + + (counter[1] << 16) + + (counter[2] << 8) + + counter[3], + attestedCredentialData, + extensions, + }; +} + +// Taken from +// https://cs.chromium.org/chromium/src/chrome/browser/resources/cryptotoken/cbor.js?rcl=c9b6055cf9c158fb4119afd561a591f8fc95aefe +class Cbor { + constructor(buffer) { + this.slice = new Uint8Array(buffer); + } + get data() { + return this.slice; + } + get length() { + return this.slice.length; + } + get empty() { + return this.slice.length == 0; + } + get hex() { + return uint8ArrayToHex(this.data); + } + compare(other) { + if (this.length < other.length) { + return -1; + } else if (this.length > other.length) { + return 1; + } + for (let i = 0; i < this.length; i++) { + if (this.slice[i] < other.slice[i]) { + return -1; + } else if (this.slice[i] > other.slice[i]) { + return 1; + } + } + return 0; + } + getU8() { + if (this.empty) { + throw('Cbor: empty during getU8'); + } + const byte = this.slice[0]; + this.slice = this.slice.subarray(1); + return byte; + } + skip(n) { + if (this.length < n) { + throw('Cbor: too few bytes to skip'); + } + this.slice = this.slice.subarray(n); + } + getBytes(n) { + if (this.length < n) { + throw('Cbor: insufficient bytes in getBytes'); + } + const ret = this.slice.subarray(0, n); + this.slice = this.slice.subarray(n); + return ret; + } + getCBORHeader() { + const copy = new Cbor(this.slice); + const a = this.getU8(); + const majorType = a >> 5; + const info = a & 31; + if (info < 24) { + return [majorType, info, new Cbor(copy.getBytes(1))]; + } else if (info < 28) { + const lengthLength = 1 << (info - 24); + let data = this.getBytes(lengthLength); + let value = 0; + for (let i = 0; i < lengthLength; i++) { + // Javascript has problems handling uint64s given the limited range of + // a double. + if (value > 35184372088831) { + throw('Cbor: cannot represent CBOR number'); + } + // Not using bitwise operations to avoid truncating to 32 bits. + value *= 256; + value += data[i]; + } + switch (lengthLength) { + case 1: + if (value < 24) { + throw('Cbor: value should have been encoded in single byte'); + } + break; + case 2: + if (value < 256) { + throw('Cbor: non-minimal integer'); + } + break; + case 4: + if (value < 65536) { + throw('Cbor: non-minimal integer'); + } + break; + case 8: + if (value < 4294967296) { + throw('Cbor: non-minimal integer'); + } + break; + } + return [majorType, value, new Cbor(copy.getBytes(1 + lengthLength))]; + } else { + throw('Cbor: CBOR contains unhandled info value ' + info); + } + } + getCBOR() { + const [major, value] = this.getCBORHeader(); + switch (major) { + case 0: + return value; + case 1: + return 0 - (1 + value); + case 2: + return this.getBytes(value); + case 3: + return this.getBytes(value); + case 4: { + let ret = new Array(value); + for (let i = 0; i < value; i++) { + ret[i] = this.getCBOR(); + } + return ret; + } + case 5: + if (value == 0) { + return {}; + } + let copy = new Cbor(this.data); + const [firstKeyMajor] = copy.getCBORHeader(); + if (firstKeyMajor == 3) { + // String-keyed map. + let lastKeyHeader = new Cbor(new Uint8Array(0)); + let lastKeyBytes = new Cbor(new Uint8Array(0)); + let ret = {}; + for (let i = 0; i < value; i++) { + const [keyMajor, keyLength, keyHeader] = this.getCBORHeader(); + if (keyMajor != 3) { + throw('Cbor: non-string in string-valued map'); + } + const keyBytes = new Cbor(this.getBytes(keyLength)); + if (i > 0) { + const headerCmp = lastKeyHeader.compare(keyHeader); + if (headerCmp > 0 || + (headerCmp == 0 && lastKeyBytes.compare(keyBytes) >= 0)) { + throw( + 'Cbor: map keys in wrong order: ' + lastKeyHeader.hex + + '/' + lastKeyBytes.hex + ' ' + keyHeader.hex + '/' + + keyBytes.hex); + } + } + lastKeyHeader = keyHeader; + lastKeyBytes = keyBytes; + ret[keyBytes.parseUTF8()] = this.getCBOR(); + } + return ret; + } else if (firstKeyMajor == 0 || firstKeyMajor == 1) { + // Number-keyed map. + let lastKeyHeader = new Cbor(new Uint8Array(0)); + let ret = {}; + for (let i = 0; i < value; i++) { + let [keyMajor, keyValue, keyHeader] = this.getCBORHeader(); + if (keyMajor != 0 && keyMajor != 1) { + throw('Cbor: non-number in number-valued map'); + } + if (i > 0 && lastKeyHeader.compare(keyHeader) >= 0) { + throw( + 'Cbor: map keys in wrong order: ' + lastKeyHeader.hex + ' ' + + keyHeader.hex); + } + lastKeyHeader = keyHeader; + if (keyMajor == 1) { + keyValue = 0 - (1 + keyValue); + } + ret[keyValue] = this.getCBOR(); + } + return ret; + } else { + throw('Cbor: map keyed by invalid major type ' + firstKeyMajor); + } + default: + throw('Cbor: unhandled major type ' + major); + } + } + parseUTF8() { + return (new TextDecoder('utf-8')).decode(this.slice); + } +} diff --git a/testing/web-platform/tests/webauthn/securecontext.http.html b/testing/web-platform/tests/webauthn/securecontext.http.html new file mode 100644 index 0000000000..27d2dbfce3 --- /dev/null +++ b/testing/web-platform/tests/webauthn/securecontext.http.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>WebAuthn Secure Context Tests</title> +<link rel="author" title="Adam Powers" href="mailto:adam@fidoalliance.org"> +<link rel="help" href="https://w3c.github.io/webauthn/#iface-credential"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src=helpers.js></script> +<body></body> +<script> +"use strict"; + +// See https://www.w3.org/TR/secure-contexts/ +// Section 1.1 - 1.4 for list of examples referenced below + +// Example 1 +// http://example.com/ opened in a top-level browsing context is not a secure context, as it was not delivered over an authenticated and encrypted channel. +test (() => { + assert_false (typeof navigator.credentials === "object" && typeof navigator.credentials.create === "function"); +}, "no navigator.credentials.create in non-secure context"); + +// Example 4: TODO +// If a non-secure context opens https://example.com/ in a new window, then things are more complicated. The new window’s status depends on how it was opened. If the non-secure context can obtain a reference to the secure context, or vice-versa, then the new window is not a secure context. +// +// This means that the following will both produce non-secure contexts: +//<a href="https://example.com/" target="_blank">Link!</a> +// <script> +// var w = window.open("https://example.com/"); +// < /script> + +// Example 6: TODO +// If https://example.com/ was somehow able to frame http://non-secure.example.com/ (perhaps the user has overridden mixed content checking?), the top-level frame would remain secure, but the framed content is not a secure context. + +// Example 7: TODO +// If, on the other hand, https://example.com/ is framed inside of http://non-secure.example.com/, then it is not a secure context, as its ancestor is not delivered over an authenticated and encrypted channel. + +// Example 9: TODO +// If http://non-secure.example.com/ in a top-level browsing context frames https://example.com/, which runs https://example.com/worker.js, then neither the framed document nor the worker are secure contexts. + +// Example 12: TODO +// https://example.com/ nested in http://non-secure.example.com/ may not connect to the secure worker, as it is not a secure context. + +// Example 13: TODO +// Likewise, if https://example.com/ nested in http://non-secure.example.com/ runs https://example.com/worker.js as a Shared Worker, then both the document and the worker are considered non-secure. + +</script> diff --git a/testing/web-platform/tests/webauthn/securecontext.https.html b/testing/web-platform/tests/webauthn/securecontext.https.html new file mode 100644 index 0000000000..f927004702 --- /dev/null +++ b/testing/web-platform/tests/webauthn/securecontext.https.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>WebAuthn Secure Context Tests</title> +<link rel="author" title="Adam Powers" href="mailto:adam@fidoalliance.org"> +<link rel="help" href="https://w3c.github.io/webauthn/#iface-credential"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src=helpers.js></script> +<body></body> +<script> +"use strict"; + +// See https://www.w3.org/TR/secure-contexts/ +// Section 1.1 - 1.4 for list of examples referenced below + +// Example 2 +// https://example.com/ opened in a top-level browsing context is a secure context, as it was delivered over an authenticated and encrypted channel. +test (() => { + assert_true (typeof navigator.credentials === "object" && typeof navigator.credentials.create === "function"); +}, "navigator.credentials.create exists in secure context"); + +// Example 3: TODO +// Example 5: TODO +// Example 8: TODO +// Example 10: TODO +// Example 11: TODO + +</script> diff --git a/testing/web-platform/tests/webauthn/webauthn-testdriver-basic.https.html b/testing/web-platform/tests/webauthn/webauthn-testdriver-basic.https.html new file mode 100644 index 0000000000..5751928301 --- /dev/null +++ b/testing/web-platform/tests/webauthn/webauthn-testdriver-basic.https.html @@ -0,0 +1,123 @@ +<!DOCTYPE html> +<title>Successful WebAuthn tests</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="resources/common-inputs.js"></script> +<script src="resources/utils.js"></script> + +<script> +"use strict"; + +let authenticator; + +promise_test(async t => { + authenticator = await window.test_driver.add_virtual_authenticator({ + protocol: "ctap1/u2f", + transport: "usb", + }); +}, "Set up the test environment"); + +let credential; +let publicKey; + +promise_test(async t => { + credential = await navigator.credentials.create({ + publicKey: MAKE_CREDENTIAL_OPTIONS, + }); + + // Perform the validations the Relying Party should do against the credential. + // https://w3c.github.io/webauthn/#sctn-registering-a-new-credential + let jsonText = + new TextDecoder("utf-8").decode(credential.response.clientDataJSON); + let clientData = JSON.parse(jsonText); + assert_equals(clientData.type, "webauthn.create"); + assert_equals(clientData.challenge, base64urlEncode(CHALLENGE)); + assert_equals(clientData.origin, window.location.origin); + + let attestationObject = + new Cbor(credential.response.attestationObject).getCBOR(); + + let rpIdHash = new Uint8Array(await crypto.subtle.digest( + { name: "SHA-256" }, new TextEncoder().encode(PUBLIC_KEY_RP.id))); + + let authenticatorData = parseAuthenticatorData(attestationObject.authData); + + assert_array_equals(authenticatorData.rpIdHash, rpIdHash) + assert_true(authenticatorData.flags.up); + assert_false(authenticatorData.flags.uv); + + publicKey = authenticatorData.attestedCredentialData.credentialPublicKey; + assert_equals(publicKey.alg, PUBLIC_KEY_PARAMETERS[0].alg); + assert_equals(publicKey.type, 2 /* EC2 */); + + assert_equals(authenticatorData.extensions, null); + assert_object_equals(credential.getClientExtensionResults(), {}); +}, "Create a credential"); + +promise_test(async t => { + let assertion = await navigator.credentials.get({ + publicKey: { + challenge: new TextEncoder().encode(CHALLENGE), + rpId: PUBLIC_KEY_RP.id, + allowCredentials: [{ + type: "public-key", + id: credential.rawId, + transports: ["usb"], + }], + userVerification: "discouraged", + }, + }); + + // Perform the validations the Relying Party should do against the assertion. + // https://w3c.github.io/webauthn/#sctn-verifying-assertion + assert_object_equals(credential.rawId, assertion.rawId); + let jsonText = + new TextDecoder("utf-8").decode(assertion.response.clientDataJSON); + let clientData = JSON.parse(jsonText); + assert_equals(clientData.type, "webauthn.get"); + assert_equals(clientData.challenge, base64urlEncode(CHALLENGE)); + assert_equals(clientData.type, "webauthn.get"); + assert_equals(clientData.origin, window.location.origin); + + let binaryAuthenticatorData = + new Uint8Array(assertion.response.authenticatorData); + + let authenticatorData = parseAuthenticatorData(binaryAuthenticatorData); + + let rpIdHash = new Uint8Array(await crypto.subtle.digest( + { name: "SHA-256" }, new TextEncoder().encode(PUBLIC_KEY_RP.id))); + + assert_array_equals(authenticatorData.rpIdHash, rpIdHash) + assert_true(authenticatorData.flags.up); + assert_false(authenticatorData.flags.uv); + + assert_equals(authenticatorData.extensions, null); + assert_object_equals(credential.getClientExtensionResults(), {}); + assert_equals(authenticatorData.attestedCredentialData, null); + + let jwkPublicKey = coseObjectToJWK(publicKey); + let key = await crypto.subtle.importKey( + "jwk", jwkPublicKey, {name: "ECDSA", namedCurve: "P-256"}, + /*extractable=*/false, ["verify"]); + + let signature = + convertDERSignatureToSubtle(new Uint8Array(assertion.response.signature)); + + let clientDataJsonHash = new Uint8Array(await crypto.subtle.digest( + "SHA-256", assertion.response.clientDataJSON)); + let signedData = + new Uint8Array(binaryAuthenticatorData.length + clientDataJsonHash.length); + signedData.set(binaryAuthenticatorData); + signedData.set(clientDataJsonHash, binaryAuthenticatorData.length); + + assert_true(await crypto.subtle.verify( + { name: "ECDSA", hash: "SHA-256" }, key, signature, signedData)); +}, "Get an assertion"); + +promise_test(async t => { + await window.test_driver.remove_virtual_authenticator(authenticator); +}, "Clean up the test environment"); + +</script> |