diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/secure-payment-confirmation | |
parent | Initial commit. (diff) | |
download | firefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/secure-payment-confirmation')
18 files changed, 1649 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-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 Binary files differnew file mode 100644 index 0000000000..4987ba65c2 --- /dev/null +++ b/testing/web-platform/tests/secure-payment-confirmation/troy.png 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, "_"); +} + |