| // Copyright 2018 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/loader/merkle_integrity_source_stream.h" |
| |
| #include <string.h> |
| |
| #include <algorithm> |
| #include <string_view> |
| #include <tuple> |
| |
| #include "base/base64.h" |
| #include "base/compiler_specific.h" |
| #include "base/containers/span.h" |
| #include "base/numerics/byte_conversions.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "net/base/io_buffer.h" |
| #include "net/filter/source_stream_type.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()); |
| std::ranges::copy(input->first(size), output->data()); |
| *output = output->subspan(size); |
| *input = input->subspan(size); |
| return size; |
| } |
| |
| } // namespace |
| |
| MerkleIntegritySourceStream::MerkleIntegritySourceStream( |
| std::string_view digest_header_value, |
| std::unique_ptr<SourceStream> upstream) |
| // TODO(ksakamoto): Use appropriate SourceType. |
| : net::FilterSourceStream(net::SourceStreamType::kNone, |
| std::move(upstream)) { |
| std::string next_proof; |
| if (!base::StartsWith(digest_header_value, kMiSha256Header) || |
| !base::Base64Decode(digest_header_value.substr(kMiSha256HeaderLength), |
| &next_proof) || |
| next_proof.size() != SHA256_DIGEST_LENGTH) { |
| failed_ = true; |
| } else { |
| base::span(next_proof_).copy_from(base::as_byte_span(next_proof)); |
| } |
| } |
| |
| MerkleIntegritySourceStream::~MerkleIntegritySourceStream() = default; |
| |
| base::expected<size_t, net::Error> MerkleIntegritySourceStream::FilterData( |
| net::IOBuffer* output_buffer, |
| size_t output_buffer_size, |
| net::IOBuffer* input_buffer, |
| size_t input_buffer_size, |
| size_t* consumed_bytes, |
| bool upstream_eof_reached) { |
| if (failed_) { |
| return base::unexpected(net::ERR_CONTENT_DECODING_FAILED); |
| } |
| |
| base::span<const char> remaining_input = |
| base::as_chars(input_buffer->first(input_buffer_size)); |
| base::span<char> remaining_output = |
| base::as_writable_chars(output_buffer->first(output_buffer_size)); |
| |
| bool ok = |
| FilterDataImpl(&remaining_output, &remaining_input, upstream_eof_reached); |
| *consumed_bytes = input_buffer_size - remaining_input.size(); |
| if (!ok) { |
| failed_ = true; |
| return base::unexpected(net::ERR_CONTENT_DECODING_FAILED); |
| } |
| return output_buffer_size - 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::U64FromBigEndian(base::as_bytes(bytes).first<8u>()); |
| if (record_size == 0u) { |
| 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::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) { |
| std::tie(*result, *input) = input->split_at(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. |
| std::ranges::fill(sha256, 0x42); |
| #endif |
| if (base::span(sha256) != base::span(next_proof_)) { |
| return false; |
| } |
| |
| if (!is_final) { |
| // Split into data and a hash. |
| base::span<const char> hash = record.subspan(record_size_); |
| record = record.first(record_size_); |
| |
| // Save the next proof. |
| CHECK_EQ(static_cast<size_t>(SHA256_DIGEST_LENGTH), hash.size()); |
| base::span(next_proof_).copy_from(base::as_bytes(hash)); |
| } |
| |
| // 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 |