summaryrefslogtreecommitdiff
path: root/chromium/content/browser/web_package/signed_exchange_envelope.cc
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/content/browser/web_package/signed_exchange_envelope.cc')
-rw-r--r--chromium/content/browser/web_package/signed_exchange_envelope.cc374
1 files changed, 374 insertions, 0 deletions
diff --git a/chromium/content/browser/web_package/signed_exchange_envelope.cc b/chromium/content/browser/web_package/signed_exchange_envelope.cc
new file mode 100644
index 00000000000..b42ccc07872
--- /dev/null
+++ b/chromium/content/browser/web_package/signed_exchange_envelope.cc
@@ -0,0 +1,374 @@
+// Copyright 2018 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 "content/browser/web_package/signed_exchange_envelope.h"
+
+#include <utility>
+
+#include "base/callback.h"
+#include "base/format_macros.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_piece.h"
+#include "base/strings/stringprintf.h"
+#include "base/trace_event/trace_event.h"
+#include "components/cbor/cbor_reader.h"
+#include "content/browser/web_package/signed_exchange_consts.h"
+#include "content/browser/web_package/signed_exchange_utils.h"
+#include "net/http/http_util.h"
+#include "url/origin.h"
+
+namespace content {
+
+namespace {
+
+// IsStateful{Request,Response}Header return true if |name| is a stateful
+// header field. Stateful header fields will cause validation failure of
+// signed exchanges.
+// Note that |name| must be lower-cased.
+// https://wicg.github.io/webpackage/draft-yasskin-httpbis-origin-signed-exchanges-impl.html#stateful-headers
+bool IsStatefulRequestHeader(base::StringPiece name) {
+ DCHECK_EQ(name, base::ToLowerASCII(name));
+
+ const char* const kStatefulRequestHeaders[] = {
+ "authorization", "cookie", "cookie2", "proxy-authorization",
+ "sec-webSocket-key"};
+
+ for (const char* field : kStatefulRequestHeaders) {
+ if (name == field)
+ return true;
+ }
+ return false;
+}
+
+bool IsStatefulResponseHeader(base::StringPiece name) {
+ DCHECK_EQ(name, base::ToLowerASCII(name));
+
+ const char* const kStatefulResponseHeaders[] = {
+ "authentication-control",
+ "authentication-info",
+ "optional-www-authenticate",
+ "proxy-authenticate",
+ "proxy-authentication-info",
+ "sec-websocket-accept",
+ "set-cookie",
+ "set-cookie2",
+ "setprofile",
+ "www-authenticate",
+ };
+
+ for (const char* field : kStatefulResponseHeaders) {
+ if (name == field)
+ return true;
+ }
+ return false;
+}
+
+bool IsMethodCacheable(base::StringPiece method) {
+ return method == "GET" || method == "HEAD" || method == "POST";
+}
+
+bool ParseRequestMap(const cbor::CBORValue& value,
+ SignedExchangeEnvelope* out,
+ SignedExchangeDevToolsProxy* devtools_proxy) {
+ TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("loading"), "ParseRequestMap");
+ if (!value.is_map()) {
+ signed_exchange_utils::ReportErrorAndTraceEvent(
+ devtools_proxy,
+ base::StringPrintf(
+ "Expected request map, got non-map type. Actual type: %d",
+ static_cast<int>(value.type())));
+ return false;
+ }
+
+ const cbor::CBORValue::MapValue& request_map = value.GetMap();
+
+ auto url_iter = request_map.find(
+ cbor::CBORValue(kUrlKey, cbor::CBORValue::Type::BYTE_STRING));
+ if (url_iter == request_map.end() || !url_iter->second.is_bytestring()) {
+ signed_exchange_utils::ReportErrorAndTraceEvent(
+ devtools_proxy, ":url is not found or not a bytestring.");
+ return false;
+ }
+ out->set_request_url(GURL(url_iter->second.GetBytestringAsString()));
+ if (!out->request_url().is_valid()) {
+ signed_exchange_utils::ReportErrorAndTraceEvent(devtools_proxy,
+ ":url is not a valid URL.");
+ return false;
+ }
+ if (out->request_url().has_ref()) {
+ signed_exchange_utils::ReportErrorAndTraceEvent(
+ devtools_proxy, ":url can't have a fragment.");
+ return false;
+ }
+ if (!out->request_url().SchemeIs("https")) {
+ signed_exchange_utils::ReportErrorAndTraceEvent(
+ devtools_proxy, ":url scheme must be 'https'.");
+ return false;
+ }
+
+ auto method_iter = request_map.find(
+ cbor::CBORValue(kMethodKey, cbor::CBORValue::Type::BYTE_STRING));
+ if (method_iter == request_map.end() ||
+ !method_iter->second.is_bytestring()) {
+ signed_exchange_utils::ReportErrorAndTraceEvent(
+ devtools_proxy, ":method is not found or not a bytestring.");
+ return false;
+ }
+ base::StringPiece method_str = method_iter->second.GetBytestringAsString();
+ // 3. If exchange’s request method is not safe (Section 4.2.1 of [RFC7231])
+ // or not cacheable (Section 4.2.3 of [RFC7231]), return “invalid”.
+ // [spec text]
+ if (!net::HttpUtil::IsMethodSafe(method_str.as_string()) ||
+ !IsMethodCacheable(method_str)) {
+ signed_exchange_utils::ReportErrorAndTraceEvent(
+ devtools_proxy,
+ base::StringPrintf(
+ "Request method is not safe or not cacheable. method: %s",
+ method_str.as_string().c_str()));
+ return false;
+ }
+ out->set_request_method(method_str);
+
+ for (const auto& it : request_map) {
+ if (!it.first.is_bytestring() || !it.second.is_bytestring()) {
+ signed_exchange_utils::ReportErrorAndTraceEvent(
+ devtools_proxy, "Non-bytestring value in the request map.");
+ return false;
+ }
+ base::StringPiece name_str = it.first.GetBytestringAsString();
+ if (name_str == kUrlKey || name_str == kMethodKey)
+ continue;
+
+ // TODO(kouhei): Add spec ref here once
+ // https://github.com/WICG/webpackage/issues/161 is resolved.
+ if (name_str != base::ToLowerASCII(name_str)) {
+ signed_exchange_utils::ReportErrorAndTraceEvent(
+ devtools_proxy,
+ base::StringPrintf(
+ "Request header name should be lower-cased. header name: %s",
+ name_str.as_string().c_str()));
+ return false;
+ }
+
+ // 4. If exchange’s headers contain a stateful header field, as defined in
+ // Section 4.1, return “invalid”. [spec text]
+ if (IsStatefulRequestHeader(name_str)) {
+ signed_exchange_utils::ReportErrorAndTraceEvent(
+ devtools_proxy,
+ base::StringPrintf(
+ "Exchange contains stateful request header. header name: %s",
+ name_str.as_string().c_str()));
+ return false;
+ }
+ }
+ return true;
+}
+
+bool ParseResponseMap(const cbor::CBORValue& value,
+ SignedExchangeEnvelope* out,
+ SignedExchangeDevToolsProxy* devtools_proxy) {
+ TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("loading"), "ParseResponseMap");
+ if (!value.is_map()) {
+ signed_exchange_utils::ReportErrorAndTraceEvent(
+ devtools_proxy,
+ base::StringPrintf(
+ "Expected request map, got non-map type. Actual type: %d",
+ static_cast<int>(value.type())));
+ return false;
+ }
+
+ const cbor::CBORValue::MapValue& response_map = value.GetMap();
+ auto status_iter = response_map.find(
+ cbor::CBORValue(kStatusKey, cbor::CBORValue::Type::BYTE_STRING));
+ if (status_iter == response_map.end() ||
+ !status_iter->second.is_bytestring()) {
+ signed_exchange_utils::ReportErrorAndTraceEvent(
+ devtools_proxy, ":status is not found or not a bytestring.");
+ return false;
+ }
+ base::StringPiece response_code_str =
+ status_iter->second.GetBytestringAsString();
+ int response_code;
+ if (!base::StringToInt(response_code_str, &response_code)) {
+ signed_exchange_utils::ReportErrorAndTraceEvent(
+ devtools_proxy, "Failed to parse status code to integer.");
+ return false;
+ }
+ out->set_response_code(static_cast<net::HttpStatusCode>(response_code));
+
+ for (const auto& it : response_map) {
+ if (!it.first.is_bytestring() || !it.second.is_bytestring()) {
+ signed_exchange_utils::ReportErrorAndTraceEvent(
+ devtools_proxy, "Non-bytestring value in the response map.");
+ return false;
+ }
+ base::StringPiece name_str = it.first.GetBytestringAsString();
+ if (name_str == kStatusKey)
+ continue;
+ if (!net::HttpUtil::IsValidHeaderName(name_str)) {
+ signed_exchange_utils::ReportErrorAndTraceEvent(
+ devtools_proxy,
+ base::StringPrintf("Invalid header name. header_name: %s",
+ name_str.as_string().c_str()));
+ return false;
+ }
+
+ // TODO(kouhei): Add spec ref here once
+ // https://github.com/WICG/webpackage/issues/161 is resolved.
+ if (name_str != base::ToLowerASCII(name_str)) {
+ signed_exchange_utils::ReportErrorAndTraceEvent(
+ devtools_proxy,
+ base::StringPrintf(
+ "Response header name should be lower-cased. header_name: %s",
+ name_str.as_string().c_str()));
+ return false;
+ }
+
+ // 4. If exchange’s headers contain a stateful header field, as defined in
+ // Section 4.1, return “invalid”. [spec text]
+ if (IsStatefulResponseHeader(name_str)) {
+ signed_exchange_utils::ReportErrorAndTraceEvent(
+ devtools_proxy,
+ base::StringPrintf(
+ "Exchange contains stateful response header. header_name: %s",
+ name_str.as_string().c_str()));
+ return false;
+ }
+
+ base::StringPiece value_str = it.second.GetBytestringAsString();
+ if (!net::HttpUtil::IsValidHeaderValue(value_str)) {
+ signed_exchange_utils::ReportErrorAndTraceEvent(devtools_proxy,
+ "Invalid header value.");
+ return false;
+ }
+ if (!out->AddResponseHeader(name_str, value_str)) {
+ signed_exchange_utils::ReportErrorAndTraceEvent(
+ devtools_proxy,
+ base::StringPrintf("Duplicate header value. header_name: %s",
+ name_str.as_string().c_str()));
+ return false;
+ }
+ }
+ return true;
+}
+
+} // namespace
+
+// static
+base::Optional<SignedExchangeEnvelope> SignedExchangeEnvelope::Parse(
+ base::StringPiece signature_header_field,
+ base::span<const uint8_t> cbor_header,
+ SignedExchangeDevToolsProxy* devtools_proxy) {
+ TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("loading"),
+ "SignedExchangeEnvelope::Parse");
+ cbor::CBORReader::DecoderError error;
+ base::Optional<cbor::CBORValue> value =
+ cbor::CBORReader::Read(cbor_header, &error);
+ if (!value.has_value()) {
+ signed_exchange_utils::ReportErrorAndTraceEvent(
+ devtools_proxy,
+ base::StringPrintf("Failed to decode CBORValue. CBOR error: %s",
+ cbor::CBORReader::ErrorCodeToString(error)));
+ return base::nullopt;
+ }
+ if (!value->is_array()) {
+ signed_exchange_utils::ReportErrorAndTraceEvent(
+ devtools_proxy,
+ base::StringPrintf(
+ "Expected top-level CBORValue to be an array. Actual type : %d",
+ static_cast<int>(value->type())));
+ return base::nullopt;
+ }
+
+ const cbor::CBORValue::ArrayValue& top_level_array = value->GetArray();
+ constexpr size_t kTopLevelArraySize = 2;
+ if (top_level_array.size() != kTopLevelArraySize) {
+ signed_exchange_utils::ReportErrorAndTraceEvent(
+ devtools_proxy,
+ base::StringPrintf("Expected top-level array to have 2 elements. "
+ "Actual element count: %" PRIuS,
+ top_level_array.size()));
+ return base::nullopt;
+ }
+
+ SignedExchangeEnvelope ret;
+
+ if (!ParseRequestMap(top_level_array[0], &ret, devtools_proxy)) {
+ signed_exchange_utils::ReportErrorAndTraceEvent(
+ devtools_proxy, "Failed to parse request map.");
+ return base::nullopt;
+ }
+ if (!ParseResponseMap(top_level_array[1], &ret, devtools_proxy)) {
+ signed_exchange_utils::ReportErrorAndTraceEvent(
+ devtools_proxy, "Failed to parse response map.");
+ return base::nullopt;
+ }
+
+ base::Optional<std::vector<SignedExchangeSignatureHeaderField::Signature>>
+ signatures = SignedExchangeSignatureHeaderField::ParseSignature(
+ signature_header_field, devtools_proxy);
+ if (!signatures || signatures->empty()) {
+ signed_exchange_utils::ReportErrorAndTraceEvent(
+ devtools_proxy, "Failed to parse signature header field.");
+ return base::nullopt;
+ }
+
+ // TODO(https://crbug.com/850475): Support multiple signatures.
+ ret.signature_ = (*signatures)[0];
+
+ // https://wicg.github.io/webpackage/draft-yasskin-http-origin-signed-responses.html#cross-origin-trust
+ // If the signature’s “validity-url” parameter is not same-origin with
+ // exchange’s effective request URI (Section 5.5 of [RFC7230]), return
+ // “invalid” [spec text]
+ const GURL request_url = ret.request_url();
+ const GURL validity_url = ret.signature().validity_url;
+ if (!url::IsSameOriginWith(request_url, validity_url)) {
+ signed_exchange_utils::ReportErrorAndTraceEvent(
+ devtools_proxy, "Validity URL must be same-origin with request URL.");
+ return base::nullopt;
+ }
+
+ return std::move(ret);
+}
+
+SignedExchangeEnvelope::SignedExchangeEnvelope() = default;
+SignedExchangeEnvelope::SignedExchangeEnvelope(const SignedExchangeEnvelope&) =
+ default;
+SignedExchangeEnvelope::SignedExchangeEnvelope(SignedExchangeEnvelope&&) =
+ default;
+SignedExchangeEnvelope::~SignedExchangeEnvelope() = default;
+SignedExchangeEnvelope& SignedExchangeEnvelope::operator=(
+ SignedExchangeEnvelope&&) = default;
+
+bool SignedExchangeEnvelope::AddResponseHeader(base::StringPiece name,
+ base::StringPiece value) {
+ std::string name_str = name.as_string();
+ DCHECK_EQ(name_str, base::ToLowerASCII(name))
+ << "Response header names should be always lower-cased.";
+ if (response_headers_.find(name_str) != response_headers_.end())
+ return false;
+
+ response_headers_.emplace(std::move(name_str), value.as_string());
+ return true;
+}
+
+scoped_refptr<net::HttpResponseHeaders>
+SignedExchangeEnvelope::BuildHttpResponseHeaders() const {
+ std::string header_str("HTTP/1.1 ");
+ header_str.append(base::NumberToString(response_code()));
+ header_str.append(" ");
+ header_str.append(net::GetHttpReasonPhrase(response_code()));
+ header_str.append(" \r\n");
+ for (const auto& it : response_headers()) {
+ header_str.append(it.first);
+ header_str.append(": ");
+ header_str.append(it.second);
+ header_str.append("\r\n");
+ }
+ header_str.append("\r\n");
+ return base::MakeRefCounted<net::HttpResponseHeaders>(
+ net::HttpUtil::AssembleRawHeaders(header_str.c_str(), header_str.size()));
+}
+
+} // namespace content