blob: 774976674343e34189f7fb49968086a4ffa778eb [file] [log] [blame]
// 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/loader/merkle_integrity_source_stream.h"
#include <string.h>
#include "base/base64.h"
#include "base/big_endian.h"
#include "base/numerics/safe_conversions.h"
#include "net/base/io_buffer.h"
namespace content {
namespace {
// Limit the record size to 16KiB to prevent browser OOM. This matches the
// maximum record size in TLS and the default maximum frame size in HTTP/2.
constexpr uint64_t kMaxRecordSize = 16 * 1024;
constexpr char kMiSha256Header[] = "mi-sha256-03=";
constexpr size_t kMiSha256HeaderLength = sizeof(kMiSha256Header) - 1;
// Copies as many bytes from |input| as will fit in |output| and advances both.
size_t CopyClamped(base::span<const char>* input, base::span<char>* output) {
size_t size = std::min(output->size(), input->size());
memcpy(output->data(), input->data(), size);
*output = output->subspan(size);
*input = input->subspan(size);
return size;
}
} // namespace
MerkleIntegritySourceStream::MerkleIntegritySourceStream(
base::StringPiece digest_header_value,
std::unique_ptr<SourceStream> upstream)
// TODO(ksakamoto): Use appropriate SourceType.
: net::FilterSourceStream(SourceStream::TYPE_NONE, std::move(upstream)) {
std::string next_proof;
if (!digest_header_value.starts_with(kMiSha256Header) ||
!base::Base64Decode(digest_header_value.substr(kMiSha256HeaderLength),
&next_proof) ||
next_proof.size() != SHA256_DIGEST_LENGTH) {
failed_ = true;
} else {
memcpy(next_proof_, next_proof.data(), SHA256_DIGEST_LENGTH);
}
}
MerkleIntegritySourceStream::~MerkleIntegritySourceStream() = default;
int MerkleIntegritySourceStream::FilterData(net::IOBuffer* output_buffer,
int output_buffer_size,
net::IOBuffer* input_buffer,
int input_buffer_size,
int* consumed_bytes,
bool upstream_eof_reached) {
if (failed_) {
return net::ERR_CONTENT_DECODING_FAILED;
}
base::span<const char> remaining_input = base::make_span(
input_buffer->data(), base::checked_cast<size_t>(input_buffer_size));
base::span<char> remaining_output = base::make_span(
output_buffer->data(), base::checked_cast<size_t>(output_buffer_size));
bool ok =
FilterDataImpl(&remaining_output, &remaining_input, upstream_eof_reached);
*consumed_bytes =
input_buffer_size - base::checked_cast<int>(remaining_input.size());
if (!ok) {
failed_ = true;
return net::ERR_CONTENT_DECODING_FAILED;
}
return output_buffer_size - base::checked_cast<int>(remaining_output.size());
}
std::string MerkleIntegritySourceStream::GetTypeAsString() const {
return "MI-SHA256";
}
bool MerkleIntegritySourceStream::FilterDataImpl(base::span<char>* output,
base::span<const char>* input,
bool upstream_eof_reached) {
std::string storage;
// Process the record size in front, if we haven't yet.
if (record_size_ == 0) {
base::span<const char> bytes;
if (!ConsumeBytes(input, 8, &bytes, &storage)) {
if (!upstream_eof_reached) {
return true; // Wait for more data later.
}
if (partial_input_.empty()) {
// As a special case, the encoding of an empty payload is itself an
// empty message (i.e. it omits the initial record size), and its
// integrity proof is SHA-256("\0").
final_record_done_ = true;
return ProcessRecord({}, final_record_done_, output);
}
return false;
}
uint64_t record_size;
base::ReadBigEndian(bytes.data(), &record_size);
if (record_size == 0) {
return false;
}
if (record_size > kMaxRecordSize) {
DVLOG(1)
<< "Rejecting MI content encoding because record size is too big: "
<< record_size;
return false;
}
record_size_ = base::checked_cast<size_t>(record_size);
}
// Clear any previous output before continuing.
if (!CopyPartialOutput(output)) {
DCHECK(output->empty());
return true;
}
// Process records until we're done or there's no more room in |output|.
while (!output->empty() && !final_record_done_) {
base::span<const char> record;
if (!ConsumeBytes(input, record_size_ + SHA256_DIGEST_LENGTH, &record,
&storage)) {
DCHECK(input->empty());
if (!upstream_eof_reached) {
return true; // Wait for more data later.
}
// The final record is shorter and does not contain a hash. Process all
// remaining input as the final record.
if (partial_input_.empty() || partial_input_.size() > record_size_) {
return false;
}
record = partial_input_;
final_record_done_ = true;
}
if (!ProcessRecord(record, final_record_done_, output)) {
return false;
}
}
if (final_record_done_) {
DCHECK(upstream_eof_reached);
DCHECK(input->empty());
}
return true;
}
bool MerkleIntegritySourceStream::CopyPartialOutput(base::span<char>* output) {
if (partial_output_offset_ == partial_output_.size()) {
return true;
}
base::span<const char> partial =
base::make_span(partial_output_).subspan(partial_output_offset_);
partial_output_offset_ += CopyClamped(&partial, output);
if (partial_output_offset_ < partial_output_.size()) {
return false;
}
partial_output_.clear();
partial_output_offset_ = 0;
return true;
}
bool MerkleIntegritySourceStream::ConsumeBytes(base::span<const char>* input,
size_t len,
base::span<const char>* result,
std::string* storage) {
// This comes from the requirement that, when ConsumeBytes returns false, the
// next call must use the same |len|.
DCHECK_LT(partial_input_.size(), len);
// Return data directly from |input| if possible.
if (partial_input_.empty() && input->size() >= len) {
*result = input->subspan(0, len);
*input = input->subspan(len);
return true;
}
// Reassemble |len| bytes from |partial_input_| and |input|.
size_t to_copy = std::min(len - partial_input_.size(), input->size());
partial_input_.append(input->data(), to_copy);
*input = input->subspan(to_copy);
if (partial_input_.size() < len) {
return false;
}
*storage = std::move(partial_input_);
partial_input_.clear();
*result = *storage;
return true;
}
bool MerkleIntegritySourceStream::ProcessRecord(base::span<const char> record,
bool is_final,
base::span<char>* output) {
DCHECK(partial_output_.empty());
// Check the hash.
SHA256_CTX ctx;
SHA256_Init(&ctx);
SHA256_Update(&ctx, reinterpret_cast<const uint8_t*>(record.data()),
record.size());
uint8_t type = is_final ? 0 : 1;
SHA256_Update(&ctx, &type, 1);
uint8_t sha256[SHA256_DIGEST_LENGTH];
SHA256_Final(sha256, &ctx);
#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
// The fuzzer will have a hard time fixing up chains of hashes, so, if
// building in fuzzer mode, everything hashes to the same garbage value.
memset(sha256, 0x42, SHA256_DIGEST_LENGTH);
#endif
if (memcmp(sha256, next_proof_, SHA256_DIGEST_LENGTH) != 0) {
return false;
}
if (!is_final) {
// Split into data and a hash.
base::span<const char> hash = record.subspan(record_size_);
record = record.subspan(0, record_size_);
// Save the next proof.
CHECK_EQ(static_cast<size_t>(SHA256_DIGEST_LENGTH), hash.size());
memcpy(next_proof_, hash.data(), SHA256_DIGEST_LENGTH);
}
// Copy whatever output there is room for.
CopyClamped(&record, output);
// If it didn't all fit, save the remaining in |partial_output_|.
DCHECK(record.empty() || output->empty());
partial_output_.append(record.data(), record.size());
return true;
}
} // namespace content