blob: df3672ab8d77c099206619195068ded0521d3930 [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/webid/digital_credentials/digital_identity_request_impl.h"
#include <memory>
#include <optional>
#include <vector>
#include "base/barrier_callback.h"
#include "base/base64url.h"
#include "base/containers/flat_set.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/json/json_reader.h"
#include "base/location.h"
#include "base/metrics/histogram_functions.h"
#include "base/values.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/webid/delegation/sd_jwt.h"
#include "content/browser/webid/flags.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/digital_identity_interstitial_type.h"
#include "content/public/browser/digital_identity_provider.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_switches.h"
#include "third_party/blink/public/mojom/webid/digital_identity_request.mojom-forward.h"
#include "third_party/re2/src/re2/re2.h"
using base::Value;
using blink::mojom::RequestDigitalIdentityStatus;
using InterstitialType = content::DigitalIdentityInterstitialType;
using RequestStatusForMetrics =
content::DigitalIdentityProvider::RequestStatusForMetrics;
using DigitalIdentityInterstitialAbortCallback =
content::DigitalIdentityProvider::DigitalIdentityInterstitialAbortCallback;
namespace content {
namespace {
using base::Value;
namespace sdjwt = ::content::sdjwt;
constexpr char kPreviewProtocol[] = "preview";
constexpr char kOpenid4vpProtocolPrefix[] = "openid4vp";
constexpr char kMdlDocumentType[] = "org.iso.18013.5.1.mDL";
constexpr char kOpenid4vpPathRegex[] =
R"(\$\['org\.iso\.18013\.5\.1'\]\['([^\)]*)'\])";
constexpr char kMdocAgeOverDataElementRegex[] = R"(age_over_\d\d)";
constexpr char kMdocAgeInYearsDataElement[] = "age_in_years";
constexpr char kMdocAgeBirthYearDataElement[] = "age_birth_year";
constexpr char kMdocBirthDateDataElement[] = "birth_date";
constexpr char kSubscriptionHint[] = "subscription_hint";
constexpr char kCarrierHint[] = "carrier_hint";
constexpr char kAndroidCarrierHint[] = "android_carrier_hint";
constexpr char kGetPhoneNumberVctValue[] =
"number-verification/device-phone-number/ts43";
constexpr char kVerifyPhoneNumberVctValue[] = "number-verification/verify/ts43";
constexpr char kDigitalIdentityDialogParam[] = "dialog";
constexpr char kDigitalIdentityNoDialogParamValue[] = "no_dialog";
constexpr char kDigitalIdentityLowRiskDialogParamValue[] = "low_risk";
constexpr char kDigitalIdentityHighRiskDialogParamValue[] = "high_risk";
// Returns entry if `dict` has a list with a single dict element for key
// `list_key`.
const Value::Dict* FindSingleElementListEntry(const Value::Dict& dict,
const std::string& list_key) {
const Value::List* list = dict.FindList(list_key);
if (!list || list->size() != 1u) {
return nullptr;
}
return list->front().GetIfDict();
}
// Returns whether an interstitial should be shown for a request which solely
// requests the passed-in claims/data elements.
bool CanClaimBypassInterstitial(const std::string& claim) {
if (re2::RE2::FullMatch(claim, re2::RE2(kMdocAgeOverDataElementRegex))) {
return true;
}
const std::string kClaimsCanBypassInterstitial[] = {
kMdocAgeInYearsDataElement,
kMdocAgeBirthYearDataElement,
kMdocBirthDateDataElement,
kSubscriptionHint,
kCarrierHint,
kAndroidCarrierHint,
};
return std::find(std::begin(kClaimsCanBypassInterstitial),
std::end(kClaimsCanBypassInterstitial),
claim) != std::end(kClaimsCanBypassInterstitial);
}
bool CanVctValueBypassInterstitial(const std::string& vct_value) {
return vct_value == kGetPhoneNumberVctValue ||
vct_value == kVerifyPhoneNumberVctValue;
}
bool CanRequestCredentialBypassInterstitialForOpenid4vpProtocolWithPresentationDefition(
const Value::Dict& request) {
const Value::Dict* presentation_dict =
request.FindDict("presentation_definition");
if (!presentation_dict) {
return false;
}
const Value::Dict* input_descriptor_dict =
FindSingleElementListEntry(*presentation_dict, "input_descriptors");
if (!input_descriptor_dict) {
return false;
}
const std::string* input_descriptor_id =
input_descriptor_dict->FindString("id");
if (!input_descriptor_id || *input_descriptor_id != kMdlDocumentType) {
return false;
}
const Value::Dict* constraints_dict =
input_descriptor_dict->FindDict("constraints");
if (!constraints_dict) {
return false;
}
const Value::Dict* field_dict =
FindSingleElementListEntry(*constraints_dict, "fields");
if (!field_dict) {
return false;
}
const Value::List* field_paths = field_dict->FindList("path");
if (!field_paths) {
return false;
}
if (!field_paths || field_paths->size() != 1u ||
!field_paths->front().is_string()) {
return false;
}
std::string mdoc_data_element;
return re2::RE2::FullMatch(field_paths->front().GetString(),
re2::RE2(kOpenid4vpPathRegex),
&mdoc_data_element) &&
CanClaimBypassInterstitial(mdoc_data_element);
}
bool CanRequestCredentialBypassInterstitialForOpenid4vpProtocolWithDCQL(
const Value::Dict& request) {
const Value::Dict* query_dict = request.FindDict("dcql_query");
if (!query_dict) {
return false;
}
auto credential_to_claims =
[](const Value::Dict& credential) -> std::vector<std::string> {
const Value::List* claims_list = credential.FindList("claims");
if (!claims_list) {
return {};
}
std::vector<std::string> claims;
for (const Value& claim : *claims_list) {
const Value::Dict* claim_dict = claim.GetIfDict();
if (!claim_dict) {
return {};
}
const Value::List* paths = claim_dict->FindList("path");
if (!paths) {
return {};
}
const std::string* claim_name = paths->back().GetIfString();
if (!claim_name) {
return {};
}
claims.push_back(*claim_name);
}
return claims;
};
auto meta_to_vct_values =
[](const Value::Dict& meta) -> std::vector<std::string> {
const Value::List* vct_values_list = meta.FindList("vct_values");
if (!vct_values_list) {
return {};
}
std::vector<std::string> vct_values;
for (const Value& vct_value : *vct_values_list) {
if (!vct_value.is_string()) {
return {};
}
vct_values.push_back(vct_value.GetString());
}
return vct_values;
};
const Value::List* credentials = query_dict->FindList("credentials");
if (!credentials) {
return false;
}
base::flat_set<std::string> all_claims;
base::flat_set<std::string> all_vct_values;
for (const Value& credential : *credentials) {
const Value::Dict* credential_dict = credential.GetIfDict();
if (!credential_dict) {
return false;
}
std::vector<std::string> credential_claims =
credential_to_claims(*credential_dict);
all_claims.insert(credential_claims.begin(), credential_claims.end());
const Value::Dict* meta_dict = credential_dict->FindDict("meta");
if (!meta_dict) {
continue;
}
std::vector<std::string> meta_vct_values = meta_to_vct_values(*meta_dict);
all_vct_values.insert(meta_vct_values.begin(), meta_vct_values.end());
}
return std::ranges::all_of(all_claims, CanClaimBypassInterstitial) &&
std::ranges::all_of(all_vct_values, CanVctValueBypassInterstitial);
}
bool CanRequestCredentialBypassInterstitialForOpenid4vpProtocol(
const Value& request) {
CHECK(request.is_dict());
const Value::Dict* request_dict = &request.GetDict();
// The request may be a JWT. In that case, we need to parse the JWT to get to
// the actual request payload.
std::optional<Value> payload;
if (const std::string* jwt_str = request_dict->FindString("request")) {
std::optional<base::Value::List> parsed_jwt = sdjwt::Jwt::Parse(*jwt_str);
if (!parsed_jwt) {
return false;
}
std::optional<sdjwt::Jwt> jwt = sdjwt::Jwt::From(*parsed_jwt);
if (!jwt) {
return false;
}
payload = base::JSONReader::Read(jwt->payload.value());
if (!payload || !payload->is_dict()) {
return false;
}
request_dict = &payload->GetDict();
}
if (request_dict->contains("presentation_definition")) {
return CanRequestCredentialBypassInterstitialForOpenid4vpProtocolWithPresentationDefition(
*request_dict);
}
if (request_dict->contains("dcql_query")) {
return CanRequestCredentialBypassInterstitialForOpenid4vpProtocolWithDCQL(
*request_dict);
}
return false;
}
bool CanRequestCredentialBypassInterstitialForPreviewProtocol(
const Value& request) {
CHECK(request.is_dict());
const Value::Dict& request_dict = request.GetDict();
const Value::Dict* selector_dict = request_dict.FindDict("selector");
if (!selector_dict) {
return false;
}
const std::string* doctype = selector_dict->FindString("doctype");
if (!doctype || *doctype != kMdlDocumentType) {
return false;
}
const Value::List* fields_list = selector_dict->FindList("fields");
if (!fields_list || fields_list->size() != 1u) {
return false;
}
const Value::Dict* field_dict = fields_list->front().GetIfDict();
if (!field_dict) {
return false;
}
const std::string* mdoc_data_element = field_dict->FindString("name");
return mdoc_data_element && CanClaimBypassInterstitial(*mdoc_data_element);
}
// Returns whether an interstitial should be shown based on the assertions being
// requested.
bool CanRequestCredentialBypassInterstitial(const std::string& protocol,
const Value& request) {
if (!request.is_dict()) {
return false;
}
if (protocol.starts_with(kOpenid4vpProtocolPrefix)) {
return CanRequestCredentialBypassInterstitialForOpenid4vpProtocol(request);
}
return protocol == kPreviewProtocol &&
CanRequestCredentialBypassInterstitialForPreviewProtocol(request);
}
blink::mojom::RequestDigitalIdentityStatus ToRequestDigitalIdentityStatus(
RequestStatusForMetrics status_for_metrics) {
switch (status_for_metrics) {
case RequestStatusForMetrics::kSuccess:
return blink::mojom::RequestDigitalIdentityStatus::kSuccess;
case RequestStatusForMetrics::kErrorAborted:
return blink::mojom::RequestDigitalIdentityStatus::kErrorCanceled;
case RequestStatusForMetrics::kErrorNoRequests:
return blink::mojom::RequestDigitalIdentityStatus::kErrorNoRequests;
case RequestStatusForMetrics::kErrorNoTransientUserActivation:
return blink::mojom::RequestDigitalIdentityStatus::
kErrorNoTransientUserActivation;
case RequestStatusForMetrics::kErrorNoCredential:
case RequestStatusForMetrics::kErrorUserDeclined:
case RequestStatusForMetrics::kErrorOther:
return blink::mojom::RequestDigitalIdentityStatus::kError;
case RequestStatusForMetrics::kErrorInvalidJson:
return blink::mojom::RequestDigitalIdentityStatus::kErrorInvalidJson;
}
}
} // anonymous namespace
// static
base::WeakPtr<DigitalIdentityRequestImpl>
DigitalIdentityRequestImpl::CreateInstance(
RenderFrameHost& host,
mojo::PendingReceiver<blink::mojom::DigitalIdentityRequest> receiver) {
// DigitalIdentityRequestImpl owns itself. It will self-destruct when a mojo
// interface error occurs, the RenderFrameHost is deleted, or the
// RenderFrameHost navigates to a new document.
DigitalIdentityRequestImpl* instance =
new DigitalIdentityRequestImpl(host, std::move(receiver));
return instance->weak_ptr_factory_.GetWeakPtr();
}
// static
std::optional<InterstitialType>
DigitalIdentityRequestImpl::ComputeInterstitialType(
RenderFrameHost& render_frame_host,
const DigitalIdentityProvider* provider,
const std::vector<blink::mojom::DigitalCredentialGetRequestPtr>&
digital_credential_requests) {
std::string dialog_param_value = base::GetFieldTrialParamValueByFeature(
features::kWebIdentityDigitalCredentials, kDigitalIdentityDialogParam);
if (dialog_param_value == kDigitalIdentityNoDialogParamValue) {
return std::nullopt;
}
if (dialog_param_value == kDigitalIdentityHighRiskDialogParamValue) {
return InterstitialType::kHighRisk;
}
if (dialog_param_value == kDigitalIdentityLowRiskDialogParamValue) {
return InterstitialType::kLowRisk;
}
if (provider->IsLastCommittedOriginLowRisk(render_frame_host)) {
return std::nullopt;
}
return std::ranges::all_of(
digital_credential_requests,
[](const blink::mojom::DigitalCredentialGetRequestPtr& request) {
return CanRequestCredentialBypassInterstitial(request->protocol,
request->data);
})
? std::nullopt
: std::optional<InterstitialType>(InterstitialType::kLowRisk);
}
DigitalIdentityRequestImpl::DigitalIdentityRequestImpl(
RenderFrameHost& host,
mojo::PendingReceiver<blink::mojom::DigitalIdentityRequest> receiver)
: DocumentService(host, std::move(receiver)) {}
DigitalIdentityRequestImpl::~DigitalIdentityRequestImpl() = default;
void DigitalIdentityRequestImpl::CompleteRequest(
std::optional<std::string> protocol,
base::expected<DigitalIdentityProvider::DigitalCredential,
RequestStatusForMetrics> response) {
RequestDigitalIdentityStatus status =
response.has_value() ? RequestDigitalIdentityStatus::kSuccess
: ToRequestDigitalIdentityStatus(response.error());
CompleteRequestWithStatus(std::move(protocol), status, std::move(response));
}
void DigitalIdentityRequestImpl::CompleteRequestWithError(
RequestStatusForMetrics status_for_metrics) {
CompleteRequest(/*protocol=*/std::nullopt,
base::unexpected(status_for_metrics));
}
void DigitalIdentityRequestImpl::CompleteRequestWithStatus(
std::optional<std::string> protocol,
RequestDigitalIdentityStatus status,
base::expected<DigitalIdentityProvider::DigitalCredential,
RequestStatusForMetrics> response) {
// Invalidate pending requests in case that the request gets aborted.
weak_ptr_factory_.InvalidateWeakPtrs();
provider_.reset();
update_interstitial_on_abort_callback_.Reset();
base::UmaHistogramEnumeration("Blink.DigitalIdentityRequest.Status",
response.has_value()
? RequestStatusForMetrics::kSuccess
: response.error());
if (response.has_value()) {
// `protocol` is nullopt if and only if there are multiple requests, in
// which case, the browser cannot pick the protocol and hence rely solely on
// the protocol in the response from the digital wallet. If absent, an error
// is returned.
if (!protocol.has_value() && !response->protocol.has_value()) {
CompleteRequestWithError(RequestStatusForMetrics::kErrorOther);
return;
}
// The protocol provided in the digital wallet response is preferred. If
// absent, the protocol specified in the original request will be used
// instead. This fallback mechanism maintains backward compatibility with
// digital wallets that do not include the protocol in their response.
std::move(callback_).Run(
status, response->protocol.has_value() ? response->protocol : protocol,
std::move(response->data));
} else {
std::move(callback_).Run(status, std::nullopt, std::nullopt);
}
}
Value BuildGetRequest(
const std::vector<blink::mojom::DigitalCredentialGetRequestPtr>&
digital_credential_requests) {
auto requests = Value::List();
for (const auto& request : digital_credential_requests) {
auto result = Value::Dict();
result.Set("protocol", request->protocol);
result.Set("data", request->data.Clone());
requests.Append(std::move(result));
}
Value::Dict out = Value::Dict().Set("requests", std::move(requests));
return Value(std::move(out));
}
Value BuildCreateRequest(
std::vector<blink::mojom::DigitalCredentialCreateRequestPtr>
digital_credential_requests) {
auto requests = Value::List();
for (const auto& request : digital_credential_requests) {
auto result = Value::Dict();
result.Set("protocol", request->protocol);
result.Set("data", std::move(request->data));
requests.Append(std::move(result));
}
Value::Dict out = Value::Dict().Set("requests", std::move(requests));
return Value(std::move(out));
}
void DigitalIdentityRequestImpl::Get(
std::vector<blink::mojom::DigitalCredentialGetRequestPtr>
digital_credential_requests,
GetCallback callback) {
if (!webid::IsDigitalCredentialsEnabled()) {
std::move(callback).Run(RequestDigitalIdentityStatus::kError,
/*protocol=*/std::nullopt, /*token=*/std::nullopt);
return;
}
if (render_frame_host().IsNestedWithinFencedFrame()) {
mojo::ReportBadMessage(
"DigitalIdentityRequest should not be allowed in fenced frame "
"trees.");
return;
}
if (callback_) {
// Only allow one in-flight wallet request.
std::move(callback).Run(RequestDigitalIdentityStatus::kErrorTooManyRequests,
/*protocol=*/std::nullopt, /*token=*/std::nullopt);
return;
}
callback_ = std::move(callback);
if (!render_frame_host().HasTransientUserActivation()) {
CompleteRequestWithError(
RequestStatusForMetrics::kErrorNoTransientUserActivation);
return;
}
if (digital_credential_requests.empty()) {
CompleteRequestWithError(RequestStatusForMetrics::kErrorNoRequests);
return;
}
WebContents* web_contents =
WebContents::FromRenderFrameHost(&render_frame_host());
if (!web_contents) {
CompleteRequestWithError(RequestStatusForMetrics::kErrorOther);
return;
}
// If there is only one request, the protocol can determined without waiting
// for the wallet response. This is added for backward compatibility with
// wallet that didn't return the protocol as part of the response.
std::optional<std::string> protocol =
digital_credential_requests.size() == 1u
? std::make_optional(digital_credential_requests[0]->protocol)
: std::nullopt;
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kUseFakeUIForDigitalIdentity)) {
// Post delayed task to enable testing abort.
GetUIThreadTaskRunner()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&DigitalIdentityRequestImpl::CompleteRequest,
weak_ptr_factory_.GetWeakPtr(), protocol,
DigitalIdentityProvider::DigitalCredential(
protocol, Value(Value::Dict().Set(
"token", "fake_test_token")))),
base::Milliseconds(1));
return;
}
provider_ = GetContentClient()->browser()->CreateDigitalIdentityProvider();
if (!provider_) {
CompleteRequestWithError(RequestStatusForMetrics::kErrorOther);
return;
}
if (!render_frame_host().IsActive() ||
render_frame_host().GetVisibilityState() !=
content::PageVisibilityState::kVisible) {
CompleteRequestWithError(RequestStatusForMetrics::kErrorOther);
return;
}
std::optional<InterstitialType> interstitial_type = ComputeInterstitialType(
render_frame_host(), provider_.get(), digital_credential_requests);
Value request_to_send = BuildGetRequest(digital_credential_requests);
if (!interstitial_type) {
OnInterstitialDone(std::move(protocol), std::move(request_to_send),
RequestStatusForMetrics::kSuccess);
return;
}
update_interstitial_on_abort_callback_ =
provider_->ShowDigitalIdentityInterstitial(
*WebContents::FromRenderFrameHost(&render_frame_host()), origin(),
*interstitial_type,
base::BindOnce(&DigitalIdentityRequestImpl::OnInterstitialDone,
weak_ptr_factory_.GetWeakPtr(), std::move(protocol),
std::move(request_to_send)));
}
void DigitalIdentityRequestImpl::Create(
std::vector<blink::mojom::DigitalCredentialCreateRequestPtr>
digital_credential_requests,
CreateCallback callback) {
if (!webid::IsDigitalCredentialsCreationEnabled()) {
std::move(callback).Run(RequestDigitalIdentityStatus::kError,
/*protocol=*/std::nullopt, /*token=*/std::nullopt);
return;
}
if (render_frame_host().IsNestedWithinFencedFrame()) {
mojo::ReportBadMessage(
"DigitalIdentityRequest should not be allowed in fenced frame "
"trees.");
return;
}
if (callback_) {
// Only allow one in-flight wallet request.
std::move(callback).Run(RequestDigitalIdentityStatus::kErrorTooManyRequests,
/*protocol=*/std::nullopt, /*token=*/std::nullopt);
return;
}
callback_ = std::move(callback);
if (!render_frame_host().HasTransientUserActivation()) {
CompleteRequestWithError(
RequestStatusForMetrics::kErrorNoTransientUserActivation);
return;
}
if (digital_credential_requests.empty()) {
CompleteRequestWithError(RequestStatusForMetrics::kErrorNoRequests);
return;
}
WebContents* web_contents =
WebContents::FromRenderFrameHost(&render_frame_host());
if (!web_contents) {
CompleteRequestWithError(RequestStatusForMetrics::kErrorOther);
return;
}
// Store the protocol to return it in tests when no digital wallet is
// available. Pick the first one arbitrarily since it covers most of the tests
// that send only one request.
std::string protocol = digital_credential_requests[0]->protocol;
Value request_to_send =
BuildCreateRequest(std::move(digital_credential_requests));
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kUseFakeUIForDigitalIdentity)) {
// Post delayed task to enable testing abort+.
GetUIThreadTaskRunner()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&DigitalIdentityRequestImpl::CompleteRequest,
weak_ptr_factory_.GetWeakPtr(), protocol,
DigitalIdentityProvider::DigitalCredential(
protocol, Value(Value::Dict().Set(
"token", "fake_test_token")))),
base::Milliseconds(1));
return;
}
provider_ = GetContentClient()->browser()->CreateDigitalIdentityProvider();
if (!provider_) {
CompleteRequestWithError(RequestStatusForMetrics::kErrorOther);
return;
}
if (!render_frame_host().IsActive() ||
render_frame_host().GetVisibilityState() !=
content::PageVisibilityState::kVisible) {
CompleteRequestWithError(RequestStatusForMetrics::kErrorOther);
return;
}
// TODO(crbug.com/378330032): Instead of passing the protocol here, it should
// be read from the wallet response.
provider_->Create(WebContents::FromRenderFrameHost(&render_frame_host()),
origin(), request_to_send,
base::BindOnce(&DigitalIdentityRequestImpl::CompleteRequest,
weak_ptr_factory_.GetWeakPtr(), protocol));
}
void DigitalIdentityRequestImpl::Abort() {
if (!callback_) {
// Renderer sent abort request after the browser sent the callback but
// before the renderer received it.
return;
}
if (update_interstitial_on_abort_callback_) {
std::move(update_interstitial_on_abort_callback_).Run();
}
CompleteRequestWithStatus(
/*protocol=*/std::nullopt, RequestDigitalIdentityStatus::kErrorCanceled,
base::unexpected(RequestStatusForMetrics::kErrorAborted));
}
void DigitalIdentityRequestImpl::OnInterstitialDone(
std::optional<std::string> protocol,
Value request_to_send,
RequestStatusForMetrics status_after_interstitial) {
if (status_after_interstitial != RequestStatusForMetrics::kSuccess) {
CompleteRequestWithError(status_after_interstitial);
return;
}
provider_->Get(
WebContents::FromRenderFrameHost(&render_frame_host()), origin(),
request_to_send,
base::BindOnce(&DigitalIdentityRequestImpl::CompleteRequest,
weak_ptr_factory_.GetWeakPtr(), std::move(protocol)));
}
} // namespace content