blob: 1566bb38849ad10c449a23a040f4d1a746e94369 [file] [log] [blame]
// Copyright 2025 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/delegation/email_verification_request.h"
#include "base/base64url.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/strings/string_split.h"
#include "base/time/time.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/webid/delegation/jwt_signer.h"
#include "content/browser/webid/delegation/sd_jwt.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/storage_partition.h"
#include "crypto/keypair.h"
#include "url/origin.h"
namespace content::webid {
std::optional<std::string> GetDomainFromEmail(const std::string& email) {
auto parts = base::RSplitStringOnce(email, "@");
if (!parts) {
return std::nullopt;
}
if (parts->first.empty() || parts->second.empty()) {
return std::nullopt;
}
// Use GURL to validate that the domain is a valid host.
// TODO(crbug.com/380367784): consider better ways to validate if
// the email domain is well formed.
GURL url("https://" + std::string(parts->second));
if (!url.is_valid() || !url.has_host() || url.GetHost() != parts->second) {
return std::nullopt;
}
return std::string(parts->second);
}
EmailVerificationRequest::EmailVerificationRequest(
RenderFrameHostImpl& render_frame_host)
: EmailVerificationRequest(
IdpNetworkRequestManager::Create(&render_frame_host),
std::make_unique<DnsRequest>(base::BindRepeating(
[](RenderFrameHost* rfh) -> network::mojom::NetworkContext* {
return rfh->GetStoragePartition()->GetNetworkContext();
},
&render_frame_host)),
render_frame_host.GetSafeRef()) {}
EmailVerificationRequest::EmailVerificationRequest(
std::unique_ptr<IdpNetworkRequestManager> network_manager,
std::unique_ptr<DnsRequest> dns_request,
base::SafeRef<RenderFrameHost> render_frame_host)
: dns_request_(std::move(dns_request)),
network_manager_(std::move(network_manager)),
render_frame_host_(render_frame_host) {}
EmailVerificationRequest::~EmailVerificationRequest() = default;
sdjwt::Jwt EmailVerificationRequest::CreateRequestToken(
const std::string& email,
const sdjwt::Jwk& public_key) {
sdjwt::Header header;
header.alg = "RS256";
header.typ = "JWT";
header.jwk = public_key;
CHECK(header.jwk);
base::Time now = base::Time::Now();
// TODO(crbug.com/380367784): figure out what's the right
// expiration time for the request token.
base::TimeDelta ttl = base::Minutes(5);
base::Time expiration = now + ttl;
sdjwt::Payload payload;
payload.email = email;
// TODO(crbug.com/380367784): figure out why/whether the
// nonce is needed here. Use a hardcoded value for now.
payload.nonce = "--a-fake-nonce--";
// TODO(crbug.com/380367784): check if `render_frame_host_` isn't an
// opaque origin, or any other validation that might be
// necessary.
payload.aud = render_frame_host_->GetLastCommittedOrigin().Serialize();
payload.exp = expiration;
payload.iat = now;
sdjwt::Jwt jwt;
jwt.header = *header.ToJson();
jwt.payload = *payload.ToJson();
return jwt;
}
// The email verification process starts once the user
// goes through Step 1 and 2 described here:
//
// https://github.com/dickhardt/email-verification-protocol?tab=readme-ov-file#3-token-request
void EmailVerificationRequest::Send(
const std::string& email,
const std::string& nonce,
EmailVerifier::OnEmailVerifiedCallback callback) {
// Step 3: Token Request
// Step 3.1: the browser extracts the domain from the email address and
// asks the DNS server who the issuer is:
std::optional<std::string> domain = GetDomainFromEmail(email);
if (!domain) {
std::move(callback).Run(std::nullopt);
return;
}
std::string hostname = "_email-verification." + *domain;
dns_request_->SendRequest(
hostname, base::BindOnce(&EmailVerificationRequest::OnDnsRequestComplete,
weak_ptr_factory_.GetWeakPtr(), email, nonce,
std::move(callback)));
}
void EmailVerificationRequest::OnDnsRequestComplete(
const std::string& email,
const std::string& nonce,
EmailVerifier::OnEmailVerifiedCallback callback,
const std::optional<std::vector<std::string>>& text_records) {
// Step 3.2: when the DNS response is received, the browser
// parses the TXT record to extract the issuer's origin.
if (!text_records || text_records->size() != 1) {
std::move(callback).Run(std::nullopt);
return;
}
const std::string& record = (*text_records)[0];
static constexpr char kIssPrefix[] = "iss=";
if (!base::StartsWith(record, kIssPrefix, base::CompareCase::SENSITIVE)) {
std::move(callback).Run(std::nullopt);
return;
}
std::string iss = record.substr(sizeof(kIssPrefix) - 1);
if (iss.empty()) {
std::move(callback).Run(std::nullopt);
return;
}
// If the issuer is found, the browser fetches the issuer's
// .well-known/web-identity file:
GURL issuer("https://" + iss);
network_manager_->FetchWellKnown(
issuer,
base::BindOnce(&EmailVerificationRequest::OnWellKnownFetched,
weak_ptr_factory_.GetWeakPtr(), email,
url::Origin::Create(issuer), nonce, std::move(callback)));
}
void EmailVerificationRequest::OnWellKnownFetched(
const std::string& email,
const url::Origin& issuer,
const std::string& nonce,
EmailVerifier::OnEmailVerifiedCallback callback,
IdpNetworkRequestManager::FetchStatus status,
const IdpNetworkRequestManager::WellKnown& well_known) {
// Step 3.3: when the .well-known/web-identity file is fetched,
// the browser checks that the issuance_endpoint is present.
if (status.parse_status != IdpNetworkRequestManager::ParseStatus::kSuccess) {
std::move(callback).Run(std::nullopt);
return;
}
if (well_known.issuance_endpoint.is_empty()) {
std::move(callback).Run(std::nullopt);
return;
}
// Step 3.4: if the issuance_endpoint is present, the browser
// creates a signed request token.
// TODO(crbug.com/380367784): understand and document why RSA was
// preferred over ECDSA here.
auto private_key = std::make_unique<crypto::keypair::PrivateKey>(
crypto::keypair::PrivateKey::GenerateRsa2048());
CHECK(private_key);
std::optional<sdjwt::Jwk> public_key = sdjwt::ExportPublicKey(*private_key);
CHECK(public_key);
sdjwt::Jwt jwt = CreateRequestToken(email, *public_key);
auto signer = sdjwt::CreateJwtSigner(*private_key);
CHECK(jwt.Sign(std::move(signer)));
auto request_token = jwt.Serialize();
CHECK(!request_token->empty());
// Step 3.5: finally, the browser sends a POST request to the
// issuance_endpoint with the request_token as a form parameter.
network_manager_->SendTokenRequest(
well_known.issuance_endpoint, email,
"request_token=" + request_token.value(),
/*idp_blindness=*/true,
base::BindOnce(&EmailVerificationRequest::OnTokenRequestComplete,
weak_ptr_factory_.GetWeakPtr(), nonce,
std::move(private_key), std::move(callback)),
// TODO(crbug.com/380367784): we should probably not be using a
// DoNothing here, but we also don't support the continuation_url.
base::DoNothing(),
// TODO(crbug.com/380367784): figure out how to measure the feature
// here.
base::DoNothing());
}
void EmailVerificationRequest::OnTokenRequestComplete(
const std::string& nonce,
std::unique_ptr<crypto::keypair::PrivateKey> private_key,
EmailVerifier::OnEmailVerifiedCallback callback,
IdpNetworkRequestManager::FetchStatus token_status,
IdpNetworkRequestManager::TokenResult&& result) {
// Step 5: Token Presentation
if (token_status.parse_status !=
IdpNetworkRequestManager::ParseStatus::kSuccess ||
!result.token || !result.token->is_string()) {
std::move(callback).Run(std::nullopt);
return;
}
auto token = sdjwt::SdJwt::Parse(result.token->GetString());
// Step 5.1: The browser parses and verifies if the SD-JWT
// is valid.
// TODO: check if all of the necessary fields of the SD-JWT
// are present and valid.
if (!token) {
std::move(callback).Run(std::nullopt);
return;
}
auto sd_jwt = sdjwt::SdJwt::From(*token);
if (!sd_jwt) {
std::move(callback).Run(std::nullopt);
return;
}
// Step 5.2: bind the nonce and the website to the
// key binding JWT.
sdjwt::SdJwtKb sd_jwt_kb;
sd_jwt_kb.sd_jwt = *sd_jwt;
sdjwt::Header header;
header.alg = "RS256";
header.typ = "kb+jwt";
sdjwt::Payload payload;
payload.aud = render_frame_host_->GetLastCommittedOrigin().Serialize();
payload.nonce = nonce;
sdjwt::Jwt kb_jwt;
kb_jwt.header = *header.ToJson();
kb_jwt.payload = *payload.ToJson();
auto signer = sdjwt::CreateJwtSigner(*private_key);
if (!kb_jwt.Sign(std::move(signer))) {
std::move(callback).Run(std::nullopt);
return;
}
sd_jwt_kb.kb_jwt = kb_jwt;
// Step 5.3: the browser notifies the page that
// the SD-JWT+KB is ready.
std::move(callback).Run(sd_jwt_kb.Serialize());
}
} // namespace content::webid