summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/webauthn
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/webauthn')
-rw-r--r--testing/web-platform/tests/webauthn/META.yml5
-rw-r--r--testing/web-platform/tests/webauthn/conditional-mediation.https.html37
-rw-r--r--testing/web-platform/tests/webauthn/createcredential-abort.https.html109
-rw-r--r--testing/web-platform/tests/webauthn/createcredential-attachment.https.html79
-rw-r--r--testing/web-platform/tests/webauthn/createcredential-badargs-authnrselection.https.html60
-rw-r--r--testing/web-platform/tests/webauthn/createcredential-badargs-challenge.https.html27
-rw-r--r--testing/web-platform/tests/webauthn/createcredential-badargs-rp.https.html42
-rw-r--r--testing/web-platform/tests/webauthn/createcredential-badargs-user.https.html45
-rw-r--r--testing/web-platform/tests/webauthn/createcredential-excludecredentials.https.html82
-rw-r--r--testing/web-platform/tests/webauthn/createcredential-extensions.https.html51
-rw-r--r--testing/web-platform/tests/webauthn/createcredential-getpublickey.https.html52
-rw-r--r--testing/web-platform/tests/webauthn/createcredential-large-blob-not-supported.https.html78
-rw-r--r--testing/web-platform/tests/webauthn/createcredential-large-blob-supported.https.html82
-rw-r--r--testing/web-platform/tests/webauthn/createcredential-minpinlength.https.html36
-rw-r--r--testing/web-platform/tests/webauthn/createcredential-passing.https.html145
-rw-r--r--testing/web-platform/tests/webauthn/createcredential-prf.https.html44
-rw-r--r--testing/web-platform/tests/webauthn/createcredential-pubkeycredparams.https.html73
-rw-r--r--testing/web-platform/tests/webauthn/createcredential-resident-key.https.html178
-rw-r--r--testing/web-platform/tests/webauthn/createcredential-timeout.https.html53
-rw-r--r--testing/web-platform/tests/webauthn/credblob-not-supported.https.html36
-rw-r--r--testing/web-platform/tests/webauthn/credblob-supported.https.html93
-rw-r--r--testing/web-platform/tests/webauthn/getcredential-abort.https.html66
-rw-r--r--testing/web-platform/tests/webauthn/getcredential-attachment.https.html71
-rw-r--r--testing/web-platform/tests/webauthn/getcredential-badargs-rpid.https.html46
-rw-r--r--testing/web-platform/tests/webauthn/getcredential-badargs-userverification.https.html28
-rw-r--r--testing/web-platform/tests/webauthn/getcredential-extensions.https.html80
-rw-r--r--testing/web-platform/tests/webauthn/getcredential-large-blob-not-supported.https.html91
-rw-r--r--testing/web-platform/tests/webauthn/getcredential-large-blob-supported.https.html90
-rw-r--r--testing/web-platform/tests/webauthn/getcredential-passing.https.html74
-rw-r--r--testing/web-platform/tests/webauthn/getcredential-prf.https.html165
-rw-r--r--testing/web-platform/tests/webauthn/getcredential-rk-passing.https.html48
-rw-r--r--testing/web-platform/tests/webauthn/getcredential-timeout.https.html72
-rw-r--r--testing/web-platform/tests/webauthn/helpers.js628
-rw-r--r--testing/web-platform/tests/webauthn/idlharness-manual.https.window.js53
-rw-r--r--testing/web-platform/tests/webauthn/idlharness.https.window.js21
-rw-r--r--testing/web-platform/tests/webauthn/public-key-credential-to-json.https.window.js157
-rw-r--r--testing/web-platform/tests/webauthn/remote-desktop-client-override.tentative.https.html53
-rw-r--r--testing/web-platform/tests/webauthn/resources/common-inputs.js33
-rw-r--r--testing/web-platform/tests/webauthn/resources/utils.js340
-rw-r--r--testing/web-platform/tests/webauthn/securecontext.http.html46
-rw-r--r--testing/web-platform/tests/webauthn/securecontext.https.html28
-rw-r--r--testing/web-platform/tests/webauthn/webauthn-testdriver-basic.https.html123
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>