blob: 56197c5e48c2b80f7bebe8afbdd9685f5063ffb8 [file] [log] [blame]
// 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 "services/network/sct_auditing_cache.h"
#include "base/callback.h"
#include "base/feature_list.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_functions.h"
#include "base/rand_util.h"
#include "components/version_info/version_info.h"
#include "crypto/secure_hash.h"
#include "crypto/sha2.h"
#include "net/base/elements_upload_data_stream.h"
#include "net/base/hash_value.h"
#include "net/base/host_port_pair.h"
#include "net/base/load_flags.h"
#include "net/base/net_errors.h"
#include "net/base/request_priority.h"
#include "net/base/upload_bytes_element_reader.h"
#include "net/cert/ct_serialization.h"
#include "net/cert/sct_status_flags.h"
#include "net/cert/signed_certificate_timestamp.h"
#include "net/cert/signed_certificate_timestamp_and_status.h"
#include "net/cert/x509_certificate.h"
#include "net/http/http_status_code.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "net/url_request/url_request.h"
#include "net/url_request/url_request_context.h"
#include "services/network/network_context.h"
#include "services/network/network_service.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "services/network/public/proto/sct_audit_report.pb.h"
#include "third_party/boringssl/src/include/openssl/pool.h"
#include "third_party/boringssl/src/include/openssl/sha.h"
namespace network {
namespace {
constexpr int kSendSCTReportTimeoutSeconds = 30;
// Records the high-water mark of the cache size (in number of reports).
void RecordSCTAuditingCacheHighWaterMarkMetrics(size_t hwm) {
base::UmaHistogramCounts1000("Security.SCTAuditing.OptIn.CacheHWM", hwm);
}
// Records whether a new report is deduplicated against an existing report in
// the cache.
void RecordSCTAuditingReportDeduplicatedMetrics(bool deduplicated) {
base::UmaHistogramBoolean("Security.SCTAuditing.OptIn.ReportDeduplicated",
deduplicated);
}
// Records whether a new report that wasn't deduplicated was sampled for
// sending to the reporting server.
void RecordSCTAuditingReportSampledMetrics(bool sampled) {
base::UmaHistogramBoolean("Security.SCTAuditing.OptIn.ReportSampled",
sampled);
}
// Records the size of a report that will be sent to the reporting server, in
// bytes. Used to track how much bandwidth is consumed by sending reports.
void RecordSCTAuditingReportSizeMetrics(size_t report_size) {
base::UmaHistogramCounts10M("Security.SCTAuditing.OptIn.ReportSize",
report_size);
}
// Records whether sending the report to the reporting server succeeded for each
// report sent.
void RecordSCTAuditingReportSucceededMetrics(bool success) {
base::UmaHistogramBoolean("Security.SCTAuditing.OptIn.ReportSucceeded",
success);
}
// Owns the SimpleURLLoader and runs the callback and then deletes itself when
// the response arrives.
class SimpleURLLoaderOwner {
public:
using LoaderDoneCallback =
base::OnceCallback<void(int /* net_error */,
int /* http_response_code */)>;
SimpleURLLoaderOwner(mojom::URLLoaderFactory* url_loader_factory,
std::unique_ptr<SimpleURLLoader> loader,
LoaderDoneCallback done_callback)
: loader_(std::move(loader)), done_callback_(std::move(done_callback)) {
// We only care about whether the report was successfully received, so we
// download the headers only.
// If the loader is destroyed, the callback will be canceled, so using
// base::Unretained here is safe.
loader_->DownloadHeadersOnly(
url_loader_factory,
base::BindOnce(&SimpleURLLoaderOwner::OnURLLoaderComplete,
base::Unretained(this)));
}
SimpleURLLoaderOwner(const SimpleURLLoaderOwner&) = delete;
SimpleURLLoaderOwner& operator=(const SimpleURLLoaderOwner&) = delete;
private:
~SimpleURLLoaderOwner() = default;
void OnURLLoaderComplete(scoped_refptr<net::HttpResponseHeaders> headers) {
if (done_callback_) {
int response_code = 0;
if (loader_->ResponseInfo() && loader_->ResponseInfo()->headers)
response_code = loader_->ResponseInfo()->headers->response_code();
std::move(done_callback_).Run(loader_->NetError(), response_code);
}
delete this;
}
std::unique_ptr<SimpleURLLoader> loader_;
LoaderDoneCallback done_callback_;
};
} // namespace
SCTAuditingCache::SCTAuditingCache(size_t cache_size)
: cache_(cache_size), cache_size_hwm_(0) {}
SCTAuditingCache::~SCTAuditingCache() {
RecordSCTAuditingCacheHighWaterMarkMetrics(cache_size_hwm_);
}
void SCTAuditingCache::MaybeEnqueueReport(
NetworkContext* context,
const net::HostPortPair& host_port_pair,
const net::X509Certificate* validated_certificate_chain,
const net::SignedCertificateTimestampAndStatusList&
signed_certificate_timestamps) {
if (!enabled_ || !context->is_sct_auditing_enabled())
return;
auto report = std::make_unique<sct_auditing::SCTClientReport>();
auto* tls_report = report->add_certificate_report();
// Encode the SCTs in the report and generate the cache key. The hash of the
// SCTs is used as the cache key to deduplicate reports with the same SCTs.
// Constructing the report in parallel with computing the hash avoids
// encoding the SCTs multiple times and avoids extra copies.
SHA256_CTX ctx;
SHA256_Init(&ctx);
for (const auto& sct : signed_certificate_timestamps) {
// Only audit valid SCTs. This ensures that they come from a known log, have
// a valid signature, and thus are expected to be public certificates. If
// there are no valid SCTs, there's no need to report anything.
if (sct.status != net::ct::SCT_STATUS_OK)
continue;
auto* sct_source_and_status = tls_report->add_included_sct();
// TODO(crbug.com/1082860): Update the proto to remove the status entirely
// since only valid SCTs are reported now.
sct_source_and_status->set_status(
sct_auditing::SCTWithVerifyStatus::SctVerifyStatus::
SCTWithVerifyStatus_SctVerifyStatus_OK);
net::ct::EncodeSignedCertificateTimestamp(
sct.sct, sct_source_and_status->mutable_serialized_sct());
SHA256_Update(&ctx, sct_source_and_status->serialized_sct().data(),
sct_source_and_status->serialized_sct().size());
}
// Don't handle reports if there were no valid SCTs.
if (tls_report->included_sct().empty())
return;
net::SHA256HashValue cache_key;
SHA256_Final(reinterpret_cast<uint8_t*>(&cache_key), &ctx);
// Check if the SCTs are already in the cache. This will update the last seen
// time if they are present in the cache.
auto it = cache_.Get(cache_key);
if (it != cache_.end()) {
RecordSCTAuditingReportDeduplicatedMetrics(true);
return;
}
RecordSCTAuditingReportDeduplicatedMetrics(false);
report->set_user_agent(version_info::GetProductNameAndVersionForUserAgent());
// Set the `cache_key` with an null report. If we don't choose to sample these
// SCTs, then we don't need to store a report as we won't reference it again
// (and can save on memory usage). If we do choose to sample these SCTs, we
// then construct the report and move it into the cache entry for `cache_key`.
cache_.Put(cache_key, nullptr);
if (base::RandDouble() > sampling_rate_) {
RecordSCTAuditingReportSampledMetrics(false);
return;
}
RecordSCTAuditingReportSampledMetrics(true);
auto* connection_context = tls_report->mutable_context();
base::TimeDelta time_since_unix_epoch =
base::Time::Now() - base::Time::UnixEpoch();
connection_context->set_time_seen(time_since_unix_epoch.InSeconds());
auto* origin = connection_context->mutable_origin();
origin->set_hostname(host_port_pair.host());
origin->set_port(host_port_pair.port());
// Convert the certificate chain to a PEM encoded vector, and then initialize
// the proto's |certificate_chain| repeated field using the data in the
// vector. Note that GetPEMEncodedChain() can fail, but we still want to
// enqueue the report for the SCTs (in that case, |certificate_chain| is not
// guaranteed to be valid).
std::vector<std::string> certificate_chain;
validated_certificate_chain->GetPEMEncodedChain(&certificate_chain);
*connection_context->mutable_certificate_chain() = {certificate_chain.begin(),
certificate_chain.end()};
// Log the size of the report. This only tracks reports that are not dropped
// due to sampling (as those reports will just be empty).
RecordSCTAuditingReportSizeMetrics(report->ByteSizeLong());
cache_.Put(cache_key, std::move(report));
// Track high-water-mark for the size of the cache.
if (cache_.size() > cache_size_hwm_)
cache_size_hwm_ = cache_.size();
SendReport(cache_key);
}
sct_auditing::SCTClientReport* SCTAuditingCache::GetPendingReport(
const net::SHA256HashValue& cache_key) {
auto it = cache_.Get(cache_key);
if (it == cache_.end())
return nullptr;
return it->second.get();
}
void SCTAuditingCache::SendReport(const net::SHA256HashValue& cache_key) {
// Ensure that the URLLoaderFactory is still connected.
if (!url_loader_factory_ || !url_loader_factory_.is_connected()) {
// TODO(cthomp): Should this signal to embedder that something has failed?
return;
}
// (1) Get the report from the cache, if it exists.
auto* report = GetPendingReport(cache_key);
if (!report) {
// TODO(crbug.com/1082860): This generally means that the report has been
// evicted from the cache. We should handle this more gracefully once we
// implement retrying reports as that will increase the likelihood.
return;
}
// (2) Create a SimpleURLLoader for the request.
auto report_request = std::make_unique<ResourceRequest>();
report_request->url = report_uri_;
report_request->method = "POST";
report_request->load_flags = net::LOAD_DISABLE_CACHE;
report_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
auto url_loader = SimpleURLLoader::Create(
std::move(report_request),
static_cast<net::NetworkTrafficAnnotationTag>(traffic_annotation_));
url_loader->SetTimeoutDuration(
base::TimeDelta::FromSeconds(kSendSCTReportTimeoutSeconds));
// (3) Serialize the report and attach it to the loader.
std::string report_data;
bool ok = report->SerializeToString(&report_data);
DCHECK(ok);
url_loader->AttachStringForUpload(report_data, "application/octet-stream");
// (4) Pass the loader to an owner for its lifetime. This initiates the
// request and will handle calling `callback` when the request completes
// (on success or error) or times out.
// The callback takes a WeakPtr as the SCTAuditingCache or Network Service
// could be destroyed before the callback triggers.
auto done_callback = base::BindOnce(&SCTAuditingCache::OnReportComplete,
weak_factory_.GetWeakPtr(), cache_key);
new SimpleURLLoaderOwner(url_loader_factory_.get(), std::move(url_loader),
std::move(done_callback));
}
void SCTAuditingCache::OnReportComplete(const net::SHA256HashValue& cache_key,
int net_error,
int http_response_code) {
// TODO(crbug.com/1082860): Mark report as complete on success, handle retries
// on failures. For now we empty the cache entry to save space once it has
// been successfully sent.
bool success = net_error == net::OK && http_response_code == net::HTTP_OK;
if (success) {
if (GetPendingReport(cache_key))
cache_.Put(cache_key, nullptr);
}
RecordSCTAuditingReportSucceededMetrics(success);
}
void SCTAuditingCache::ClearCache() {
cache_.Clear();
}
} // namespace network