Commit 4c8f6737 authored by Danyao Wang's avatar Danyao Wang Committed by Commit Bot

[Web Payments] Create WebAuthn challenge for Secure Payment Confirmation.

Teach the browser to generate a WebAuthn challenge that is a SHA-256
hash over transaction data.

Also added a test page that automates expectation updating for the unit
test.

Bug: 1121020
Change-Id: Ic5515135eb8724eedc49d966f3986b0a48bbd519
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2381096
Commit-Queue: Danyao Wang <danyao@chromium.org>
Auto-Submit: Danyao Wang <danyao@chromium.org>
Reviewed-by: default avatarAdam Langley <agl@chromium.org>
Reviewed-by: default avatarRouslan Solomakhin <rouslan@chromium.org>
Cr-Commit-Position: refs/heads/master@{#802741}
parent 599c821b
......@@ -50,6 +50,7 @@ static_library("content") {
deps = [
":content_common",
":utils",
"//base",
"//components/autofill/core/browser",
"//components/keyed_service/content",
"//components/payments/content/utility",
......@@ -63,6 +64,7 @@ static_library("content") {
"//components/url_formatter",
"//components/webdata/common",
"//content/public/browser",
"//crypto",
"//device/fido",
"//services/data_decoder/public/cpp",
"//third_party/blink/public:blink_headers",
......@@ -183,6 +185,7 @@ source_set("unit_tests") {
"payment_request_spec_unittest.cc",
"payment_request_state_unittest.cc",
"payment_response_helper_unittest.cc",
"secure_payment_confirmation_app_unittest.cc",
"secure_payment_confirmation_model_unittest.cc",
"service_worker_payment_app_unittest.cc",
]
......
......@@ -11,6 +11,7 @@ include_rules = [
"+components/url_formatter",
"+components/webdata/common",
"+content/public",
"+crypto",
"+device/fido",
"+mojo/public/cpp",
"+net",
......
......@@ -21,6 +21,7 @@
#include "components/payments/core/method_strings.h"
#include "components/payments/core/payer_data.h"
#include "content/public/common/content_features.h"
#include "crypto/sha2.h"
#include "device/fido/fido_transport_protocol.h"
#include "device/fido/fido_types.h"
#include "device/fido/public_key_credential_descriptor.h"
......@@ -31,6 +32,50 @@ namespace {
static constexpr int kDefaultTimeoutMinutes = 3;
// Creates a SHA-256 hash over the Secure Payment Confirmation bundle, which is
// a JSON string (without whitespace) with the following structure:
// {
// "merchantData" {
// "merchantOrigin": "https://merchant.example",
// "total": {
// "currency": "CAD",
// "value": "1.25",
// },
// },
// "networkData": "YW=",
// }
// where "networkData" is the base64 encoding of the |networkData| specified in
// the SecurePaymentConfirmationRequest.
std::vector<uint8_t> GetSecurePaymentConfirmationChallenge(
const std::vector<uint8_t>& network_data,
const url::Origin& merchant_origin,
const mojom::PaymentCurrencyAmountPtr& amount) {
base::Value total(base::Value::Type::DICTIONARY);
total.SetKey("currency", base::Value(amount->currency));
total.SetKey("value", base::Value(amount->value));
base::Value merchant_data(base::Value::Type::DICTIONARY);
merchant_data.SetKey("merchantOrigin",
base::Value(merchant_origin.Serialize()));
merchant_data.SetKey("total", std::move(total));
base::Value transaction_data(base::Value::Type::DICTIONARY);
transaction_data.SetKey("networkData",
base::Value(base::Base64Encode(network_data)));
transaction_data.SetKey("merchantData", std::move(merchant_data));
// TODO(crbug.com/1123054): change to a more robust alternative that does not
// depend on the exact whitespace, escaping and ordering of the JSON
// serialization.
std::string json;
bool success = base::JSONWriter::Write(transaction_data, &json);
DCHECK(success) << "Failed to write JSON for " << transaction_data;
std::string sha256_hash = crypto::SHA256HashString(json);
std::vector<uint8_t> output_bytes(sha256_hash.begin(), sha256_hash.end());
return output_bytes;
}
} // namespace
SecurePaymentConfirmationApp::SecurePaymentConfirmationApp(
......@@ -84,9 +129,9 @@ void SecurePaymentConfirmationApp::InvokePaymentApp(Delegate* delegate) {
options->allow_credentials = std::move(credentials);
// TODO(https://crbug.com/1110324): Combine |merchant_origin_|, |total_|, and
// |request_->network_data| into a challenge to invoke the authenticator.
options->challenge = request_->network_data;
// Create a new challenge that is a hash of the transaction data.
options->challenge = GetSecurePaymentConfirmationChallenge(
request_->network_data, merchant_origin_, total_);
// We are nullifying the security check by design, and the origin that created
// the credential isn't saved anywhere.
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/payments/content/secure_payment_confirmation_app.h"
#include "base/base64.h"
#include "base/strings/string_piece.h"
#include "base/strings/utf_string_conversions.h"
#include "components/autofill/core/browser/payments/internal_authenticator.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/webauthn/authenticator.mojom.h"
#include "url/origin.h"
namespace payments {
namespace {
using ::testing::_;
using ::testing::Eq;
static constexpr char kNetworkDataBase64[] = "aaaa";
static constexpr char kCredentialIdBase64[] = "cccc";
class MockAuthenticator : public autofill::InternalAuthenticator {
public:
MOCK_METHOD1(SetEffectiveOrigin, void(const url::Origin&));
MOCK_METHOD2(
MakeCredential,
void(blink::mojom::PublicKeyCredentialCreationOptionsPtr options,
blink::mojom::Authenticator::MakeCredentialCallback callback));
MOCK_METHOD1(IsUserVerifyingPlatformAuthenticatorAvailable,
void(blink::mojom::Authenticator::
IsUserVerifyingPlatformAuthenticatorAvailableCallback));
MOCK_METHOD0(Cancel, void());
MOCK_METHOD1(VerifyChallenge, void(const std::vector<uint8_t>&));
// Implements an autofill::InternalAuthenticator method to delegate fields of
// |options| to gmock methods for easier verification.
void GetAssertion(
blink::mojom::PublicKeyCredentialRequestOptionsPtr options,
blink::mojom::Authenticator::GetAssertionCallback callback) override {
VerifyChallenge(options->challenge);
}
};
class SecurePaymentConfirmationAppTest : public testing::Test {
protected:
SecurePaymentConfirmationAppTest()
: label_(base::ASCIIToUTF16("test instrument")),
total_(mojom::PaymentCurrencyAmount::New()) {
total_->currency = "USD";
total_->value = "1.25";
}
void SetUp() override {
ASSERT_TRUE(base::Base64Decode(kNetworkDataBase64, &network_data_bytes_));
ASSERT_TRUE(base::Base64Decode(kCredentialIdBase64, &credential_id_bytes_));
}
mojom::SecurePaymentConfirmationRequestPtr MakeRequest() {
auto request = mojom::SecurePaymentConfirmationRequest::New();
request->network_data = std::vector<uint8_t>(network_data_bytes_.begin(),
network_data_bytes_.end());
return request;
}
base::string16 label_;
mojom::PaymentCurrencyAmountPtr total_;
std::string network_data_bytes_;
std::string credential_id_bytes_;
};
TEST_F(SecurePaymentConfirmationAppTest, Smoke) {
std::vector<uint8_t> credential_id(credential_id_bytes_.begin(),
credential_id_bytes_.end());
auto authenticator = std::make_unique<MockAuthenticator>();
MockAuthenticator* mock_authenticator = authenticator.get();
SecurePaymentConfirmationApp app(
"effective_rp.example",
/*icon=*/std::make_unique<SkBitmap>(), label_, std::move(credential_id),
url::Origin::Create(GURL("https://merchant.example")), total_,
MakeRequest(), std::move(authenticator));
EXPECT_CALL(*mock_authenticator, SetEffectiveOrigin(Eq(url::Origin::Create(
GURL("https://effective_rp.example")))));
// This is the SHA-256 hash of the serialized JSON string:
// {"merchantData":{"merchantOrigin":"https://merchant.example","total":
// {"currency":"USD","value":"1.25"}},"networkData":"aaaa"}
//
// To update the test expectation, open
// //components/test/data/payments/secure_payment_confirmation_debut.html in a
// browser and follow the instructions.
std::vector<uint8_t> expected_bytes = {
240, 123, 37, 51, 16, 34, 244, 220, 166, 179, 139,
85, 229, 152, 242, 133, 88, 44, 222, 133, 49, 97,
146, 20, 207, 119, 43, 142, 171, 239, 125, 250};
EXPECT_CALL(*mock_authenticator, VerifyChallenge(Eq(expected_bytes)));
app.InvokePaymentApp(/*delegate=*/nullptr);
}
} // namespace
} // namespace payments
<!DOCTYPE html>
<!--
Copyright 2020 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1">
<title>Secure Payment Confirmation Debug Tool</title>
<style>
#output {
font-family: monospace;
}
</style>
<script>
/**
* Computes the SHA-256 hash of the provided payload to print on screen.
*/
async function getHash() {
let raw_input = document.querySelector('#json').value;
let json_str = sanitizeInput(raw_input);
print('#json_str', json_str);
try {
let bytes = Uint8Array.from(json_str, c => c.charCodeAt(0));
print('#json_bytes', bytes.toString());
// Hash over binary form of b64 encoding.
let hash = await crypto.subtle.digest('SHA-256', bytes.buffer);
// hash is an ArrayBuffer, convert it to Uint8Array.
let array = new Uint8Array(hash);
print('#hash_bytes', array.toString());
} catch (e) {
console.log(e.message);
}
let json = JSON.parse(json_str);
console.log(json);
}
/**
* Creates a compat serialization of the provided raw JSON representation.
*/
function sanitizeInput(raw) {
let json = JSON.parse(raw);
let sanitized = JSON.stringify(json, null, 0);
return sanitized;
}
/**
* Helper function to print |value| to the DOMElement with |selector|.
*/
function print(selector, value) {
document.querySelector(selector).innerText = value;
}
</script>
</head>
<body>
<h1>Secure Payment Confirmation Unit Test Helper</h1>
<p>
This page is a helper utility to generate new test expectations for
//components/payments/content/secure_payment_confirmation_app_unittest.cc.
</p>
<p>Steps:</p>
<ol>
<li>Copy new payload into the text box.</li>
<li>Click on "Get SHA-256"</li>
<li>Copy the generated SHA-256 bytes into |expected_bytes| in secure_payment_confirmation_app_unittest.cc.</li>
</ol>
<hr>
<p>Payload:</p>
<textarea id="json" rows="20" cols="60">{
"merchantData": {
"merchantOrigin": "https://merchant.example",
"total": {
"currency": "USD",
"value":"1.25"
}
},
"networkData":"aaaa"
}</textarea>
<p><button id="getHash" onclick="getHash()">Get SHA-256</button></p>
<hr>
<div id="output">
<p>Input string: </p>
<div id="json_str"></div>
<p>Input bytes: </p>
<div id="json_bytes"></div>
<p>SHA-256 bytes: </p>
<div id="hash_bytes"></div>
</div>
</body>
</html>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment