Commit 98abd377 authored by David Benjamin's avatar David Benjamin Committed by Commit Bot

Rework MerkleIntegritySourceStream.

This cuts down on the number of copies and fixes some other bits:

- Add a fuzzer.

- Document a place where the original implementation did not match the
  specification. (The final record's size is a little iffy. We probably
  want a small spec tweak.)

- Use a streaming SHA-256 implementation, rather than making a copy to
  stick the 0 or 1 in the hash.

- If there is no more room in the output (the consumer may be issuing
  smaller reads), stop processing input. There is no need to make a copy
  of the entire input. MerkleIntegeritySourceStream only needs to buffer
  at most one record. (Ideally we wouldn't even do and instead
  coordinate with the base class's read buffer, but that would require
  tweaking the FilteredSourceStream interface. This CL addresses the
  easy stuff.)

- Fix O(N^2) behavior if the caller issues tiny reads in the buffered
  output.

- If the record is entirely in the input buffer (common case), don't
  make a copy to extract it.

- If the output fits entirely in the output buffer (common case), don't
  make a copy to return it.

- Flesh out missing tests, based on code coverage tools and important
  security checks (notably truncation).

  (For others trying to repeat the coverage bits: this file was
  unfortunately placed in //content rather than //net, so I wasn't able
  to get the coverage tools to work without hacking it into
  net_unittests locally. It seems the X server dependency is
  problematic for tools/code_coverage?? Also content_unittests is huge.)

- s/MI-256/MI-SHA256/. There are other 256-bit hashes.

Bug: 814591
Change-Id: If927d3f49085a5bec31939846c9a55f8903da34a
Reviewed-on: https://chromium-review.googlesource.com/981798Reviewed-by: default avatarYutaka Hirano <yhirano@chromium.org>
Reviewed-by: default avatarMatt Menke <mmenke@chromium.org>
Reviewed-by: default avatarMax Moroz <mmoroz@chromium.org>
Commit-Queue: David Benjamin <davidben@chromium.org>
Cr-Commit-Position: refs/heads/master@{#547874}
parent b84d9222
......@@ -4,9 +4,11 @@
#include "content/browser/loader/merkle_integrity_source_stream.h"
#include <string.h>
#include "base/base64url.h"
#include "base/big_endian.h"
#include "crypto/sha2.h"
#include "base/numerics/safe_conversions.h"
#include "net/base/io_buffer.h"
namespace content {
......@@ -20,23 +22,32 @@ constexpr uint64_t kMaxRecordSize = 16 * 1024;
constexpr char kMiSha256Header[] = "mi-sha256=";
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 mi_header_value,
std::unique_ptr<SourceStream> upstream)
// TODO(ksakamoto): Use appropriate SourceType.
: net::FilterSourceStream(SourceStream::TYPE_NONE, std::move(upstream)),
record_size_(0),
failed_(false) {
: net::FilterSourceStream(SourceStream::TYPE_NONE, std::move(upstream)) {
// TODO(ksakamoto): Support quoted parameter value.
if (mi_header_value.size() < kMiSha256HeaderLength ||
mi_header_value.substr(0, kMiSha256HeaderLength) != kMiSha256Header ||
std::string next_proof;
if (!mi_header_value.starts_with(kMiSha256Header) ||
!base::Base64UrlDecode(mi_header_value.substr(kMiSha256HeaderLength),
base::Base64UrlDecodePolicy::DISALLOW_PADDING,
&next_proof_) ||
next_proof_.size() != crypto::kSHA256Length) {
&next_proof) ||
next_proof.size() != SHA256_DIGEST_LENGTH) {
failed_ = true;
} else {
memcpy(next_proof_, next_proof.data(), SHA256_DIGEST_LENGTH);
}
}
......@@ -48,76 +59,180 @@ int MerkleIntegritySourceStream::FilterData(net::IOBuffer* output_buffer,
int input_buffer_size,
int* consumed_bytes,
bool upstream_eof_reached) {
if (failed_)
if (failed_) {
return net::ERR_CONTENT_DECODING_FAILED;
}
// TODO(ksakamoto): Avoid unnecessary buffer copying.
input_.append(input_buffer->data(), input_buffer_size);
*consumed_bytes = input_buffer_size;
if (!ProcessInput(upstream_eof_reached)) {
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());
}
int bytes_out =
std::min(output_.size(), static_cast<size_t>(output_buffer_size));
output_.copy(output_buffer->data(), bytes_out);
output_.erase(0, bytes_out);
return bytes_out;
std::string MerkleIntegritySourceStream::GetTypeAsString() const {
return "MI-SHA256";
}
bool MerkleIntegritySourceStream::ProcessInput(bool upstream_eof_reached) {
// TODO(ksakamoto): Use shift iterator or StringPiece instead of substr/erase.
bool MerkleIntegritySourceStream::FilterDataImpl(base::span<char>* output,
base::span<const char>* input,
bool upstream_eof_reached) {
std::string storage;
// Read the record size (the first 8 octets of the stream).
if (!record_size_) {
if (input_.size() < 8)
// 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)) {
return !upstream_eof_reached;
base::ReadBigEndian(input_.data(), &record_size_);
input_.erase(0, 8);
if (record_size_ == 0)
}
uint64_t record_size;
base::ReadBigEndian(bytes.data(), &record_size);
if (record_size == 0) {
return false;
if (record_size_ > kMaxRecordSize) {
}
if (record_size > kMaxRecordSize) {
DVLOG(1)
<< "Rejecting MI content encoding because record size is too big: "
<< record_size_;
<< record_size;
return false;
}
record_size_ = base::checked_cast<size_t>(record_size);
}
// Process records other than the last.
while (input_.size() >= record_size_ + crypto::kSHA256Length) {
std::string chunk = input_.substr(0, record_size_ + crypto::kSHA256Length);
input_.erase(0, record_size_ + crypto::kSHA256Length);
chunk.push_back('\x01');
std::string hash = crypto::SHA256HashString(chunk);
if (next_proof_ != hash)
return false;
output_.append(chunk.substr(0, record_size_));
next_proof_ = chunk.substr(record_size_, crypto::kSHA256Length);
// Clear any previous output before continuing.
if (!CopyPartialOutput(output)) {
DCHECK(output->empty());
return true;
}
// Process the last record.
if (upstream_eof_reached && !next_proof_.empty()) {
if (input_.size() > record_size_)
// 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 the final record.
//
// TODO(davidben): This matches the previous implementation in that it
// allows empty final records, but this does not match the specification
// and means some inputs have two valid encodings. However, the
// specification's version cannot represent the empty string. Update this
// when https://github.com/martinthomson/http-mice/issues/3 is resolved.
if (partial_input_.size() > record_size_) {
return false;
}
record = partial_input_;
final_record_done_ = true;
}
if (!ProcessRecord(record, final_record_done_, output)) {
return false;
}
}
input_.push_back('\0');
std::string hash = crypto::SHA256HashString(input_);
if (next_proof_ != hash)
return false;
if (final_record_done_) {
DCHECK(upstream_eof_reached);
DCHECK(input->empty());
}
return true;
}
output_.append(input_.substr(0, input_.size() - 1));
input_.clear();
next_proof_.clear();
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;
}
std::string MerkleIntegritySourceStream::GetTypeAsString() const {
return "MI-256";
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
......@@ -8,10 +8,12 @@
#include <stdint.h>
#include <string>
#include "base/containers/span.h"
#include "base/macros.h"
#include "base/strings/string_piece.h"
#include "content/common/content_export.h"
#include "net/filter/filter_source_stream.h"
#include "third_party/boringssl/src/include/openssl/sha.h"
namespace content {
......@@ -36,14 +38,48 @@ class CONTENT_EXPORT MerkleIntegritySourceStream
std::string GetTypeAsString() const override;
private:
bool ProcessInput(bool upstream_eof_reached);
std::string input_;
std::string output_;
// SHA-256 hash for the next record, or empty if validation is completed.
std::string next_proof_;
uint64_t record_size_;
bool failed_;
// Processes as many bytes of |input| as are available or fit in
// |output|. Both |input| and |output| are advanced past any bytes consumed or
// written to, respectively. Returns true if all input processed, possibly
// none, was valid and false on fatal error.
bool FilterDataImpl(base::span<char>* output,
base::span<const char>* input,
bool upstream_eof_reached);
// Copies |partial_output_| to output, as much as fits and advances both
// buffers. Returns whether all output was copied.
bool CopyPartialOutput(base::span<char>* output);
// Consumes the next |len| bytes of data from |partial_input_| and |input|
// and, if available, points |result| to it and returns true. |result| will
// point into either |input| or data copied to |storage|. |input| is advanced
// past any consumed bytes. If |len| bytes are not available, returns false
// and fully consumes |input| |partial_input_| for a future call.
bool ConsumeBytes(base::span<const char>* input,
size_t len,
base::span<const char>* result,
std::string* storage);
// Processes a record and returns whether it was valid. If valid, writes the
// contents into |output|, advancing past any bytes written. If |output| was
// not large enough, excess data will be copied into an internal buffer for a
// future call.
bool ProcessRecord(base::span<const char> record,
bool is_final,
base::span<char>* output);
// The partial input block, if the previous input buffer was too small.
std::string partial_input_;
// The partial output block, if the previous output buffer was too small.
std::string partial_output_;
// The index of |partial_output_| that has not been returned yet.
size_t partial_output_offset_ = 0;
// SHA-256 hash for the next record, if |final_record_done_| is false.
uint8_t next_proof_[SHA256_DIGEST_LENGTH];
size_t record_size_ = 0;
bool failed_ = false;
// Whether the final record has been processed.
bool final_record_done_ = false;
DISALLOW_COPY_AND_ASSIGN(MerkleIntegritySourceStream);
};
......
......@@ -22,6 +22,8 @@ const char kMISingleRecord[] =
"mi-sha256=dcRDgR2GM35DluAV13PzgnG6-pvQwPywfFvAu1UeFrs";
const char kMIMultipleRecords[] =
"mi-sha256=IVa9shfs0nyKEhHqtB3WVNANJ2Njm5KjQLjRtnbkYJ4";
const char kMIWholeNumberOfRecords[] =
"mi-sha256=L2vdwBplKvIr0ZPkcuskWZfEVDgVdHa6aD363UpKuZs";
enum class ReadResultType {
// Each call to AddReadResult is a separate read from the lower layer
......@@ -177,22 +179,57 @@ TEST_P(MerkleIntegritySourceStreamTest, MalformedMIHeader) {
EXPECT_EQ(net::ERR_CONTENT_DECODING_FAILED, result);
}
TEST_P(MerkleIntegritySourceStreamTest, WrongMIAttributeName) {
Init("mi-sha255=bjQLnP-zepicpUTmu3gKLHiQHT-zNzh2hRGjBhevoB0");
source()->AddReadResult(nullptr, 0, net::OK, GetParam().mode);
std::string actual_output;
int result = ReadStream(&actual_output);
EXPECT_EQ(net::ERR_CONTENT_DECODING_FAILED, result);
}
TEST_P(MerkleIntegritySourceStreamTest, HashTooShort) {
Init("mi-sha256=bjQLnP-zepicpUTmu3gKLHiQHT-zNzh2hRGjBhevoA");
source()->AddReadResult(nullptr, 0, net::OK, GetParam().mode);
std::string actual_output;
int result = ReadStream(&actual_output);
EXPECT_EQ(net::ERR_CONTENT_DECODING_FAILED, result);
}
TEST_P(MerkleIntegritySourceStreamTest, HashTooLong) {
Init("mi-sha256=bjQLnP-zepicpUTmu3gKLHiQHT-zNzh2hRGjBhevoB0A");
source()->AddReadResult(nullptr, 0, net::OK, GetParam().mode);
std::string actual_output;
int result = ReadStream(&actual_output);
EXPECT_EQ(net::ERR_CONTENT_DECODING_FAILED, result);
}
TEST_P(MerkleIntegritySourceStreamTest, RecordSizeOnly) {
Init(kMIEmptyBody);
const uint8_t record_size[] = {0, 0, 0, 0, 0, 0, 0, 10};
source()->AddReadResult(reinterpret_cast<const char*>(record_size),
sizeof(record_size), net::OK, GetParam().mode);
const uint8_t kRecordSize[] = {0, 0, 0, 0, 0, 0, 0, 10};
source()->AddReadResult(reinterpret_cast<const char*>(kRecordSize),
sizeof(kRecordSize), net::OK, GetParam().mode);
source()->AddReadResult(nullptr, 0, net::OK, GetParam().mode);
std::string actual_output;
int result = ReadStream(&actual_output);
EXPECT_EQ(net::OK, result);
}
TEST_P(MerkleIntegritySourceStreamTest, TruncatedRecordSize) {
Init(kMIEmptyBody);
const uint8_t kRecordSize[] = {0, 0, 0, 0, 0, 0, 1};
source()->AddReadResult(reinterpret_cast<const char*>(kRecordSize),
sizeof(kRecordSize), net::OK, GetParam().mode);
source()->AddReadResult(nullptr, 0, net::OK, GetParam().mode);
std::string actual_output;
int result = ReadStream(&actual_output);
EXPECT_EQ(net::ERR_CONTENT_DECODING_FAILED, result);
}
TEST_P(MerkleIntegritySourceStreamTest, RecordSizeOnlyWrongHash) {
Init(kMISingleRecord);
const uint8_t record_size[] = {0, 0, 0, 0, 0, 0, 0, 10};
source()->AddReadResult(reinterpret_cast<const char*>(record_size),
sizeof(record_size), net::OK, GetParam().mode);
const uint8_t kRecordSize[] = {0, 0, 0, 0, 0, 0, 0, 10};
source()->AddReadResult(reinterpret_cast<const char*>(kRecordSize),
sizeof(kRecordSize), net::OK, GetParam().mode);
source()->AddReadResult(nullptr, 0, net::OK, GetParam().mode);
std::string actual_output;
int result = ReadStream(&actual_output);
......@@ -202,10 +239,10 @@ TEST_P(MerkleIntegritySourceStreamTest, RecordSizeOnlyWrongHash) {
TEST_P(MerkleIntegritySourceStreamTest, RecordSizeHuge) {
Init(kMIEmptyBody);
// 2^64 - 1 is far too large.
const uint8_t record_size[] = {0xff, 0xff, 0xff, 0xff,
const uint8_t kRecordSize[] = {0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff};
source()->AddReadResult(reinterpret_cast<const char*>(record_size),
sizeof(record_size), net::OK, GetParam().mode);
source()->AddReadResult(reinterpret_cast<const char*>(kRecordSize),
sizeof(kRecordSize), net::OK, GetParam().mode);
std::string actual_output;
int result = ReadStream(&actual_output);
EXPECT_EQ(net::ERR_CONTENT_DECODING_FAILED, result);
......@@ -214,10 +251,22 @@ TEST_P(MerkleIntegritySourceStreamTest, RecordSizeHuge) {
TEST_P(MerkleIntegritySourceStreamTest, RecordSizeTooBig) {
Init(kMIEmptyBody);
// 2^16 + 1 just exceeds the limit.
const uint8_t record_size[] = {0x00, 0x00, 0x00, 0x00,
const uint8_t kRecordSize[] = {0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x40, 0x01};
source()->AddReadResult(reinterpret_cast<const char*>(record_size),
sizeof(record_size), net::OK, GetParam().mode);
source()->AddReadResult(reinterpret_cast<const char*>(kRecordSize),
sizeof(kRecordSize), net::OK, GetParam().mode);
std::string actual_output;
int result = ReadStream(&actual_output);
EXPECT_EQ(net::ERR_CONTENT_DECODING_FAILED, result);
}
TEST_P(MerkleIntegritySourceStreamTest, RecordSizeZero) {
Init(kMIEmptyBody);
// Zero is not a valid record size.
const uint8_t kRecordSize[] = {0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00};
source()->AddReadResult(reinterpret_cast<const char*>(kRecordSize),
sizeof(kRecordSize), net::OK, GetParam().mode);
std::string actual_output;
int result = ReadStream(&actual_output);
EXPECT_EQ(net::ERR_CONTENT_DECODING_FAILED, result);
......@@ -226,26 +275,26 @@ TEST_P(MerkleIntegritySourceStreamTest, RecordSizeTooBig) {
// https://tools.ietf.org/html/draft-thomson-http-mice-02#section-4.1
TEST_P(MerkleIntegritySourceStreamTest, SingleRecord) {
Init(kMISingleRecord);
const uint8_t record_size[] = {0, 0, 0, 0, 0, 0, 0, 0x29};
const std::string message("When I grow up, I want to be a watermelon");
source()->AddReadResult(reinterpret_cast<const char*>(record_size),
sizeof(record_size), net::OK, GetParam().mode);
source()->AddReadResult(message.data(), message.size(), net::OK,
const uint8_t kRecordSize[] = {0, 0, 0, 0, 0, 0, 0, 41};
const std::string kMessage("When I grow up, I want to be a watermelon");
source()->AddReadResult(reinterpret_cast<const char*>(kRecordSize),
sizeof(kRecordSize), net::OK, GetParam().mode);
source()->AddReadResult(kMessage.data(), kMessage.size(), net::OK,
GetParam().mode);
source()->AddReadResult(nullptr, 0, net::OK, GetParam().mode);
std::string actual_output;
int rv = ReadStream(&actual_output);
EXPECT_EQ(static_cast<int>(message.size()), rv);
EXPECT_EQ(message, actual_output);
EXPECT_EQ(static_cast<int>(kMessage.size()), rv);
EXPECT_EQ(kMessage, actual_output);
}
TEST_P(MerkleIntegritySourceStreamTest, SingleRecordWrongHash) {
Init(kMIEmptyBody);
const uint8_t record_size[] = {0, 0, 0, 0, 0, 0, 0, 0x29};
const std::string message("When I grow up, I want to be a watermelon");
source()->AddReadResult(reinterpret_cast<const char*>(record_size),
sizeof(record_size), net::OK, GetParam().mode);
source()->AddReadResult(message.data(), message.size(), net::OK,
const uint8_t kRecordSize[] = {0, 0, 0, 0, 0, 0, 0, 41};
const std::string kMessage("When I grow up, I want to be a watermelon");
source()->AddReadResult(reinterpret_cast<const char*>(kRecordSize),
sizeof(kRecordSize), net::OK, GetParam().mode);
source()->AddReadResult(kMessage.data(), kMessage.size(), net::OK,
GetParam().mode);
source()->AddReadResult(nullptr, 0, net::OK, GetParam().mode);
std::string actual_output;
......@@ -254,55 +303,244 @@ TEST_P(MerkleIntegritySourceStreamTest, SingleRecordWrongHash) {
EXPECT_EQ(0u, actual_output.size());
}
// The final record may not be larger than the record size.
TEST_P(MerkleIntegritySourceStreamTest, SingleRecordTooLarge) {
Init(kMISingleRecord);
const uint8_t kRecordSize[] = {0, 0, 0, 0, 0, 0, 0, 40};
const std::string kMessage("When I grow up, I want to be a watermelon");
source()->AddReadResult(reinterpret_cast<const char*>(kRecordSize),
sizeof(kRecordSize), net::OK, GetParam().mode);
source()->AddReadResult(kMessage.data(), kMessage.size(), net::OK,
GetParam().mode);
source()->AddReadResult(nullptr, 0, net::OK, GetParam().mode);
std::string actual_output;
int rv = ReadStream(&actual_output);
EXPECT_EQ(net::ERR_CONTENT_DECODING_FAILED, rv);
EXPECT_EQ("", actual_output);
}
// https://tools.ietf.org/html/draft-thomson-http-mice-02#section-4.2
TEST_P(MerkleIntegritySourceStreamTest, MultipleRecords) {
Init(kMIMultipleRecords);
const uint8_t record_size[] = {0, 0, 0, 0, 0, 0, 0, 16};
const std::string message("When I grow up, I want to be a watermelon");
const uint8_t kRecordSize[] = {0, 0, 0, 0, 0, 0, 0, 16};
const std::string kMessage("When I grow up, I want to be a watermelon");
source()->AddReadResult(reinterpret_cast<const char*>(record_size),
sizeof(record_size), net::OK, GetParam().mode);
source()->AddReadResult(message.data(), 16, net::OK, GetParam().mode);
source()->AddReadResult(reinterpret_cast<const char*>(kRecordSize),
sizeof(kRecordSize), net::OK, GetParam().mode);
source()->AddReadResult(kMessage.data(), 16, net::OK, GetParam().mode);
std::string hash1 =
Base64Decode("OElbplJlPK-Rv6JNK6p5_515IaoPoZo-2elWL7OQ60A");
source()->AddReadResult(hash1.data(), 32, net::OK, GetParam().mode);
source()->AddReadResult(message.data() + 16, 16, net::OK, GetParam().mode);
source()->AddReadResult(kMessage.data() + 16, 16, net::OK, GetParam().mode);
std::string hash2 =
Base64Decode("iPMpmgExHPrbEX3_RvwP4d16fWlK4l--p75PUu_KyN0");
source()->AddReadResult(hash2.data(), 32, net::OK, GetParam().mode);
source()->AddReadResult(message.data() + 32, message.size() - 32, net::OK,
source()->AddReadResult(kMessage.data() + 32, kMessage.size() - 32, net::OK,
GetParam().mode);
source()->AddReadResult(nullptr, 0, net::OK, GetParam().mode);
std::string actual_output;
int rv = ReadStream(&actual_output);
EXPECT_EQ(static_cast<int>(message.size()), rv);
EXPECT_EQ(message, actual_output);
EXPECT_EQ(static_cast<int>(kMessage.size()), rv);
EXPECT_EQ(kMessage, actual_output);
}
TEST_P(MerkleIntegritySourceStreamTest, MultipleRecordsAllAtOnce) {
Init(kMIMultipleRecords);
const uint8_t kRecordSize[] = {0, 0, 0, 0, 0, 0, 0, 16};
const std::string kMessage("When I grow up, I want to be a watermelon");
std::string body(reinterpret_cast<const char*>(kRecordSize),
sizeof(kRecordSize));
body += kMessage.substr(0, 16);
body += Base64Decode("OElbplJlPK-Rv6JNK6p5_515IaoPoZo-2elWL7OQ60A");
body += kMessage.substr(16, 16);
body += Base64Decode("iPMpmgExHPrbEX3_RvwP4d16fWlK4l--p75PUu_KyN0");
body += kMessage.substr(32);
source()->AddReadResult(body.data(), body.size(), net::OK, GetParam().mode);
source()->AddReadResult(nullptr, 0, net::OK, GetParam().mode);
std::string actual_output;
int rv = ReadStream(&actual_output);
EXPECT_EQ(static_cast<int>(kMessage.size()), rv);
EXPECT_EQ(kMessage, actual_output);
}
TEST_P(MerkleIntegritySourceStreamTest, MultipleRecordsWrongLastRecordHash) {
Init(kMIMultipleRecords);
const uint8_t record_size[] = {0, 0, 0, 0, 0, 0, 0, 16};
const std::string message("When I grow up, I want to be a watermelon!");
const uint8_t kRecordSize[] = {0, 0, 0, 0, 0, 0, 0, 16};
const std::string kMessage("When I grow up, I want to be a watermelon!");
source()->AddReadResult(reinterpret_cast<const char*>(record_size),
sizeof(record_size), net::OK, GetParam().mode);
source()->AddReadResult(message.data(), 16, net::OK, GetParam().mode);
source()->AddReadResult(reinterpret_cast<const char*>(kRecordSize),
sizeof(kRecordSize), net::OK, GetParam().mode);
source()->AddReadResult(kMessage.data(), 16, net::OK, GetParam().mode);
std::string hash1 =
Base64Decode("OElbplJlPK-Rv6JNK6p5_515IaoPoZo-2elWL7OQ60A");
source()->AddReadResult(hash1.data(), 32, net::OK, GetParam().mode);
source()->AddReadResult(message.data() + 16, 16, net::OK, GetParam().mode);
source()->AddReadResult(kMessage.data() + 16, 16, net::OK, GetParam().mode);
std::string hash2 =
Base64Decode("iPMpmgExHPrbEX3_RvwP4d16fWlK4l--p75PUu_KyN0");
source()->AddReadResult(hash2.data(), 32, net::OK, GetParam().mode);
source()->AddReadResult(message.data() + 32, message.size() - 32, net::OK,
source()->AddReadResult(kMessage.data() + 32, kMessage.size() - 32, net::OK,
GetParam().mode);
source()->AddReadResult(nullptr, 0, net::OK, GetParam().mode);
std::string actual_output;
int rv = ReadStream(&actual_output);
EXPECT_EQ(net::ERR_CONTENT_DECODING_FAILED, rv);
EXPECT_EQ(message.substr(0, 32), actual_output);
EXPECT_EQ(kMessage.substr(0, 32), actual_output);
}
TEST_P(MerkleIntegritySourceStreamTest, MultipleRecordsWrongFirstRecordHash) {
Init(kMIEmptyBody);
const uint8_t kRecordSize[] = {0, 0, 0, 0, 0, 0, 0, 16};
const std::string kMessage("When I grow up, I want to be a watermelon!");
source()->AddReadResult(reinterpret_cast<const char*>(kRecordSize),
sizeof(kRecordSize), net::OK, GetParam().mode);
source()->AddReadResult(kMessage.data(), 16, net::OK, GetParam().mode);
std::string hash1 =
Base64Decode("OElbplJlPK-Rv6JNK6p5_515IaoPoZo-2elWL7OQ60A");
source()->AddReadResult(hash1.data(), 32, net::OK, GetParam().mode);
std::string actual_output;
int rv = ReadStream(&actual_output);
EXPECT_EQ(net::ERR_CONTENT_DECODING_FAILED, rv);
EXPECT_EQ("", actual_output);
}
TEST_P(MerkleIntegritySourceStreamTest, TrailingNetError) {
Init(kMIMultipleRecords);
const uint8_t kRecordSize[] = {0, 0, 0, 0, 0, 0, 0, 16};
const std::string kMessage("When I grow up, I want to be a watermelon");
source()->AddReadResult(reinterpret_cast<const char*>(kRecordSize),
sizeof(kRecordSize), net::OK, GetParam().mode);
source()->AddReadResult(kMessage.data(), 16, net::OK, GetParam().mode);
std::string hash1 =
Base64Decode("OElbplJlPK-Rv6JNK6p5_515IaoPoZo-2elWL7OQ60A");
source()->AddReadResult(hash1.data(), 32, net::OK, GetParam().mode);
source()->AddReadResult(kMessage.data() + 16, 16, net::OK, GetParam().mode);
std::string hash2 =
Base64Decode("iPMpmgExHPrbEX3_RvwP4d16fWlK4l--p75PUu_KyN0");
source()->AddReadResult(hash2.data(), 32, net::OK, GetParam().mode);
source()->AddReadResult(kMessage.data() + 32, kMessage.size() - 32, net::OK,
GetParam().mode);
source()->AddReadResult(nullptr, 0, net::ERR_FAILED, GetParam().mode);
std::string actual_output;
int rv = ReadStream(&actual_output);
EXPECT_EQ(net::ERR_FAILED, rv);
// MerkleIntegritySourceStream cannot read the last record without a clean EOF
// to denote its end.
EXPECT_EQ(kMessage.substr(0, 32), actual_output);
}
// Test that truncations are noticed, by way of the final record not matching
// the hash.
TEST_P(MerkleIntegritySourceStreamTest, Truncated) {
Init(kMIMultipleRecords);
const uint8_t kRecordSize[] = {0, 0, 0, 0, 0, 0, 0, 16};
const std::string kMessage("When I grow up, I want to be a w");
source()->AddReadResult(reinterpret_cast<const char*>(kRecordSize),
sizeof(kRecordSize), net::OK, GetParam().mode);
source()->AddReadResult(kMessage.data(), 16, net::OK, GetParam().mode);
std::string hash1 =
Base64Decode("OElbplJlPK-Rv6JNK6p5_515IaoPoZo-2elWL7OQ60A");
source()->AddReadResult(hash1.data(), 32, net::OK, GetParam().mode);
source()->AddReadResult(kMessage.data() + 16, 16, net::OK, GetParam().mode);
std::string hash2 =
Base64Decode("iPMpmgExHPrbEX3_RvwP4d16fWlK4l--p75PUu_KyN0");
source()->AddReadResult(hash2.data(), 32, net::OK, GetParam().mode);
// |hash2| is the hash of "atermelon", but this stream ends early. Decoding
// thus should fail.
source()->AddReadResult(nullptr, 0, net::OK, GetParam().mode);
std::string actual_output;
int rv = ReadStream(&actual_output);
EXPECT_EQ(net::ERR_CONTENT_DECODING_FAILED, rv);
EXPECT_EQ(kMessage, actual_output);
}
// Test that the final record is allowed to be empty.
//
// TODO(davidben): This does not match the specification and means some inputs
// have two valid encodings. However, the specification's version cannot
// represent the empty string. Update the code and possibly this test depending
// on how https://github.com/martinthomson/http-mice/issues/3 is resolved.
TEST_P(MerkleIntegritySourceStreamTest, EmptyFinalRecord) {
Init("mi-sha256=JJnIuaOEc2247K9V88VQAQy1GJuQ6ylaVM7mG69QkE4");
const uint8_t kRecordSize[] = {0, 0, 0, 0, 0, 0, 0, 16};
const std::string kMessage(
"When I grow up, I want to be a watermelon!! \xf0\x9f\x8d\x89");
source()->AddReadResult(reinterpret_cast<const char*>(kRecordSize),
sizeof(kRecordSize), net::OK, GetParam().mode);
source()->AddReadResult(kMessage.data(), 16, net::OK, GetParam().mode);
std::string hash1 =
Base64Decode("hhJEKpkbuZoWUjzBPAZxMUN2DXdJ6epkS0McZh77IXo");
source()->AddReadResult(hash1.data(), 32, net::OK, GetParam().mode);
source()->AddReadResult(kMessage.data() + 16, 16, net::OK, GetParam().mode);
std::string hash2 =
Base64Decode("RKTTVSMiH3bkxUQKreVATPL1KUd5eqRdmDgRQcZq_80");
source()->AddReadResult(hash2.data(), 32, net::OK, GetParam().mode);
source()->AddReadResult(kMessage.data() + 32, 16, net::OK, GetParam().mode);
std::string hash3 =
Base64Decode("bjQLnP-zepicpUTmu3gKLHiQHT-zNzh2hRGjBhevoB0");
source()->AddReadResult(hash3.data(), 32, net::OK, GetParam().mode);
source()->AddReadResult(nullptr, 0, net::OK, GetParam().mode);
std::string actual_output;
int rv = ReadStream(&actual_output);
EXPECT_EQ(static_cast<int>(kMessage.size()), rv);
EXPECT_EQ(kMessage, actual_output);
}
TEST_P(MerkleIntegritySourceStreamTest, WholeNumberOfRecords) {
Init(kMIWholeNumberOfRecords);
const uint8_t kRecordSize[] = {0, 0, 0, 0, 0, 0, 0, 16};
const std::string kMessage(
"When I grow up, I want to be a watermelon!! \xf0\x9f\x8d\x89");
source()->AddReadResult(reinterpret_cast<const char*>(kRecordSize),
sizeof(kRecordSize), net::OK, GetParam().mode);
source()->AddReadResult(kMessage.data(), 16, net::OK, GetParam().mode);
std::string hash1 =
Base64Decode("2s-MNG6NrTt556s__HYnQTjG3WOktEcXZ61O8mzG9f4");
source()->AddReadResult(hash1.data(), 32, net::OK, GetParam().mode);
source()->AddReadResult(kMessage.data() + 16, 16, net::OK, GetParam().mode);
std::string hash2 =
Base64Decode("qa_cQSMjFyZsm0cnYG4H6LqwOM_hzMSclK6I8iVoZYQ");
source()->AddReadResult(hash2.data(), 32, net::OK, GetParam().mode);
source()->AddReadResult(kMessage.data() + 32, 16, net::OK, GetParam().mode);
source()->AddReadResult(nullptr, 0, net::OK, GetParam().mode);
std::string actual_output;
int rv = ReadStream(&actual_output);
EXPECT_EQ(static_cast<int>(kMessage.size()), rv);
EXPECT_EQ(kMessage, actual_output);
}
TEST_P(MerkleIntegritySourceStreamTest, WholeNumberOfRecordsAllAtOnce) {
Init(kMIWholeNumberOfRecords);
const uint8_t kRecordSize[] = {0, 0, 0, 0, 0, 0, 0, 16};
const std::string kMessage(
"When I grow up, I want to be a watermelon!! \xf0\x9f\x8d\x89");
std::string body(reinterpret_cast<const char*>(kRecordSize),
sizeof(kRecordSize));
body += kMessage.substr(0, 16);
body += Base64Decode("2s-MNG6NrTt556s__HYnQTjG3WOktEcXZ61O8mzG9f4");
body += kMessage.substr(16, 16);
body += Base64Decode("qa_cQSMjFyZsm0cnYG4H6LqwOM_hzMSclK6I8iVoZYQ");
body += kMessage.substr(32, 16);
source()->AddReadResult(body.data(), body.size(), net::OK, GetParam().mode);
source()->AddReadResult(nullptr, 0, net::OK, GetParam().mode);
std::string actual_output;
int rv = ReadStream(&actual_output);
EXPECT_EQ(static_cast<int>(kMessage.size()), rv);
EXPECT_EQ(kMessage, actual_output);
}
} // namespace content
# 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.
# The fuzzer build fills all hashes with 0x42.
"\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42"
# The header version of the hash.
"mi-sha256=QkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkI\\ "
# base::FuzzedDataProvider separator.
"\\ "
# Use a 16-byte record size if you don't have better ideas.
"\x00\x00\x00\x00\x00\x00\x00\x10"
......@@ -144,3 +144,24 @@ fuzzer_test("appcache_manifest_parser_fuzzer") {
seed_corpus =
"//third_party/WebKit/LayoutTests/http/tests/appcache/resources/"
}
fuzzer_test("merkle_integrity_source_stream_fuzzer") {
sources = [
"merkle_integrity_source_stream_fuzzer.cc",
]
# This fuzzer depends on net::FuzzedSourceStream, in net_fuzzer_test_support,
# but both it and //content:test_support set up similar globals. As
# MerkleIntegritySourceStream does not depend on anything in //content and
# will ultimately live in //net, use the //net one instead of the //content
# one.
deps = [
"//content/browser:for_content_tests",
"//content/renderer:for_content_tests",
"//content/shell:content_shell_lib",
"//content/test:test_support",
"//net:net_fuzzer_test_support",
"//net:test_support",
]
dict = "//content/test/data/fuzzer_dictionaries/merkle_integrity_source_stream_fuzzer.dict"
}
// 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" // nogncheck
#include <string>
#include "base/logging.h"
#include "base/memory/scoped_refptr.h"
#include "base/test/fuzzed_data_provider.h"
#include "net/base/io_buffer.h"
#include "net/base/test_completion_callback.h"
#include "net/filter/fuzzed_source_stream.h"
// Fuzzer for MerkleIntegritySourceStream
//
// |data| contains a header prefix, and then is used to build a
// FuzzedSourceStream.
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
base::FuzzedDataProvider data_provider(data, size);
std::string header = data_provider.ConsumeRandomLengthString(256);
net::TestCompletionCallback callback;
auto fuzzed_source_stream =
std::make_unique<net::FuzzedSourceStream>(&data_provider);
auto mi_stream = std::make_unique<content::MerkleIntegritySourceStream>(
header, std::move(fuzzed_source_stream));
while (true) {
size_t read_size = data_provider.ConsumeUint32InRange(1, 1024);
auto io_buffer = base::MakeRefCounted<net::IOBufferWithSize>(read_size);
int result = mi_stream->Read(io_buffer.get(), io_buffer->size(),
callback.callback());
// Releasing the pointer to IOBuffer immediately is more likely to lead to a
// use-after-free.
io_buffer = nullptr;
if (callback.GetResult(result) <= 0)
break;
}
return 0;
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment