summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/secure-payment-confirmation
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /testing/web-platform/tests/secure-payment-confirmation
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/secure-payment-confirmation')
-rw-r--r--testing/web-platform/tests/secure-payment-confirmation/META.yml4
-rw-r--r--testing/web-platform/tests/secure-payment-confirmation/authentication-accepted.https.html79
-rw-r--r--testing/web-platform/tests/secure-payment-confirmation/authentication-cannot-bypass-spc.https.html56
-rw-r--r--testing/web-platform/tests/secure-payment-confirmation/authentication-cross-origin.sub.https.html71
-rw-r--r--testing/web-platform/tests/secure-payment-confirmation/authentication-disallowed-when-hidden.https.html57
-rw-r--r--testing/web-platform/tests/secure-payment-confirmation/authentication-icon-data-url.https.html58
-rw-r--r--testing/web-platform/tests/secure-payment-confirmation/authentication-in-iframe.sub.https.html146
-rw-r--r--testing/web-platform/tests/secure-payment-confirmation/authentication-invalid-icon.https.html109
-rw-r--r--testing/web-platform/tests/secure-payment-confirmation/authentication-optout.https.html50
-rw-r--r--testing/web-platform/tests/secure-payment-confirmation/authentication-rejected.https.html49
-rw-r--r--testing/web-platform/tests/secure-payment-confirmation/authentication-requires-user-activation.https.html48
-rw-r--r--testing/web-platform/tests/secure-payment-confirmation/constructor-validate-payment-method-data.https.html358
-rw-r--r--testing/web-platform/tests/secure-payment-confirmation/constructor.https.html201
-rw-r--r--testing/web-platform/tests/secure-payment-confirmation/enrollment-in-iframe.sub.https.html166
-rw-r--r--testing/web-platform/tests/secure-payment-confirmation/enrollment.https.html64
-rw-r--r--testing/web-platform/tests/secure-payment-confirmation/resources/iframe-authenticate.html59
-rw-r--r--testing/web-platform/tests/secure-payment-confirmation/resources/iframe-enroll.html28
-rw-r--r--testing/web-platform/tests/secure-payment-confirmation/troy.pngbin0 -> 884 bytes
-rw-r--r--testing/web-platform/tests/secure-payment-confirmation/utils.sub.js103
19 files changed, 1706 insertions, 0 deletions
diff --git a/testing/web-platform/tests/secure-payment-confirmation/META.yml b/testing/web-platform/tests/secure-payment-confirmation/META.yml
new file mode 100644
index 0000000000..cdcab0e705
--- /dev/null
+++ b/testing/web-platform/tests/secure-payment-confirmation/META.yml
@@ -0,0 +1,4 @@
+spec: https://w3c.github.io/secure-payment-confirmation/
+suggested_reviewers:
+ - stephenmcgruer
+ - rsolomakhin
diff --git a/testing/web-platform/tests/secure-payment-confirmation/authentication-accepted.https.html b/testing/web-platform/tests/secure-payment-confirmation/authentication-accepted.https.html
new file mode 100644
index 0000000000..b9417b88e0
--- /dev/null
+++ b/testing/web-platform/tests/secure-payment-confirmation/authentication-accepted.https.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test for the 'secure-payment-confirmation' payment method authentication - accepted case</title>
+<link rel="help" href="https://w3c.github.io/secure-payment-confirmation#sctn-authentication">
+<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="utils.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const authenticator = await window.test_driver.add_virtual_authenticator(
+ AUTHENTICATOR_OPTS);
+ t.add_cleanup(() => {
+ return window.test_driver.remove_virtual_authenticator(authenticator);
+ });
+
+ await window.test_driver.set_spc_transaction_mode("autoAccept");
+ t.add_cleanup(() => {
+ return window.test_driver.set_spc_transaction_mode("none");
+ });
+
+ const credential = await createCredential();
+
+ const challenge = 'server challenge';
+ const payeeOrigin = 'https://merchant.com';
+ const displayName = 'Troycard ***1234';
+ const request = new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [credential.rawId],
+ challenge: Uint8Array.from(challenge, c => c.charCodeAt(0)),
+ payeeOrigin,
+ rpId: window.location.hostname,
+ timeout: 60000,
+ instrument: {
+ displayName,
+ icon: ICON_URL,
+ },
+ }
+ }], PAYMENT_DETAILS);
+
+ await test_driver.bless('user activation');
+ const responsePromise = request.show();
+
+ const response = await responsePromise;
+ await response.complete('success');
+
+ const cred = response.details;
+ assert_equals(cred.id, credential.id);
+
+ const clientDataJSON = JSON.parse(arrayBufferToString(cred.response.clientDataJSON));
+ assert_equals(clientDataJSON.type, 'payment.get');
+ assert_equals(clientDataJSON.challenge, base64UrlEncode(challenge));
+ assert_equals(clientDataJSON.origin, window.location.origin);
+ assert_false(clientDataJSON.crossOrigin);
+
+ // Payment-specific information.
+ assert_equals(clientDataJSON.payment.rpId, window.location.hostname);
+ assert_equals(clientDataJSON.payment.topOrigin, window.location.origin);
+ assert_equals(clientDataJSON.payment.payeeOrigin, payeeOrigin);
+ assert_equals(clientDataJSON.payment.total.value, PAYMENT_DETAILS.total.amount.value);
+ assert_equals(clientDataJSON.payment.total.currency, PAYMENT_DETAILS.total.amount.currency);
+ assert_equals(clientDataJSON.payment.instrument.icon, ICON_URL);
+ assert_equals(clientDataJSON.payment.instrument.displayName, displayName);
+
+ // If the User Agent still supports the legacy 'rp' output parameter, it
+ // should be identical to the 'rpId' output parameter. See
+ // https://github.com/w3c/secure-payment-confirmation/pull/198
+ if ('rp' in clientDataJSON.payment) {
+ assert_equals(clientDataJSON.payment.rp, clientDataJSON.payment.rpId);
+ }
+
+ // TODO: Verify cred.response.signature, to validate that it covers all fields
+ // from clientDataJSON.
+}, 'Successful SPC authentication');
+</script>
diff --git a/testing/web-platform/tests/secure-payment-confirmation/authentication-cannot-bypass-spc.https.html b/testing/web-platform/tests/secure-payment-confirmation/authentication-cannot-bypass-spc.https.html
new file mode 100644
index 0000000000..2125f644c7
--- /dev/null
+++ b/testing/web-platform/tests/secure-payment-confirmation/authentication-cannot-bypass-spc.https.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test for the 'secure-payment-confirmation' payment method authentication - cannot authenticate via navigator.credentials.get directly</title>
+<link rel="help" href="https://w3c.github.io/secure-payment-confirmation#client-extension-processing-authentication">
+<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="utils.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const authenticator = await window.test_driver.add_virtual_authenticator(
+ AUTHENTICATOR_OPTS);
+ t.add_cleanup(() => {
+ return window.test_driver.remove_virtual_authenticator(authenticator);
+ });
+
+ await window.test_driver.set_spc_transaction_mode("autoAccept");
+ t.add_cleanup(() => {
+ return window.test_driver.set_spc_transaction_mode("none");
+ });
+
+ const credential = await createCredential();
+
+ const challenge = 'server challenge';
+ const payeeOrigin = 'https://merchant.com';
+ const displayName = 'Troycard ***1234';
+ const paymentInputs = {
+ isPayment: true,
+ rp: window.location.hostname,
+ topOrigin: window.location.origin,
+ payeeOrigin,
+ total: PAYMENT_DETAILS['total'],
+ instrument: {
+ displayName,
+ icon: ICON_URL,
+ },
+ };
+ const publicKey = {
+ allowCredentials: [
+ { type: 'public-key', id: credential.rawId, transports: [ 'internal' ] },
+ ],
+ challenge: Uint8Array.from(challenge, c => c.charCodeAt(0)),
+ timeout: 60000,
+ userVerification: 'required',
+ extensions: {
+ payment: paymentInputs,
+ },
+ };
+
+ return promise_rejects_dom(t, 'NotAllowedError',
+ navigator.credentials.get({publicKey}));
+}, 'Cannot bypass SPC authentication UI via navigator.credentials.get');
+</script>
diff --git a/testing/web-platform/tests/secure-payment-confirmation/authentication-cross-origin.sub.https.html b/testing/web-platform/tests/secure-payment-confirmation/authentication-cross-origin.sub.https.html
new file mode 100644
index 0000000000..bf03d4c11c
--- /dev/null
+++ b/testing/web-platform/tests/secure-payment-confirmation/authentication-cross-origin.sub.https.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test for the 'secure-payment-confirmation' payment method authentication - cross origin</title>
+<link rel="help" href="https://w3c.github.io/secure-payment-confirmation#client-extension-processing-authentication">
+<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="utils.sub.js"></script>
+
+<!-- This test requires a non-empty body to workaround https://github.com/web-platform-tests/wpt/issues/34563 -->
+<body><div>Non-empty body</div></body>
+
+<script>
+'use strict';
+
+promise_test(async t => {
+ // Make sure that we are testing a cross-origin authentication ceremony.
+ assert_not_equals(window.location.hostname, '{{hosts[alt][]}}',
+ 'This test must not be run on the alt hostname.');
+
+ const authenticator = await window.test_driver.add_virtual_authenticator(
+ AUTHENTICATOR_OPTS);
+ t.add_cleanup(() => {
+ return window.test_driver.remove_virtual_authenticator(authenticator);
+ });
+
+ await window.test_driver.set_spc_transaction_mode("autoAccept");
+ t.add_cleanup(() => {
+ return window.test_driver.set_spc_transaction_mode("none");
+ });
+
+ // Create a credential for the WPT alt domain.
+ const credential = await createCredentialForAltDomain();
+ assert_equals(credential.error, null);
+
+ const challenge = 'server challenge';
+ const payeeOrigin = 'https://merchant.com';
+ const displayName = 'Troycard ***1234';
+ const request = new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [credential.rawId],
+ challenge: Uint8Array.from(challenge, c => c.charCodeAt(0)),
+ payeeOrigin,
+ rpId: '{{hosts[alt][]}}',
+ timeout: 60000,
+ instrument: {
+ displayName,
+ icon: ICON_URL,
+ },
+ }
+ }], PAYMENT_DETAILS);
+
+ await test_driver.bless('user activation');
+ const responsePromise = request.show();
+
+ const response = await responsePromise;
+ await response.complete('success');
+
+ const cred = response.details;
+ assert_equals(cred.id, credential.id);
+
+ const clientDataJSON = JSON.parse(arrayBufferToString(cred.response.clientDataJSON));
+
+ // The origin should be ourselves, whilst the RP should be the alt hostname
+ // (as the owner of the credential).
+ assert_equals(clientDataJSON.origin, window.location.origin);
+ assert_equals(clientDataJSON.payment.rpId, '{{hosts[alt][]}}');
+}, 'Cross-origin SPC authentication ceremony');
+</script>
diff --git a/testing/web-platform/tests/secure-payment-confirmation/authentication-disallowed-when-hidden.https.html b/testing/web-platform/tests/secure-payment-confirmation/authentication-disallowed-when-hidden.https.html
new file mode 100644
index 0000000000..1c01fa0e89
--- /dev/null
+++ b/testing/web-platform/tests/secure-payment-confirmation/authentication-disallowed-when-hidden.https.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test for the 'secure-payment-confirmation' payment method authentication - accepted case</title>
+<link rel="help" href="https://w3c.github.io/secure-payment-confirmation#sctn-authentication">
+<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>
+<!-- For minimize() -->
+<script src="/page-visibility/resources/window_state_context.js"></script>
+<script src="utils.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const {minimize, restore} = window_state_context(t);
+
+ const authenticator = await window.test_driver.add_virtual_authenticator(
+ AUTHENTICATOR_OPTS);
+ t.add_cleanup(() => {
+ return window.test_driver.remove_virtual_authenticator(authenticator);
+ });
+
+ await window.test_driver.set_spc_transaction_mode("autoAccept");
+ t.add_cleanup(() => {
+ return window.test_driver.set_spc_transaction_mode("none");
+ });
+
+ const credential = await createCredential();
+
+ const challenge = 'server challenge';
+ const payeeOrigin = 'https://merchant.com';
+ const displayName = 'Troycard ***1234';
+ const request = new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [credential.rawId],
+ challenge: Uint8Array.from(challenge, c => c.charCodeAt(0)),
+ payeeOrigin,
+ rpId: window.location.hostname,
+ timeout: 60000,
+ instrument: {
+ displayName,
+ icon: ICON_URL,
+ },
+ }
+ }], PAYMENT_DETAILS);
+
+ // Before we trigger the Payment Request, minimize the window. This should
+ // cause the show() call to be rejected.
+ await minimize();
+ assert_equals(document.hidden, true);
+
+ await test_driver.bless('user activation');
+ return promise_rejects_dom(t, "AbortError", request.show());
+}, 'SPC authentication cannot be triggered from a hidden context');
+</script>
diff --git a/testing/web-platform/tests/secure-payment-confirmation/authentication-icon-data-url.https.html b/testing/web-platform/tests/secure-payment-confirmation/authentication-icon-data-url.https.html
new file mode 100644
index 0000000000..cd820d84c3
--- /dev/null
+++ b/testing/web-platform/tests/secure-payment-confirmation/authentication-icon-data-url.https.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test for the 'secure-payment-confirmation' payment method authentication - data URL icon</title>
+<link rel="help" href="https://w3c.github.io/secure-payment-confirmation#sctn-steps-to-check-if-a-payment-can-be-made">
+<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="utils.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const authenticator = await window.test_driver.add_virtual_authenticator(
+ AUTHENTICATOR_OPTS);
+ t.add_cleanup(() => {
+ return window.test_driver.remove_virtual_authenticator(authenticator);
+ });
+
+ await window.test_driver.set_spc_transaction_mode("autoAccept");
+ t.add_cleanup(() => {
+ return window.test_driver.set_spc_transaction_mode("none");
+ });
+
+ const credential = await createCredential();
+
+ const challenge = 'server challenge';
+ const payeeOrigin = 'https://merchant.com';
+ const displayName = 'Troycard ***1234';
+ // Use a Data URL for the request, which should be accepted.
+ const request = new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [credential.rawId],
+ challenge: Uint8Array.from(challenge, c => c.charCodeAt(0)),
+ payeeOrigin,
+ rpId: window.location.hostname,
+ timeout: 60000,
+ instrument: {
+ displayName,
+ icon: ICON_DATA_URL,
+ },
+ }
+ }], PAYMENT_DETAILS);
+
+ await test_driver.bless('user activation');
+ const responsePromise = request.show();
+
+ const response = await responsePromise;
+ await response.complete('success');
+
+ const cred = response.details;
+ assert_equals(cred.id, credential.id);
+
+ const clientDataJSON = JSON.parse(arrayBufferToString(cred.response.clientDataJSON));
+ assert_equals(clientDataJSON.payment.instrument.icon, ICON_DATA_URL);
+}, 'SPC authentication with data URL instrument icon');
+</script>
diff --git a/testing/web-platform/tests/secure-payment-confirmation/authentication-in-iframe.sub.https.html b/testing/web-platform/tests/secure-payment-confirmation/authentication-in-iframe.sub.https.html
new file mode 100644
index 0000000000..4402e2825d
--- /dev/null
+++ b/testing/web-platform/tests/secure-payment-confirmation/authentication-in-iframe.sub.https.html
@@ -0,0 +1,146 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test for the 'secure-payment-confirmation' payment method authentication - cross origin</title>
+<link rel="help" href="https://w3c.github.io/secure-payment-confirmation#sctn-authentication">
+<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="utils.sub.js"></script>
+
+<!-- This test requires a non-empty body to workaround https://github.com/web-platform-tests/wpt/issues/34563 -->
+<body><div>Non-empty body</div></body>
+
+<script>
+'use strict';
+
+promise_test(async t => {
+ // Make sure that we are testing calling SPC in a cross-origin iframe.
+ assert_not_equals(window.location.hostname, '{{hosts[alt][]}}',
+ 'This test must not be run on the alt hostname.');
+
+ const authenticator = await window.test_driver.add_virtual_authenticator(
+ AUTHENTICATOR_OPTS);
+ t.add_cleanup(() => {
+ return window.test_driver.remove_virtual_authenticator(authenticator);
+ });
+
+ await window.test_driver.set_spc_transaction_mode('autoAccept');
+ t.add_cleanup(() => {
+ return window.test_driver.set_spc_transaction_mode('none');
+ });
+
+ const credential = await createCredential();
+
+ const frame = document.createElement('iframe');
+ frame.allow = 'payment';
+ frame.src = 'https://{{hosts[alt][]}}:{{ports[https][0]}}' +
+ '/secure-payment-confirmation/resources/iframe-authenticate.html';
+
+ // Wait for the iframe to load.
+ const readyPromise = new Promise(resolve => {
+ window.addEventListener('message', function handler(evt) {
+ if (evt.source === frame.contentWindow && evt.data.type == 'loaded') {
+ window.removeEventListener('message', handler);
+
+ resolve(evt.data);
+ }
+ });
+ });
+ document.body.appendChild(frame);
+ await readyPromise;
+
+ // Setup the result promise before triggering authentication, to avoid a
+ // race.
+ const resultPromise = new Promise(resolve => {
+ window.addEventListener('message', function handler(evt) {
+ if (evt.source === frame.contentWindow && evt.data.type == 'spc_result') {
+ // We're done with the child iframe now.
+ document.body.removeChild(frame);
+ window.removeEventListener('message', handler);
+
+ resolve(evt.data);
+ }
+ });
+ });
+
+ const rpId = window.location.hostname;
+ frame.contentWindow.postMessage([credential.rawId, rpId], '*');
+
+ const result = await resultPromise;
+
+ assert_not_own_property(result, 'error');
+
+ assert_equals(result.id, credential.id);
+ assert_equals(result.clientDataJSON.origin, 'https://{{hosts[alt][]}}:{{ports[https][0]}}');
+ assert_equals(result.clientDataJSON.payment.topOrigin, window.location.origin);
+ // The credential was created in this frame, and so we are the rp.
+ assert_equals(result.clientDataJSON.payment.rpId, window.location.hostname);
+ // The payeeOrigin should be unrelated to what the origin and topOrigin are.
+ assert_equals(result.clientDataJSON.payment.payeeOrigin, 'https://merchant.com');
+}, 'SPC authentication ceremony in cross-origin iframe');
+
+promise_test(async t => {
+ // Make sure that we are testing calling SPC in a cross-origin iframe.
+ assert_not_equals(window.location.hostname, '{{hosts[alt][]}}',
+ 'This test must not be run on the alt hostname.');
+
+ const authenticator = await window.test_driver.add_virtual_authenticator(
+ AUTHENTICATOR_OPTS);
+ t.add_cleanup(() => {
+ return window.test_driver.remove_virtual_authenticator(authenticator);
+ });
+
+ await window.test_driver.set_spc_transaction_mode('autoAccept');
+ t.add_cleanup(() => {
+ return window.test_driver.set_spc_transaction_mode('none');
+ });
+
+ const credential = await createCredential();
+
+ const frame = document.createElement('iframe');
+ // This iframe does *not* have a payments permission specified on it, and so
+ // should not allow SPC authentication.
+ frame.src = 'https://{{hosts[alt][]}}:{{ports[https][0]}}' +
+ '/secure-payment-confirmation/resources/iframe-authenticate.html';
+
+ // Wait for the iframe to load.
+ const readyPromise = new Promise(resolve => {
+ window.addEventListener('message', function handler(evt) {
+ if (evt.source === frame.contentWindow && evt.data.type == 'loaded') {
+ window.removeEventListener('message', handler);
+
+ resolve(evt.data);
+ }
+ });
+ });
+ document.body.appendChild(frame);
+ await readyPromise;
+
+ // Setup the result promise before triggering authentication, to avoid a
+ // race.
+ const resultPromise = new Promise(resolve => {
+ window.addEventListener('message', function handler(evt) {
+ if (evt.source === frame.contentWindow && evt.data.type == 'spc_result') {
+ // We're done with the child iframe now.
+ document.body.removeChild(frame);
+ window.removeEventListener('message', handler);
+
+ resolve(evt.data);
+ }
+ });
+ });
+
+ const rpId = window.location.hostname;
+ frame.contentWindow.postMessage([credential.rawId, rpId], '*');
+
+ const result = await resultPromise;
+
+ assert_own_property(result, 'error');
+ assert_true(result.error instanceof DOMException);
+ assert_equals(result.error.name, 'SecurityError');
+
+ assert_not_own_property(result, 'id');
+ assert_not_own_property(result, 'clientDataJSON');
+}, 'SPC authentication ceremony in cross-origin iframe without payment permission');
+</script>
diff --git a/testing/web-platform/tests/secure-payment-confirmation/authentication-invalid-icon.https.html b/testing/web-platform/tests/secure-payment-confirmation/authentication-invalid-icon.https.html
new file mode 100644
index 0000000000..988d867462
--- /dev/null
+++ b/testing/web-platform/tests/secure-payment-confirmation/authentication-invalid-icon.https.html
@@ -0,0 +1,109 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test for the 'secure-payment-confirmation' payment method authentication - invalid icon</title>
+<link rel="help" href="https://w3c.github.io/secure-payment-confirmation#sctn-steps-to-check-if-a-payment-can-be-made">
+<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="utils.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const authenticator = await window.test_driver.add_virtual_authenticator(
+ AUTHENTICATOR_OPTS);
+ t.add_cleanup(() => {
+ return window.test_driver.remove_virtual_authenticator(authenticator);
+ });
+
+ // We deliberately do *NOT* set an SPC transaction mode override, as SPC
+ // should reject without any UX being shown in the case of an invalid image.
+ await window.test_driver.set_spc_transaction_mode("none");
+
+ const credential = await createCredential();
+
+ const challenge = 'server challenge';
+ const payeeOrigin = 'https://merchant.com';
+ const displayName = 'Troycard ***1234';
+
+ // First try an icon that cannot be downloaded.
+ let request = new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [credential.rawId],
+ challenge: Uint8Array.from(challenge, c => c.charCodeAt(0)),
+ payeeOrigin,
+ rpId: window.location.hostname,
+ timeout: 60000,
+ instrument: {
+ displayName,
+ icon: NONEXISTENT_ICON_URL,
+ },
+ }
+ }], PAYMENT_DETAILS);
+ await test_driver.bless('user activation');
+ await promise_rejects_dom(t, "NotSupportedError", request.show());
+
+ // Now try an icon that cannot be decoded.
+ request = new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [credential.rawId],
+ challenge: Uint8Array.from(challenge, c => c.charCodeAt(0)),
+ payeeOrigin,
+ rpId: window.location.hostname,
+ timeout: 60000,
+ instrument: {
+ displayName,
+ icon: INVALID_ICON_DATA_URL,
+ },
+ }
+ }], PAYMENT_DETAILS);
+ await test_driver.bless('user activation');
+ await promise_rejects_dom(t, "NotSupportedError", request.show());
+}, 'SPC authentication with an invalid icon');
+
+promise_test(async t => {
+ const authenticator = await window.test_driver.add_virtual_authenticator(
+ AUTHENTICATOR_OPTS);
+ t.add_cleanup(() => {
+ return window.test_driver.remove_virtual_authenticator(authenticator);
+ });
+
+ await window.test_driver.set_spc_transaction_mode("autoAccept");
+ t.add_cleanup(() => {
+ return window.test_driver.set_spc_transaction_mode("none");
+ });
+
+ const credential = await createCredential();
+
+ const challenge = 'server challenge';
+ const payeeOrigin = 'https://merchant.com';
+ const displayName = 'Troycard ***1234';
+
+ let request = new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [credential.rawId],
+ challenge: Uint8Array.from(challenge, c => c.charCodeAt(0)),
+ payeeOrigin,
+ rpId: window.location.hostname,
+ timeout: 60000,
+ instrument: {
+ displayName,
+ icon: NONEXISTENT_ICON_URL,
+ iconMustBeShown: false,
+ },
+ }
+ }], PAYMENT_DETAILS);
+
+ await test_driver.bless('user activation');
+ const responsePromise = request.show();
+ const response = await responsePromise;
+ await response.complete('success');
+ const cred = response.details;
+ const clientDataJSON = JSON.parse(arrayBufferToString(cred.response.clientDataJSON));
+ assert_equals(clientDataJSON.payment.instrument.icon, '');
+}, 'SPC authentication allowing an invalid icon with iconMustBeShown option.');
+</script>
diff --git a/testing/web-platform/tests/secure-payment-confirmation/authentication-optout.https.html b/testing/web-platform/tests/secure-payment-confirmation/authentication-optout.https.html
new file mode 100644
index 0000000000..a69e0a456a
--- /dev/null
+++ b/testing/web-platform/tests/secure-payment-confirmation/authentication-optout.https.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test for the 'secure-payment-confirmation' payment method authentication - user opt out case</title>
+<link rel="help" href="https://w3c.github.io/secure-payment-confirmation/#sctn-user-opt-out">
+<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="utils.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const authenticator = await window.test_driver.add_virtual_authenticator(
+ AUTHENTICATOR_OPTS);
+ t.add_cleanup(() => {
+ return window.test_driver.remove_virtual_authenticator(authenticator);
+ });
+
+ await window.test_driver.set_spc_transaction_mode("autoOptOut");
+ t.add_cleanup(() => {
+ return window.test_driver.set_spc_transaction_mode("none");
+ });
+
+
+ const credential = await createCredential();
+
+ const challenge = 'server challenge';
+ const payeeOrigin = 'https://merchant.com';
+ const displayName = 'Troycard ***1234';
+ const request = new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [credential.rawId],
+ challenge: Uint8Array.from(challenge, c => c.charCodeAt(0)),
+ rpId: window.location.hostname,
+ payeeOrigin,
+ timeout: 60000,
+ instrument: {
+ displayName,
+ icon: ICON_URL,
+ },
+ showOptOut: true,
+ }
+ }], PAYMENT_DETAILS);
+
+ await test_driver.bless('user activation');
+ return promise_rejects_dom(t, "OptOutError", request.show());
+}, 'SPC opt-out returns OptOutError');
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/secure-payment-confirmation/authentication-rejected.https.html b/testing/web-platform/tests/secure-payment-confirmation/authentication-rejected.https.html
new file mode 100644
index 0000000000..7c84abc9e0
--- /dev/null
+++ b/testing/web-platform/tests/secure-payment-confirmation/authentication-rejected.https.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test for the 'secure-payment-confirmation' payment method authentication - user rejects case</title>
+<link rel="help" href="https://w3c.github.io/secure-payment-confirmation/sctn-authentication">
+<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="utils.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const authenticator = await window.test_driver.add_virtual_authenticator(
+ AUTHENTICATOR_OPTS);
+ t.add_cleanup(() => {
+ return window.test_driver.remove_virtual_authenticator(authenticator);
+ });
+
+ await window.test_driver.set_spc_transaction_mode("autoReject");
+ t.add_cleanup(() => {
+ return window.test_driver.set_spc_transaction_mode("none");
+ });
+
+
+ const credential = await createCredential();
+
+ const challenge = 'server challenge';
+ const payeeOrigin = 'https://merchant.com';
+ const displayName = 'Troycard ***1234';
+ const request = new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [credential.rawId],
+ challenge: Uint8Array.from(challenge, c => c.charCodeAt(0)),
+ rpId: window.location.hostname,
+ payeeOrigin,
+ timeout: 60000,
+ instrument: {
+ displayName,
+ icon: ICON_URL,
+ },
+ }
+ }], PAYMENT_DETAILS);
+
+ await test_driver.bless('user activation');
+ return promise_rejects_dom(t, "NotAllowedError", request.show());
+}, 'Rejected SPC authentication');
+</script>
diff --git a/testing/web-platform/tests/secure-payment-confirmation/authentication-requires-user-activation.https.html b/testing/web-platform/tests/secure-payment-confirmation/authentication-requires-user-activation.https.html
new file mode 100644
index 0000000000..dd6a737696
--- /dev/null
+++ b/testing/web-platform/tests/secure-payment-confirmation/authentication-requires-user-activation.https.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test for the 'secure-payment-confirmation' payment method authentication - requires user activation</title>
+<link rel="help" href="https://w3c.github.io/secure-payment-confirmation/sctn-authentication">
+<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="utils.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const authenticator = await window.test_driver.add_virtual_authenticator(
+ AUTHENTICATOR_OPTS);
+ t.add_cleanup(() => {
+ return window.test_driver.remove_virtual_authenticator(authenticator);
+ });
+
+ await window.test_driver.set_spc_transaction_mode("autoAccept");
+ t.add_cleanup(() => {
+ return window.test_driver.set_spc_transaction_mode("none");
+ });
+
+
+ const credential = await createCredential();
+
+ const challenge = 'server challenge';
+ const payeeOrigin = 'https://merchant.com';
+ const displayName = 'Troycard ***1234';
+ const request = new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [credential.rawId],
+ challenge: Uint8Array.from(challenge, c => c.charCodeAt(0)),
+ rpId: window.location.hostname,
+ payeeOrigin,
+ timeout: 60000,
+ instrument: {
+ displayName,
+ icon: ICON_URL,
+ },
+ }
+ }], PAYMENT_DETAILS);
+
+ return promise_rejects_dom(t, "SecurityError", request.show());
+}, 'SPC authentication not allowed without a user activation');
+</script>
diff --git a/testing/web-platform/tests/secure-payment-confirmation/constructor-validate-payment-method-data.https.html b/testing/web-platform/tests/secure-payment-confirmation/constructor-validate-payment-method-data.https.html
new file mode 100644
index 0000000000..1ece6318b5
--- /dev/null
+++ b/testing/web-platform/tests/secure-payment-confirmation/constructor-validate-payment-method-data.https.html
@@ -0,0 +1,358 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="timeout" content="long">
+<title>Tests for the 'secure-payment-confirmation' steps to validate payment method data</title>
+<link rel="help" href="https://w3c.github.io/secure-payment-confirmation/#sctn-steps-to-validate-payment-method-data">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+'use strict';
+
+const details = {total:
+ {label: 'Total', amount: {value: '0.01', currency: 'USD'}}};
+
+// This file contains tests for the 'steps to validate payment method data',
+// which occurs during construction of the PaymentRequest. For general tests
+// around construction, see constructor.https.html.
+
+test(() => {
+ assert_throws_js(RangeError, () => {
+ new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ relyingPartyId: 'relying-party.example',
+ // Empty credentialIds field.
+ credentialIds: [],
+ challenge: Uint8Array.from('x', c => c.charCodeAt(0)),
+ payeeOrigin: window.location.origin,
+ timeout: 60000,
+ instrument: {
+ displayName: 'X',
+ icon: 'https://example.test/icon.png',
+ },
+ rpId: 'relying-party.example',
+ },
+ }], details);
+ });
+}, 'Empty credentialIds field throws exception.');
+
+test(() => {
+ assert_throws_js(RangeError, () => {
+ new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ relyingPartyId: 'relying-party.example',
+ credentialIds: [
+ Uint8Array.from('c1', c => c.charCodeAt(0)),
+ new Uint8Array(), // Empty
+ Uint8Array.from('c2', c => c.charCodeAt(0)),
+ ],
+ challenge: Uint8Array.from('x', c => c.charCodeAt(0)),
+ payeeOrigin: window.location.origin,
+ timeout: 60000,
+ instrument: {
+ displayName: 'X',
+ icon: 'https://example.test/icon.png',
+ },
+ rpId: 'relying-party.example',
+ },
+ }], details);
+ });
+}, 'Empty ID within credentialIds field throws exception.');
+
+test(() => {
+ new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [
+ Uint8Array.from('c1', c => c.charCodeAt(0)),
+ Uint8Array.from('c2', c => c.charCodeAt(0))
+ ],
+ challenge: Uint8Array.from('x', c => c.charCodeAt(0)),
+ // Omitted payee origin, instead with payee name.
+ payeeName: 'Example Merchant',
+ timeout: 60000,
+ instrument: {
+ displayName: 'X',
+ icon: 'https://example.test/icon.png',
+ },
+ rpId: 'relying-party.example',
+ },
+ }], details);
+}, 'Multiple IDs in credentialIds is valid.');
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ // Large credentialIds value.
+ credentialIds: [Uint8Array.from(
+ 'x'.repeat(1024 * 1024), c => c.charCodeAt(0))],
+ challenge: Uint8Array.from('x', c => c.charCodeAt(0)),
+ payeeOrigin: window.location.origin,
+ timeout: 60000,
+ instrument: {
+ displayName: 'X',
+ icon: 'https://example.test/icon.png',
+ },
+ rpId: 'relying-party.example',
+ },
+ }], details);
+ });
+}, 'Large credentialIds value throws exception.');
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [Uint8Array.from('x', c => c.charCodeAt(0))],
+ // Null challenge fields.
+ challenge: null,
+ payeeOrigin: window.location.origin,
+ timeout: 60000,
+ instrument: {
+ displayName: 'X',
+ icon: 'https://example.test/icon.png',
+ },
+ rpId: 'relying-party.example',
+ },
+ }], details);
+ });
+}, 'Null challenge field throws exception.');
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [Uint8Array.from('x', c => c.charCodeAt(0))],
+ // Empty challenge fields.
+ challenge: [],
+ payeeOrigin: window.location.origin,
+ timeout: 60000,
+ instrument: {
+ displayName: 'X',
+ icon: 'https://example.test/icon.png',
+ },
+ rpId: 'relying-party.example',
+ },
+ }], details);
+ });
+}, 'Empty challenge field throws exception.');
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [Uint8Array.from('x', c => c.charCodeAt(0))],
+ // Large challenge value.
+ challenge: Uint8Array.from('x'.repeat(1024 * 1024), c => c.charCodeAt(0)),
+ payeeOrigin: window.location.origin,
+ timeout: 60000,
+ instrument: {
+ displayName: 'X',
+ icon: 'https://example.test/icon.png',
+ },
+ rpId: 'relying-party.example',
+ },
+ }], details);
+ });
+}, 'Large challenge value throws exception.');
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [Uint8Array.from('x', c => c.charCodeAt(0))],
+ challenge: Uint8Array.from('x', c => c.charCodeAt(0)),
+ payeeOrigin: window.location.origin,
+ timeout: 60000,
+ instrument: {
+ displayName: '',
+ icon: 'https://example.test/icon.png',
+ },
+ rpId: 'relying-party.example',
+ },
+ }], details);
+ });
+}, 'Empty instrument.displayName field throws exception.');
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [Uint8Array.from('x', c => c.charCodeAt(0))],
+ challenge: Uint8Array.from('x', c => c.charCodeAt(0)),
+ payeeOrigin: window.location.origin,
+ timeout: 60000,
+ instrument: {
+ displayName: 'X',
+ icon: '',
+ },
+ rpId: 'relying-party.example',
+ },
+ }], details);
+ });
+}, 'Empty instrument.icon field throws exception.');
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [Uint8Array.from('x', c => c.charCodeAt(0))],
+ challenge: Uint8Array.from('x', c => c.charCodeAt(0)),
+ payeeOrigin: window.location.origin,
+ timeout: 60000,
+ instrument: {
+ displayName: 'X',
+ icon: 'thisisnotaurl',
+ },
+ rpId: 'relying-party.example',
+ },
+ }], details);
+ });
+}, 'Invalid instrument.icon URL throws exception.');
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [Uint8Array.from('x', c => c.charCodeAt(0))],
+ challenge: Uint8Array.from('x', c => c.charCodeAt(0)),
+ payeeOrigin: window.location.origin,
+ timeout: 60000,
+ instrument: {
+ displayName: 'X',
+ icon: 'https://example.test/icon.png',
+ },
+ rpId: 'domains cannot have spaces.com',
+ },
+ }], details);
+ });
+}, 'Invalid rpId field throws exception.');
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ // Omitted payee origin and payee name.
+ credentialIds: [Uint8Array.from('x', c => c.charCodeAt(0))],
+ challenge: Uint8Array.from('x', c => c.charCodeAt(0)),
+ timeout: 60000,
+ instrument: {
+ displayName: 'X',
+ icon: 'https://example.test/icon.png',
+ },
+ rpId: 'relying-party.example',
+ },
+ }], details);
+ });
+}, 'Omitting both payee origin and payee name throws exception.');
+
+test(() => {
+ new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [Uint8Array.from('x', c => c.charCodeAt(0))],
+ challenge: Uint8Array.from('x', c => c.charCodeAt(0)),
+ // Omitted payee origin, instead with payee name.
+ payeeName: 'Example Merchant',
+ timeout: 60000,
+ instrument: {
+ displayName: 'X',
+ icon: 'https://example.test/icon.png',
+ },
+ rpId: 'relying-party.example',
+ },
+ }], details);
+}, 'Payee name without payee origin is valid.');
+
+test(() => {
+ new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [Uint8Array.from('x', c => c.charCodeAt(0))],
+ challenge: Uint8Array.from('x', c => c.charCodeAt(0)),
+ // Both payee origin and payee name.
+ payeeName: 'Example Merchant',
+ payeeOrigin: window.location.origin,
+ timeout: 60000,
+ instrument: {
+ displayName: 'X',
+ icon: 'https://example.test/icon.png',
+ },
+ rpId: 'relying-party.example',
+ },
+ }], details);
+}, 'Providing both payee name and payee origin is valid.');
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [Uint8Array.from('x', c => c.charCodeAt(0))],
+ challenge: Uint8Array.from('x', c => c.charCodeAt(0)),
+ // Empty payee name
+ payeeName: '',
+ payeeOrigin: window.location.origin,
+ timeout: 60000,
+ instrument: {
+ displayName: 'X',
+ icon: 'https://example.test/icon.png',
+ },
+ rpId: 'relying-party.example',
+ },
+ }], details);
+ });
+}, 'Empty payee name throws exception.');
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [Uint8Array.from('x', c => c.charCodeAt(0))],
+ challenge: Uint8Array.from('x', c => c.charCodeAt(0)),
+ payeeName: 'Example Merchant',
+ // Empty payee origin
+ payeeOrigin: '',
+ timeout: 60000,
+ instrument: {
+ displayName: 'X',
+ icon: 'https://example.test/icon.png',
+ },
+ rpId: 'relying-party.example',
+ },
+ }], details);
+ });
+}, 'Empty payee origin throws exception.');
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [Uint8Array.from('x', c => c.charCodeAt(0))],
+ challenge: Uint8Array.from('x', c => c.charCodeAt(0)),
+ payeeName: 'Example Merchant',
+ payeeOrigin: 'http://thepayee.com',
+ timeout: 60000,
+ instrument: {
+ displayName: 'X',
+ icon: 'https://example.test/icon.png',
+ },
+ rpId: 'relying-party.example',
+ },
+ }], details);
+ });
+}, 'Non-HTTPS payee origin throws exception.');
+</script>
diff --git a/testing/web-platform/tests/secure-payment-confirmation/constructor.https.html b/testing/web-platform/tests/secure-payment-confirmation/constructor.https.html
new file mode 100644
index 0000000000..e42f8d47c7
--- /dev/null
+++ b/testing/web-platform/tests/secure-payment-confirmation/constructor.https.html
@@ -0,0 +1,201 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="timeout" content="long">
+<title>Test for the 'secure-payment-confirmation' payment method constructor</title>
+<link rel="help" href="https://w3c.github.io/secure-payment-confirmation/#sctn-payment-method-spc">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+'use strict';
+
+const details = {total:
+ {label: 'Total', amount: {value: '0.01', currency: 'USD'}}};
+
+// This file contains general tests for constructing a Secure Payment
+// Confirmation payment request, that are not parts of the 'steps to validate
+// payment method data'. For those, see constructor-validate-payment-method-data.https.html
+
+test(() => {
+ new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ // All valid parameters.
+ credentialIds: [Uint8Array.from('x', c => c.charCodeAt(0))],
+ challenge: Uint8Array.from('x', c => c.charCodeAt(0)),
+ payeeOrigin: window.location.origin,
+ timeout: 60000,
+ instrument: {
+ displayName: 'X',
+ icon: 'https://example.test/icon.png',
+ },
+ rpId: 'relying-party.example',
+ },
+ }], details);
+}, 'Valid payment method data does not throw exceptions.');
+
+test(() => {
+ new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [Uint8Array.from('x', c => c.charCodeAt(0))],
+ challenge: Uint8Array.from('x', c => c.charCodeAt(0)),
+ payeeOrigin: window.location.origin,
+ // Omitted timeout field.
+ instrument: {
+ displayName: 'X',
+ icon: 'https://example.test/icon.png',
+ },
+ rpId: 'relying-party.example',
+ },
+ }], details);
+}, 'The timeout field is optional.');
+
+test(() => {
+ assert_throws_js(RangeError, () => {
+ new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [Uint8Array.from('x', c => c.charCodeAt(0))],
+ challenge: Uint8Array.from('x', c => c.charCodeAt(0)),
+ payeeOrigin: window.location.origin,
+ timeout: 60000,
+ instrument: {
+ displayName: 'X',
+ icon: 'https://example.test/icon.png',
+ },
+ rpId: 'relying-party.example',
+ },
+ }, {supportedMethods: 'https://example.com/pay'}], details);
+ });
+}, 'Extra payment method not allowed afterward.');
+
+test(() => {
+ assert_throws_js(RangeError, () => {
+ new PaymentRequest([{supportedMethods: 'https://example.com/pay'}, {
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [Uint8Array.from('x', c => c.charCodeAt(0))],
+ challenge: Uint8Array.from('x', c => c.charCodeAt(0)),
+ payeeOrigin: window.location.origin,
+ timeout: 60000,
+ instrument: {
+ displayName: 'X',
+ icon: 'https://example.test/icon.png',
+ },
+ rpId: 'relying-party.example',
+ },
+ }], details);
+ });
+}, 'Extra payment method not allowed beforehand.');
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ // Omitted credentialIds field.
+ challenge: Uint8Array.from('x', c => c.charCodeAt(0)),
+ payeeOrigin: window.location.origin,
+ timeout: 60000,
+ instrument: {
+ displayName: 'X',
+ icon: 'https://example.test/icon.png',
+ },
+ rpId: 'relying-party.example',
+ },
+ }], details);
+ });
+}, 'The credentialIds field is required.');
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [Uint8Array.from('x', c => c.charCodeAt(0))],
+ // Omitted challenge field.
+ payeeOrigin: window.location.origin,
+ timeout: 60000,
+ instrument: {
+ displayName: 'X',
+ icon: 'https://example.test/icon.png',
+ },
+ rpId: 'relying-party.example',
+ },
+ }], details);
+ });
+}, 'The challenge field is required.');
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [Uint8Array.from('x', c => c.charCodeAt(0))],
+ challenge: Uint8Array.from('x', c => c.charCodeAt(0)),
+ payeeOrigin: window.location.origin,
+ timeout: 60000,
+ rpId: 'relying-party.example',
+ // Omitted instrument field.
+ },
+ }], details);
+ });
+}, 'Instrument field is required.');
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [Uint8Array.from('x', c => c.charCodeAt(0))],
+ challenge: Uint8Array.from('x', c => c.charCodeAt(0)),
+ payeeOrigin: window.location.origin,
+ timeout: 60000,
+ instrument: {
+ // Ommitted instrument display name.
+ icon: 'https://example.test/icon.png',
+ },
+ rpId: 'relying-party.example',
+ },
+ }], details);
+ });
+}, 'Instrument display name is required.');
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [Uint8Array.from('x', c => c.charCodeAt(0))],
+ challenge: Uint8Array.from('x', c => c.charCodeAt(0)),
+ payeeOrigin: window.location.origin,
+ timeout: 60000,
+ instrument: {
+ displayName: 'X',
+ // Ommitted instrument icon.
+ },
+ rpId: 'relying-party.example',
+ },
+ }], details);
+ });
+}, 'Instrument icon is required.');
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [Uint8Array.from('x', c => c.charCodeAt(0))],
+ challenge: Uint8Array.from('x', c => c.charCodeAt(0)),
+ payeeOrigin: window.location.origin,
+ timeout: 60000,
+ instrument: {
+ displayName: 'X',
+ icon: 'https://example.test/icon.png',
+ },
+ // Omitted rpId.
+ },
+ }], details);
+ });
+}, 'rpId is required.');
+</script>
diff --git a/testing/web-platform/tests/secure-payment-confirmation/enrollment-in-iframe.sub.https.html b/testing/web-platform/tests/secure-payment-confirmation/enrollment-in-iframe.sub.https.html
new file mode 100644
index 0000000000..9a0f2093a1
--- /dev/null
+++ b/testing/web-platform/tests/secure-payment-confirmation/enrollment-in-iframe.sub.https.html
@@ -0,0 +1,166 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test for the 'secure-payment-confirmation' payment method enrollment - cross origin</title>
+<link rel="help" href="https://w3c.github.io/secure-payment-confirmation#client-extension-processing-registration">
+<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="utils.sub.js"></script>
+
+<!-- This test requires a non-empty body to workaround https://github.com/web-platform-tests/wpt/issues/34563 -->
+<body><div>Non-empty body</div></body>
+
+<script>
+'use strict';
+
+promise_test(async t => {
+ // Make sure that we are testing enrolling an SPC credential in a
+ // cross-origin iframe.
+ assert_not_equals(window.location.hostname, '{{hosts[alt][]}}',
+ 'This test must not be run on the alt hostname.');
+
+ const authenticator = await window.test_driver.add_virtual_authenticator(
+ AUTHENTICATOR_OPTS);
+ t.add_cleanup(() => {
+ return window.test_driver.remove_virtual_authenticator(authenticator);
+ });
+
+ const frame = document.createElement('iframe');
+ frame.allow = 'payment';
+ frame.src = 'https://{{hosts[alt][]}}:{{ports[https][0]}}' +
+ '/secure-payment-confirmation/resources/iframe-enroll.html';
+
+ // Wait for the iframe to load.
+ const readyPromise = new Promise(resolve => {
+ window.addEventListener('message', function handler(evt) {
+ if (evt.source === frame.contentWindow && evt.data.type == 'loaded') {
+ window.removeEventListener('message', handler);
+
+ resolve(evt.data);
+ }
+ });
+ });
+ document.body.appendChild(frame);
+ await readyPromise;
+
+ const resultPromise = new Promise(resolve => {
+ window.addEventListener('message', function handler(evt) {
+ if (evt.source === frame.contentWindow && evt.data.type == 'spc_result') {
+ window.removeEventListener('message', handler);
+ document.body.removeChild(frame);
+ resolve(evt.data);
+ }
+ });
+ });
+ frame.contentWindow.postMessage({ userActivation: true }, '*');
+ const result = await resultPromise;
+
+ // Because we specified the 'payment' permission and the iframe had a user
+ // activation, the enrollment should work.
+ assert_equals(result.error, null);
+ assert_own_property(result, 'id');
+ assert_own_property(result, 'rawId');
+}, 'SPC enrollment in cross-origin iframe');
+
+promise_test(async t => {
+ // Make sure that we are testing enrolling an SPC credential in a
+ // cross-origin iframe.
+ assert_not_equals(window.location.hostname, '{{hosts[alt][]}}',
+ 'This test must not be run on the alt hostname.');
+
+ const authenticator = await window.test_driver.add_virtual_authenticator(
+ AUTHENTICATOR_OPTS);
+ t.add_cleanup(() => {
+ return window.test_driver.remove_virtual_authenticator(authenticator);
+ });
+
+ const frame = document.createElement('iframe');
+ frame.allow = 'payment';
+ frame.src = 'https://{{hosts[alt][]}}:{{ports[https][0]}}' +
+ '/secure-payment-confirmation/resources/iframe-enroll.html';
+
+ // Wait for the iframe to load.
+ const readyPromise = new Promise(resolve => {
+ window.addEventListener('message', function handler(evt) {
+ if (evt.source === frame.contentWindow && evt.data.type == 'loaded') {
+ window.removeEventListener('message', handler);
+
+ resolve(evt.data);
+ }
+ });
+ });
+ document.body.appendChild(frame);
+ await readyPromise;
+
+ const resultPromise = new Promise(resolve => {
+ window.addEventListener('message', function handler(evt) {
+ if (evt.source === frame.contentWindow && evt.data.type == 'spc_result') {
+ window.removeEventListener('message', handler);
+ document.body.removeChild(frame);
+ resolve(evt.data);
+ }
+ });
+ });
+ frame.contentWindow.postMessage({ userActivation: false }, '*');
+ const result = await resultPromise;
+
+ // Without a user activation, we expect a SecurityError.
+ assert_true(result.error instanceof DOMException);
+ assert_equals(result.error.name, 'SecurityError');
+ assert_not_own_property(result, 'id');
+ assert_not_own_property(result, 'rawId');
+}, 'SPC enrollment in cross-origin iframe fails without user activation');
+
+promise_test(async t => {
+ // Make sure that we are testing enrolling an SPC credential in a
+ // cross-origin iframe.
+ assert_not_equals(window.location.hostname, '{{hosts[alt][]}}',
+ 'This test must not be run on the alt hostname.');
+
+ const authenticator = await window.test_driver.add_virtual_authenticator(
+ AUTHENTICATOR_OPTS);
+ t.add_cleanup(() => {
+ return window.test_driver.remove_virtual_authenticator(authenticator);
+ });
+
+ const frame = document.createElement('iframe');
+ // This iframe does *not* have a payments permission specified on it, and so
+ // should not allow SPC credential creation.
+ frame.src = 'https://{{hosts[alt][]}}:{{ports[https][0]}}' +
+ '/secure-payment-confirmation/resources/iframe-enroll.html';
+
+ // Wait for the iframe to load.
+ const readyPromise = new Promise(resolve => {
+ window.addEventListener('message', function handler(evt) {
+ if (evt.source === frame.contentWindow && evt.data.type == 'loaded') {
+ window.removeEventListener('message', handler);
+
+ resolve(evt.data);
+ }
+ });
+ });
+ document.body.appendChild(frame);
+ await readyPromise;
+
+ const resultPromise = new Promise(resolve => {
+ window.addEventListener('message', function handler(evt) {
+ if (evt.source === frame.contentWindow && evt.data.type == 'spc_result') {
+ window.removeEventListener('message', handler);
+ document.body.removeChild(frame);
+ resolve(evt.data);
+ }
+ });
+ });
+ frame.contentWindow.postMessage({ userActivation: true }, '*');
+ const result = await resultPromise;
+
+ // Because we didn't specify the 'payment' permission, the enrollment should
+ // result in an error.
+ assert_own_property(result, 'error');
+ assert_true(result.error instanceof DOMException);
+ assert_equals(result.error.name, 'NotSupportedError');
+ assert_not_own_property(result, 'id');
+ assert_not_own_property(result, 'rawId');
+}, 'SPC enrollment in cross-origin iframe without payment permission');
+</script>
diff --git a/testing/web-platform/tests/secure-payment-confirmation/enrollment.https.html b/testing/web-platform/tests/secure-payment-confirmation/enrollment.https.html
new file mode 100644
index 0000000000..b93822c7f1
--- /dev/null
+++ b/testing/web-platform/tests/secure-payment-confirmation/enrollment.https.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="timeout" content="long">
+<title>Test for registering a PublicKeyCredential with "payment" extension</title>
+<link rel="help" href="https://w3c.github.io/secure-payment-confirmation/#client-extension-processing-registration">
+<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=../webauthn/helpers.js></script>
+<body></body>
+<script>
+class CreatePaymentCredentialsTest extends CreateCredentialsTest {
+ constructor(authenticatorSelection) {
+ super('options.publicKey.extensions', {
+ payment: { isPayment: true },
+ });
+ this.testObject.options.publicKey.authenticatorSelection = {
+ userVerification: 'required',
+ residentKey: 'required',
+ authenticatorAttachment: 'platform',
+ };
+ authenticatorSelection ||= {};
+ // Override specific fields.
+ extendObject(this.testObject.options.publicKey.authenticatorSelection, authenticatorSelection);
+ }
+}
+
+standardSetup(function() {
+ // good creates
+ new CreatePaymentCredentialsTest().runTest('Payment credential is created successfully.');
+ new CreatePaymentCredentialsTest({residentKey: 'preferred'})
+ .runTest('Payment credential allows residentKey to be "preferred".');
+
+ // bad creates
+ new CreatePaymentCredentialsTest({userVerification: 'preferred'})
+ .runTest('Payment credential requires userVerification to be "required", not "preferred".', "NotSupportedError");
+ new CreatePaymentCredentialsTest({userVerification: 'discouraged'})
+ .runTest('Payment credential requires userVerification to be "required", not "discouraged".', "NotSupportedError");
+ new CreatePaymentCredentialsTest({residentKey: 'discouraged'})
+ .runTest('Payment credential does not allow residentKey to be "discouraged".', "NotSupportedError");
+ new CreatePaymentCredentialsTest({authenticatorAttachment: 'cross-platform'})
+ .runTest('Payment credential requires authenticatorAttachment to be "platform", not "cross-platform".', "NotSupportedError");
+
+ // abort creates
+ let abortController = new AbortController();
+ abortController.abort();
+ new CreatePaymentCredentialsTest()
+ .modify("options.signal", abortController.signal)
+ .runTest("Payment credential abort without reason", "AbortError");
+
+ abortController = new AbortController();
+ abortController.abort(new Error('error'));
+ new CreatePaymentCredentialsTest()
+ .modify("options.signal", abortController.signal)
+ .runTest("Payment credential abort reason with Error", Error);
+}, {
+ protocol: 'ctap2_1',
+ transport: 'internal',
+ hasResidentKey: true,
+ hasUserVerification: true,
+ isUserVerified: true,
+});
+</script>
diff --git a/testing/web-platform/tests/secure-payment-confirmation/resources/iframe-authenticate.html b/testing/web-platform/tests/secure-payment-confirmation/resources/iframe-authenticate.html
new file mode 100644
index 0000000000..828e81f60d
--- /dev/null
+++ b/testing/web-platform/tests/secure-payment-confirmation/resources/iframe-authenticate.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>SPC Authentication iframe</title>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="../utils.sub.js"></script>
+<script>
+'use strict';
+
+// Setup the listener first, to avoid race conditions.
+window.addEventListener('message', async function handler(evt) {
+ window.removeEventListener('message', handler);
+
+ const credentialId = evt.data[0];
+ const rpId = evt.data[1];
+
+ // Assume that our parent has already created a virtual authenticator device
+ // and set the SPC transaction mode.
+ const challenge = 'server challenge';
+ const payeeOrigin = 'https://merchant.com';
+ const displayName = 'Troycard ***1234';
+
+ try {
+ const request = new PaymentRequest([{
+ supportedMethods: 'secure-payment-confirmation',
+ data: {
+ credentialIds: [credentialId],
+ challenge: Uint8Array.from(challenge, c => c.charCodeAt(0)),
+ payeeOrigin,
+ rpId,
+ timeout: 60000,
+ instrument: {
+ displayName,
+ icon: ICON_URL,
+ },
+ }
+ }], PAYMENT_DETAILS);
+
+ test_driver.set_test_context(window.parent);
+ await test_driver.bless('user activation');
+ const responsePromise = request.show();
+
+ const response = await responsePromise;
+ await response.complete('success');
+
+ const cred = response.details;
+
+ // Let our parent know the results. Some WebAuthn fields cannot be cloned, so
+ // we have to do some teardown ourselves.
+ const clientDataJSON = JSON.parse(arrayBufferToString(cred.response.clientDataJSON))
+ window.parent.postMessage({ type: 'spc_result', id: cred.id, clientDataJSON }, '*');
+ } catch (e) {
+ window.parent.postMessage({ type: 'spc_result', error: e }, '*');
+ }
+});
+
+// Now let our parent know that we are ready to receive the credential ID.
+window.parent.postMessage({ type: 'loaded' }, '*');
+</script>
diff --git a/testing/web-platform/tests/secure-payment-confirmation/resources/iframe-enroll.html b/testing/web-platform/tests/secure-payment-confirmation/resources/iframe-enroll.html
new file mode 100644
index 0000000000..f1a4d0daf0
--- /dev/null
+++ b/testing/web-platform/tests/secure-payment-confirmation/resources/iframe-enroll.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>SPC Enrollment iframe</title>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="../utils.sub.js"></script>
+<script>
+'use strict';
+
+// Setup the listener first, to avoid race conditions.
+window.addEventListener('message', async function handler(evt) {
+ window.removeEventListener('message', handler);
+
+ if (evt.data.userActivation) {
+ test_driver.set_test_context(window.parent);
+ await test_driver.bless('user activation');
+ }
+ // Assume that our parent has already created a virtual authenticator device.
+ await createCredential().then(credential => {
+ parent.postMessage({type: 'spc_result', id: credential.id, rawId: credential.rawId, error: null}, '*');
+ }).catch(e => {
+ parent.postMessage({type: 'spc_result', error: e}, '*');
+ });
+});
+
+// Now let our parent know that we are ready to enroll.
+window.parent.postMessage({ type: 'loaded' }, '*');
+</script>
diff --git a/testing/web-platform/tests/secure-payment-confirmation/troy.png b/testing/web-platform/tests/secure-payment-confirmation/troy.png
new file mode 100644
index 0000000000..4987ba65c2
--- /dev/null
+++ b/testing/web-platform/tests/secure-payment-confirmation/troy.png
Binary files differ
diff --git a/testing/web-platform/tests/secure-payment-confirmation/utils.sub.js b/testing/web-platform/tests/secure-payment-confirmation/utils.sub.js
new file mode 100644
index 0000000000..acb016367d
--- /dev/null
+++ b/testing/web-platform/tests/secure-payment-confirmation/utils.sub.js
@@ -0,0 +1,103 @@
+const PAYMENT_DETAILS = {
+ total: {label: 'Total', amount: {value: '0.01', currency: 'USD'}}
+};
+const AUTHENTICATOR_OPTS = {
+ protocol: 'ctap2_1',
+ transport: 'internal',
+ hasResidentKey: true,
+ hasUserVerification: true,
+ isUserVerified: true,
+};
+
+const ICON_URL = 'https://{{hosts[][www]}}:{{ports[https][0]}}/secure-payment-confirmation/troy.png';
+const NONEXISTENT_ICON_URL = 'https://{{hosts[][www]}}:{{ports[https][0]}}/secure-payment-confirmation/nonexistent.png';
+
+const ICON_DATA_URL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAUCAYAAADskT9PAAAAAXNSR0IArs4c6QAAA+lJREFUSEvtlmtQVGUYx3/n7I1dLgtSsIm4ss1EogM4Uanp6IeGqQ81Do0xECyWxKTkWAxDOkmCDYSOmUZjph8wyj7gTDT1oYnJC3ITYrSLkgMul1lCLsouugvs7tnd5pwJx6ZZ40NDfeg5H95z5rzzPv/n8v+/j2C1Wsubmpr26nQ6FQtoHo/Hn5mZWSmYTCbJ6XSqZmdnF9A96PV6jEajXzCbzcGhoaEFdT7nzGw28z+AkBmIioyguLCAXtsAX37zLcFg8B8vU8gSqFQiqSnLOXGkhkH7b+QV7USSJDRaDT6fhCAIqESRWY8HjSigFUU8gQAiAt5AAK1Wg0qlwu/34/X6lP06nVZZJZ+ET5KUYEICyFiVSumOIh6xWLjjcnHmQhtnW9rJ2fQ8Z1vbUKnUrExJpurAEfIS4ticEM9hm52Hw8M4OTRCbl4O6SnJ/HT1GsfrvyBx8UO8sa0Qg17HZ6e/or2z+/4ArNkvUJifTWR4BMNjo3R2/4hjcootuVncdk0jCkE6f+7hZt2nHEg2MyKIvHWlj/oUC7X2US6lpvP6ViujYzfJshbxZvGrvPjcMwwOj7J1Rym3Xa77AzAY9ByqKicjLZWyvdW0dHSxv3I3G9Y8wa+9NuobGhnp7+e75cswCAKbu69w3TXN92tXYdKoyR6+xa6DVQR9EiXl71K9p4xwg5491e9zrqX9bi+FLIFOp+N03VEWm+IpKC6hzzbAqU8+xGJOpKyyhjPNrcRpVfQ/u4HxGQ8Z57sQgMbVaayN1LP+hx62VVWSnLSU1o4uNq5bw8Xuy5S8vQ+P1/v3AIxRETTUHSM22khzRxefNzTy0f59+P0BNuUXMulwKo33cfqjFMQt4uWrNp56IJqC+BguuGbJar9Mbn4Or215iWAgiGPKyc5dFfT0Xv8Tk0JmQKNRU2TN5cnH0hHVao6fPMX2V/IZHZ+g9J0qhRGypRkjKU1K4OsJB9stS7jlclM7PEbzhIP1qx/n8HsVSmbOtV7kYO0xboxPzA+ATJfIiHCijVEIgojD6SR2UQwej1eJXvJLzMmCUaPGHwhiCtPi8PqwrFzBssQE0lNX8PTGdQwM2fng6AkmHVNc67PND4AoiiQtXYLBYFC4OzMzo/Da5Z5WgEAQr1dSKGqMivrj0CBut5uait2YHowlTKfD4bxDxf5DXPqlR9nj8/nmB0DepdFoFLGRHcvCISuhnE75W36XH7knRFFQQMqm1WgpLrQSYTBgH7nB+bYO+gftf3E878tIPlhxfM96bwhz8jwHQP4XE21UgHt9PtzT00iSP6SE/zduQ3kgmZqaUsl1Xki7O5D82yPZ7y210ZoMhOgBAAAAAElFTkSuQmCC';
+const INVALID_ICON_DATA_URL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAUCAYAAADskT9PAAAAAXNSR0IArs4c6QAAA+lJREFUSEvtlmtQVGUYx3/n7I1dLgtSsIm4ss1EogM4Uanp6IeGqQ81Do0xECyWxKTkWAxDOkmCDYSOmUZjph8wyj7gTDT1oYnJC3ITYrSLkgMul1lCLsouugvs7tnd5pwJx6ZZ40NDfeg5H95z5rzzPv/n8v+/j2C1Wsubmpr26nQ6FQtoHo/Hn5mZWSmYTCbJ6XSqZmdnF9A96PV6jEajXzCbzcGhoaEFdT7nzGw28z+AkBmIioyguLCAXtsAX37zLcFg8B8vU8gSqFQiqSnLOXGkhkH7b+QV7USSJDRaDT6fhCAIqESRWY8HjSigFUU8gQAiAt5AAK1Wg0qlwu/34/X6lP06nVZZJZ+ET5KUYEICyFiVSumOIh6xWLjjcnHmQhtnW9rJ2fQ8Z1vbUKnUrExJpurAEfIS4ticEM9hm52Hw8M4OTRCbl4O6SnJ/HT1GsfrvyBx8UO8sa0Qg17HZ6e/or2z+/4ArNkvUJifTWR4BMNjo3R2/4hjcootuVncdk0jCkE6f+7hZt2nHEg2MyKIvHWlj/oUC7X2US6lpvP6ViujYzfJshbxZvGrvPjcThisIsNonsenseG9Y8wa+9NuobGhnp7+e75cswCAKbu69w3TXN92tXYdKoyR6+xa6DVQR9EiXl71K9p4xwg5491e9zrqX9bi+FLIFOp+N03VEWm+IpKC6hzzbAqU8+xGJOpKyyhjPNrcRpVfQ/u4HxGQ8Z57sQgMbVaayN1LP+hx62VVWSnLSU1o4uNq5bw8Xuy5S8vQ+P1/v3AIxRETTUHSM22khzRxefNzTy0f59+P0BNuUXMulwKo33cfqjFMQt4uWrNp56IJqC+BguuGbJar9Mbn4Or215iWAgiGPKyc5dFfT0Xv8Tk0JmQKNRU2TN5cnH0hHVao6fPMX2V/IZHZ+g9J0qhRGypRkjKU1K4OsJB9stS7jlclM7PEbzhIP1qx/n8HsVSmbOtV7kYO0xboxPzA+ATJfIiHCijVEIgojD6SR2UQwej1eJXvJLzMmCUaPGHwhiCtPi8PqwrFzBssQE0lNX8PTGdQwM2fng6AkmHVNc67PND4AoiiQtXYLBYFC4OzMzo/Da5Z5WgEAQr1dSKGqMivrj0CBut5uait2YHowlTKfD4bxDxf5DXPqlR9nj8/nmB0DepdFoFLGRHcvCISuhnE75W36XH7knRFFQQMqm1WgpLrQSYTBgH7nB+bYO+gftf3E878tIPlhxfM96bwhz8jwHQP4XE21UgHt9PtzT00iSP6SE/zduQ3kgmZqaUsl1Xki7O5D82yPZ7y210ZoMhOgBAAAAAElFTkSuQmCC';
+
+// Creates and returns a WebAuthn credential, optionally with the payment
+// extension set.
+//
+// Assumes that a virtual authenticator has already been created.
+async function createCredential(set_payment_extension=true) {
+ const challengeBytes = new Uint8Array(16);
+ window.crypto.getRandomValues(challengeBytes);
+
+ const publicKey = {
+ challenge: challengeBytes,
+ rp: {
+ name: 'Acme',
+ },
+ user: {
+ id: new Uint8Array(16),
+ name: 'jane.doe@example.com',
+ displayName: 'Jane Doe',
+ },
+ pubKeyCredParams: [{
+ type: 'public-key',
+ alg: -7, // 'ES256'
+ }],
+ authenticatorSelection: {
+ userVerification: 'required',
+ residentKey: 'required',
+ authenticatorAttachment: 'platform',
+ },
+ timeout: 30000,
+ };
+
+ if (set_payment_extension) {
+ publicKey.extensions = {
+ payment: { isPayment: true },
+ };
+ }
+
+ return navigator.credentials.create({publicKey});
+}
+
+// Creates a SPC credential in an iframe for the WPT 'alt' domain. Returns a
+// promise that resolves with the created credential id.
+//
+// Assumes that a virtual authenticator has already been created.
+async function createCredentialForAltDomain() {
+ const frame = document.createElement('iframe');
+ frame.allow = 'payment';
+ frame.src = 'https://{{hosts[alt][]}}:{{ports[https][0]}}' +
+ '/secure-payment-confirmation/resources/iframe-enroll.html';
+
+ // Wait for the iframe to load.
+ const readyPromise = new Promise(resolve => {
+ window.addEventListener('message', function handler(evt) {
+ if (evt.source === frame.contentWindow && evt.data.type == 'loaded') {
+ window.removeEventListener('message', handler);
+
+ resolve(evt.data);
+ }
+ });
+ });
+ document.body.appendChild(frame);
+ await readyPromise;
+
+ // Setup the result promise, and then trigger credential creation.
+ const resultPromise = new Promise(resolve => {
+ window.addEventListener('message', function handler(evt) {
+ if (evt.source === frame.contentWindow && evt.data.type == 'spc_result') {
+ document.body.removeChild(frame);
+ window.removeEventListener('message', handler);
+
+ resolve(evt.data);
+ }
+ });
+ });
+ frame.contentWindow.postMessage({ userActivation: true }, '*');
+ return resultPromise;
+}
+
+function arrayBufferToString(buffer) {
+ return String.fromCharCode(...new Uint8Array(buffer));
+}
+
+function base64UrlEncode(data) {
+ let result = btoa(data);
+ return result.replace(/=+$/g, '').replace(/\+/g, "-").replace(/\//g, "_");
+}
+