Commit af99d385 authored by Ivan Vilata-i-Balaguer's avatar Ivan Vilata-i-Balaguer
Browse files

Merge branch 'multi-peer-download' into master.

This enables clients to increase retrieval speed by concurrently downloading
the same data shared by several clients for the same URL, or common data from
the beginning of unfinished downloads of the URL (like canceled transfers or
streamed videos).  A reference client is chosen which has signatures for the
newest and most complete data for the URL.

This introduces the new v5 protocol (with a different `ouisig` chunk
extension) and v2 HTTP store (with a different `sigs` file format).
parents 10b82392 6f4e040f
......@@ -56,7 +56,7 @@ Please note that neither the initial signature nor framing headers (`Transfer-En
```
HTTP/1.1 200 OK
X-Ouinet-Version: 4
X-Ouinet-Version: 5
X-Ouinet-URI: https://example.com/foo
X-Ouinet-Injection: id=d6076384-2295-462b-a047-fe2c9274e58d,ts=1516048310
Date: Mon, 15 Jan 2018 20:31:50 GMT
......@@ -72,11 +72,11 @@ Trailer: Digest, X-Ouinet-Data-Size, X-Ouinet-Sig1
100000
0123456789...
100000;ouisig=BASE64(BSIG(d607…e58d NUL 0 NUL HASH[0]=SHA2-512(BLOCK[0])))
100000;ouisig=BASE64(BSIG(d607…e58d NUL 0 NUL CHASH[0]=SHA2-512(SHA2-512(BLOCK[0]))))
0123456789...
4;ouisig=BASE64(BSIG(d607…e58d NUL 1048576 NUL HASH[1]=SHA2-512(HASH[0] BLOCK[1])))
4;ouisig=BASE64(BSIG(d607…e58d NUL 1048576 NUL CHASH[1]=SHA2-512(CHASH[0] SHA2-512(BLOCK[1]))))
abcd
0;ouisig=BASE64(BSIG(d607…e58d NUL 2097152 NUL HASH[2]=SHA2-512(HASH[1] BLOCK[2])))
0;ouisig=BASE64(BSIG(d607…e58d NUL 2097152 NUL CHASH[2]=SHA2-512(CHASH[1] SHA2-512(BLOCK[2]))))
Digest: SHA-256=BASE64(SHA2-256(COMPLETE_BODY))
X-Ouinet-Data-Size: 1048580
X-Ouinet-Sig1: keyId="ed25519=????",algorithm="hs2019",created=1516048311,
......@@ -96,10 +96,12 @@ The signature string for each block covers the following values (separated by nu
This helps detecting an attacker which replies to a range request with a range of the expected length, with correctly signed and ordered blocks, that however starts at the wrong offset.
- A **chain hash** (binary) computed from the chain hash of the previous block and the data of the block itself: for the i-th block, `HASH[i]=SHA2-512(HASH[i-1] BLOCK[i])`, with `HASH[0]=SHA2-512(BLOCK[0])`.
- A **chain hash** (binary) computed from the chain hash of the previous block and the **data hash** of the block itself: for the i-th block, `DHASH[i]=SHA2-512(BLOCK[i])` and `CHASH[i]=SHA2-512(CHASH[i-1] DHASH[i])`, with `CHASH[0]=SHA2-512(DHASH[0])`.
Signing the hash instead of block data itself spares the signer from keeping the whole block in memory for producing the signature (the hash algorithm can be fed as data comes in from the origin).
Using the data block hash instead of its data allows to independently verify the signatures without needing to be in possession of the data itself, just the hashes.
Keeping the injection identifier out of the hash allows to compare the hashes at particular blocks of different injections (if transmitted independently) to ascertain that their data is the same up to that block.
The chaining precludes the attacker from reordering correctly signed blocks for this injection. SHA2-512 is used as a compromise between security and speed on 64-bit platforms; although the hash is longer than the slower SHA2-256, it will be seldom transmitted (e.g. for range requests as indicated below).
......@@ -112,7 +114,7 @@ If a client got to get and save a complete response from the injector, it may se
## Range requests
If a client sends an HTTP range request to another client, the later aligns it to block boundaries (this is acceptable according to [RFC7233#4.1][] — "a client cannot rely on receiving the same ranges that it requested"). The `Content-Range:` header in the response is not part of the initial nor final signatures. If the range does not start at the beginning of the data, the first block `i` is accompanied with a `ouihash=BASE64(HASH[i-1])` chunk extension to enable checking its `ouisig`. Please note that to ease serving range requests, a client storing a response may cache all chain hashes along their blocks, so as to avoid having to compute the `ouihash` of the first block in the range.
If a client sends an HTTP range request to another client, the later aligns it to block boundaries (this is acceptable according to [RFC7233#4.1][] — "a client cannot rely on receiving the same ranges that it requested"). The `Content-Range:` header in the response is not part of the initial nor final signatures. If the range does not start at the beginning of the data, the first block `i` is accompanied with a `ouihash=BASE64(CHASH[i-1])` chunk extension to enable checking its `ouisig`. Please note that to ease serving range requests, a client storing a response may cache all chain hashes along their blocks, so as to avoid having to compute the `ouihash` of the first block in the range.
[RFC7233#4.1]: https://tools.ietf.org/html/rfc7233#section-4.1
......
......@@ -696,8 +696,10 @@ Client::build( shared_ptr<bt::MainlineDht> dht
sys::error_code ec;
auto old_store_dir = cache_dir / "data"; // v0 store
if (is_directory(old_store_dir)) {
// Remove obsolete stores.
for (const auto& dirn : {"data", "data-v1"}) {
auto old_store_dir = cache_dir / dirn;
if (!is_directory(old_store_dir)) continue;
LOG_INFO("Removing obsolete HTTP store...");
fs::remove_all(old_store_dir, ec);
if (ec) LOG_ERROR("Removing obsolete HTTP store: failed; ec:", ec.message());
......@@ -705,7 +707,7 @@ Client::build( shared_ptr<bt::MainlineDht> dht
ec = {};
}
auto store_dir = cache_dir / "data-v1";
auto store_dir = cache_dir / "data-v2";
fs::create_directories(store_dir, ec);
if (ec) return or_throw<ClientPtr>(yield, ec);
auto http_store = make_unique<cache::HttpStore>(
......
#include "hash_list.h"
#include "http_sign.h"
using namespace ouinet;
using namespace ouinet::cache;
using boost::optional;
bool HashList::verify() const {
using Digest = util::SHA512::digest_type;
optional<Digest> last_digest;
size_t block_size = signed_head.block_size();
size_t last_offset = 0;
bool first = true;
for (auto& digest : block_hashes) {
util::SHA512 sha;
if (last_digest) {
sha.update(*last_digest);
}
sha.update(digest);
last_digest = sha.close();
if (first) {
first = false;
} else {
last_offset += block_size;
}
}
if (!last_digest) return false;
return cache::Block::verify( signed_head.injection_id()
, last_offset
, *last_digest
, signature
, signed_head.public_key());
}
#pragma once
#include "../util/hash.h"
#include "../util/crypto.h"
#include "../response_part.h"
#include "signed_head.h"
namespace ouinet { namespace cache {
struct HashList {
using Digest = util::SHA512::digest_type;
using PubKey = util::Ed25519PublicKey;
SignedHead signed_head;
std::vector<Digest> block_hashes;
PubKey::sig_array_t signature;
bool verify() const;
};
}}
This diff is collapsed.
......@@ -58,35 +58,6 @@ namespace ouinet { namespace http_ {
namespace ouinet { namespace cache {
// Ouinet-specific declarations for injection using HTTP signatures
// ----------------------------------------------------------------
// Get an extended version of the given response head
// with an additional signature header and
// other headers required to support that signature and
// a future one for the full message head (as part of the trailer).
//
// Example:
//
// ...
// X-Ouinet-Version: 2
// X-Ouinet-URI: https://example.com/foo
// X-Ouinet-Injection: id=d6076384-2295-462b-a047-fe2c9274e58d,ts=1516048310
// X-Ouinet-BSigs: keyId="...",algorithm="hs2019",size=65536
// X-Ouinet-Sig0: keyId="...",algorithm="hs2019",created=1516048310,
// headers="(response-status) (created) ... x-ouinet-injection",
// signature="..."
// Transfer-Encoding: chunked
// Trailer: X-Ouinet-Data-Size, Digest, X-Ouinet-Sig1
//
http::response_header<>
http_injection_head( const http::request_header<>& rqh
, http::response_header<> rsh
, const std::string& injection_id
, std::chrono::seconds::rep injection_ts
, const ouinet::util::Ed25519PrivateKey&
, const std::string& key_id);
// Get an extended version of the given response trailer
// with added headers completing the signature of the message.
//
......@@ -152,39 +123,10 @@ http::response_header<>
http_injection_merge( http::response_header<> rsh
, const http::fields& rst);
// Verify that the given response head contains
// good signatures for it from the given public key.
// Return a head which only contains headers covered by at least one such signature,
// plus good signatures themselves and signatures for unknown keys.
// Bad signatures are dropped to avoid propagating them along good signatures.
// Framing headers are preserved.
//
// If no good signatures exist, or any other error happens,
// return an empty head.
http::response_header<>
http_injection_verify( http::response_header<>
, const ouinet::util::Ed25519PublicKey&);
// Get a `keyId` encoding the given public key itself.
std::string
http_key_id_for_injection(const ouinet::util::Ed25519PublicKey&);
// Decode the given `keyId` into a public key.
boost::optional<util::Ed25519PublicKey>
http_decode_key_id(boost::string_view key_id);
// A simple container for a parsed block signatures HTTP header.
// Only the `hs2019` algorithm with an explicit key is supported,
// so the ready-to-use key is left in `pk`.
struct HttpBlockSigs {
util::Ed25519PublicKey pk;
boost::string_view algorithm; // always "hs2019"
size_t size;
static
boost::optional<HttpBlockSigs> parse(boost::string_view);
};
// Allows reading parts of a response from stream `in`
// while signing with the private key `sk`.
class SigningReader : public ouinet::http_response::Reader {
......@@ -318,6 +260,21 @@ http_digest(ouinet::util::SHA256&);
std::string
http_digest(const http::response<http::dynamic_body>&);
namespace Block {
using Signature = util::Ed25519PublicKey::sig_array_t;
using Digest = util::SHA512::digest_type;
Signature sign( boost::string_view injection_id
, size_t offset
, const Digest& chained_digest
, const util::Ed25519PrivateKey&);
bool verify( boost::string_view injection_id
, size_t offset
, const Digest& chained_digest
, const Signature& signature
, const util::Ed25519PublicKey&);
};
// Generic HTTP signatures
// -----------------------
......
......@@ -23,10 +23,10 @@
#include "../util/atomic_file.h"
#include "../util/bytes.h"
#include "../util/file_io.h"
#include "../util/hash.h"
#include "../util/str.h"
#include "../util/variant.h"
#include "http_sign.h"
#include "signed_head.h"
#define _LOGPFX "HTTP store: "
#define _DEBUG(...) LOG_DEBUG(_LOGPFX, __VA_ARGS__)
......@@ -115,18 +115,21 @@ parse_data_block_offset(const std::string& s) // `^[0-9a-f]*$`
return offset;
}
// A signatures file entry with `OFFSET[i] SIGNATURE[i] HASH[i-1]`.
// A signatures file entry with `OFFSET[i] SIGNATURE[i] CHASH[i-1]`.
struct SigEntry {
std::size_t offset;
std::string signature;
std::string data_digest;
std::string prev_digest;
using parse_buffer = std::string;
std::string str() const
{
static const auto line_format = "%x %s %s\n";
return (boost::format(line_format) % offset % signature % prev_digest).str();
static const auto pad_digest = util::base64_encode(util::SHA512::zero_digest());
static const auto line_format = "%016x %s %s %s\n";
return ( boost::format(line_format) % offset % signature % data_digest
% (prev_digest.empty() ? pad_digest : prev_digest)).str();
}
std::string chunk_exts() const
......@@ -144,14 +147,6 @@ struct SigEntry {
return exts.str();
}
static
parse_buffer create_parse_buffer()
{
// We may use a flat buffer or something like that,
// but this will suffice for the moment.
return {};
}
template<class Stream>
static
boost::optional<SigEntry>
......@@ -172,10 +167,12 @@ struct SigEntry {
boost::string_view line(buf);
line.remove_suffix(buf.size() - line_len + 1); // leave newline out
static const boost::regex line_regex(
"([0-9a-f]+)" // LOWER_HEX(OFFSET[i])
" ([A-Za-z0-9+/]+=*)" // BASE64(SIG[i])
" ([A-Za-z0-9+/]+=*)?" // BASE64(HASH([i-1]))
static const auto pad_digest = util::base64_encode(util::SHA512::zero_digest());
static const boost::regex line_regex( // Ensure lines are fixed size!
"([0-9a-f]{16})" // PAD016_LHEX(OFFSET[i])
" ([A-Za-z0-9+/=]{88})" // BASE64(SIG[i]) (88 = size(BASE64(Ed25519-SIG)))
" ([A-Za-z0-9+/=]{88})" // BASE64(DHASH[i]) (88 = size(BASE64(SHA2-512)))
" ([A-Za-z0-9+/=]{88})" // BASE64(CHASH([i-1])) (88 = size(BASE64(SHA2-512)))
);
boost::cmatch m;
if (!boost::regex_match(line.begin(), line.end(), m, line_regex)) {
......@@ -183,7 +180,8 @@ struct SigEntry {
return or_throw(yield, sys::errc::make_error_code(sys::errc::bad_message), boost::none);
}
auto offset = parse_data_block_offset(m[1].str());
SigEntry entry{offset, m[2].str(), m[3].str()};
SigEntry entry{ offset, m[2].str(), m[3].str()
, (m[4] == pad_digest ? "" : m[4].str())};
buf.erase(0, line_len); // consume used input
return entry;
}
......@@ -234,7 +232,7 @@ public:
_ERROR("Missing parameters for data block signatures; uri=", uri);
return or_throw(yield, asio::error::invalid_argument);
}
auto bs_params = cache::HttpBlockSigs::parse(bsh);
auto bs_params = cache::SignedHead::BlockSigs::parse(bsh);
if (!bs_params) {
_ERROR("Malformed parameters for data block signatures; uri=", uri);
return or_throw(yield, asio::error::invalid_argument);
......@@ -278,13 +276,22 @@ public:
return or_throw(yield, asio::error::invalid_argument);
}
auto block_digest = block_hash.close();
block_hash = {};
e.data_digest = util::base64_encode(block_digest);
// Encode the chained hash for the previous block.
if (prev_block_digest)
e.prev_digest = util::base64_encode(*prev_block_digest);
// Prepare hash for next data block: HASH[i]=SHA2-512(HASH[i-1] BLOCK[i])
prev_block_digest = block_hash.close();
block_hash = {}; block_hash.update(*prev_block_digest);
// Prepare hash for next data block: CHASH[i]=SHA2-512(CHASH[i-1] BLOCK[i])
util::SHA512 chain_hash;
if (prev_block_digest) chain_hash.update(*prev_block_digest);
chain_hash.update(block_digest);
prev_block_digest = chain_hash.close();
util::file_io::write(*sigsf, asio::buffer(e.str()), cancel, yield);
}
......@@ -350,43 +357,51 @@ class HttpStoreReader : public http_response::AbstractReader {
private:
static const std::size_t http_forward_block = 16384;
http_response::Head
parse_head(Cancel cancel, asio::yield_context yield)
{
assert(headf.is_open());
auto close_headf = defer([&headf = headf] { headf.close(); }); // no longer needed
public:
template<class IStream>
static
SignedHead read_signed_head(IStream& is, Cancel& cancel, asio::yield_context yield) {
assert(is.is_open());
auto on_cancel = cancel.connect([&] { is.close(); });
// Put in heap to avoid exceeding coroutine stack limit.
auto buffer = std::make_unique<beast::static_buffer<http_forward_block>>();
auto parser = std::make_unique<http::response_parser<http::empty_body>>();
sys::error_code ec;
http::async_read_header(headf, *buffer, *parser, yield[ec]);
http::async_read_header(is, *buffer, *parser, yield[ec]);
if (cancel) ec = asio::error::operation_aborted;
if (ec) return or_throw<http_response::Head>(yield, ec);
if (ec) return or_throw<SignedHead>(yield, ec);
if (!parser->is_header_done()) {
_ERROR("Failed to parse stored response head");
return or_throw<http_response::Head>(yield, sys::errc::make_error_code(sys::errc::no_message));
return or_throw<SignedHead>(yield, sys::errc::make_error_code(sys::errc::no_message));
}
auto head = parser->release().base();
uri = head[http_::response_uri_hdr].to_string();
if (uri.empty()) {
_ERROR("Missing URI in stored head");
return or_throw<http_response::Head>(yield, sys::errc::make_error_code(sys::errc::no_message));
}
auto bsh = head[http_::response_block_signatures_hdr];
if (bsh.empty()) {
_ERROR("Missing stored parameters for data block signatures; uri=", uri);
return or_throw<http_response::Head>(yield, sys::errc::make_error_code(sys::errc::no_message));
auto head_o = SignedHead::create_from_trusted_source(parser->release().base());
if (!head_o) {
return or_throw<SignedHead>(yield, sys::errc::make_error_code(sys::errc::no_message));
}
auto bs_params = cache::HttpBlockSigs::parse(bsh);
if (!bs_params) {
_ERROR("Malformed stored parameters for data block signatures; uri=", uri);
return or_throw<http_response::Head>(yield, sys::errc::make_error_code(sys::errc::no_message));
return std::move(*head_o);
}
public:
http_response::Head
parse_head(Cancel cancel, asio::yield_context yield)
{
sys::error_code ec;
auto head = read_signed_head(headf, cancel, yield[ec]);
if (ec) {
if (ec != asio::error::operation_aborted) {
_ERROR("Failed to parse stored response head");
}
return or_throw<http_response::Head>(yield, ec);
}
block_size = bs_params->size;
block_size = head.block_size();
auto data_size_hdr = head[http_::response_data_size_hdr];
auto data_size_opt = parse::number<std::size_t>(data_size_hdr);
if (!data_size_opt)
......@@ -422,8 +437,11 @@ private:
&& head[http::field::transfer_encoding].empty()
&& head[http::field::trailer].empty())) {
_WARN("Found framing headers in stored head, cleaning; uri=", uri);
head = http_injection_merge(std::move(head), {});
auto retval = http_injection_merge(std::move(head), {});
retval.set(http::field::transfer_encoding, "chunked");
return retval;
}
head.set(http::field::transfer_encoding, "chunked");
return head;
}
......@@ -457,9 +475,6 @@ protected:
assert(_is_head_done);
if (!sigsf.is_open()) return boost::none;
if (sigs_buffer.size() == 0)
sigs_buffer = SigEntry::create_parse_buffer();
return SigEntry::parse(sigsf, sigs_buffer, cancel, yield);
}
......@@ -977,4 +992,80 @@ HttpStore::size( Cancel cancel
return or_throw(yield, ec, sz);
}
HashList
http_store_load_hash_list( const fs::path& dir
, asio::executor exec
, Cancel& cancel
, asio::yield_context yield)
{
using Sha = util::SHA512;
using Digest = Sha::digest_type;
sys::error_code ec;
auto headf = util::file_io::open_readonly(exec, dir / head_fname, ec);
if (ec) return or_throw<HashList>(yield, ec);
auto sigsf = util::file_io::open_readonly(exec, dir / sigs_fname, ec);
if (ec) return or_throw<HashList>(yield, ec);
auto bodyf = util::file_io::open_readonly(exec, dir / body_fname, ec);
if (ec) return or_throw<HashList>(yield, ec);
HashList hl;
hl.signed_head = HttpStoreReader::read_signed_head(headf, cancel, yield[ec]);
if (cancel) ec = asio::error::operation_aborted;
if (ec) return or_throw<HashList>(yield, ec);
boost::optional<SigEntry> last_sig_entry;
std::string sig_buffer;
static const auto decode = [](const std::string& s) -> boost::optional<Digest> {
std::string d = util::base64_decode(s);
if (d.size() != Sha::size()) return boost::none;
return util::bytes::to_array<uint8_t, Sha::size()>(d);
};
while(true) {
auto opt_sig_entry = SigEntry::parse(sigsf, sig_buffer, cancel, yield[ec]);
if (cancel) ec = asio::error::operation_aborted;
if (ec) return or_throw<HashList>(yield, ec);
if (!opt_sig_entry) break;
last_sig_entry = opt_sig_entry;
auto d = decode(opt_sig_entry->data_digest);
if (!d) return or_throw<HashList>(yield, asio::error::bad_descriptor);
hl.block_hashes.push_back(*d);
}
if (!last_sig_entry) return or_throw<HashList>(yield, asio::error::bad_descriptor);
auto c = decode(last_sig_entry->prev_digest);
if (!c) return or_throw<HashList>(yield, asio::error::bad_descriptor);
std::string sig = util::base64_decode(last_sig_entry->signature);
if (sig.size() != util::Ed25519PublicKey::sig_size) {
return or_throw<HashList>(yield, asio::error::bad_descriptor);
}
hl.signature = util::bytes::to_array<uint8_t, util::Ed25519PublicKey::sig_size>(sig);
return hl;
}
HashList
HttpStore::load_hash_list( const std::string& key
, Cancel cancel
, asio::yield_context yield) const
{
auto dir = path_from_key(path, key);
return http_store_load_hash_list(dir, executor, cancel, yield);
}
}} // namespaces
......@@ -6,6 +6,7 @@
#include <boost/asio/spawn.hpp>
#include <boost/filesystem/path.hpp>
#include "hash_list.h"
#include "../constants.h"
#include "../response_reader.h"
#include "../util/signal.h"
......@@ -45,13 +46,17 @@ using reader_uptr = std::unique_ptr<http_response::AbstractReader>;
//
// - `body`: This is the raw body data (flat, no chunking or other framing).
//
// - `sigs`: This contains block signatures and chained hashes. It consists
// of LF-terminated lines with the following format for blocks i=0,1...:
// - `sigs`: This contains block signatures and chained hashes. It consists of
// fixed length, LF-terminated lines with the following format
// for blocks i=0,1...:
//
// LOWER_HEX(OFFSET[i])<SP>BASE64(SIG[i])<SP>BASE64(HASH([i-1]))
// PAD016_LHEX(OFFSET[i])<SP>BASE64(SIG[i])<SP>BASE64(DHASH[i])<SP>BASE64(CHASH[i-1])
//
// Where `BASE64(HASH[-1])` and `HASH[-1]` are the empty string and
// `HASH[i]=HASH(HASH[i-1] DATA[i])`.
// Where `PAD016_LHEX(x)` represents `x` in lower-case hexadecimal, zero-padded to 16 characters,
// `BASE64(CHASH[-1])` is established as `BASE64('\0' * 64)` (for padding the first line),
// `CHASH[-1]` is established as the empty string (for `CHASH[0]` computation),
// `DHASH[i]=SHA2-512(DATA[i])` (block data hash)
// `CHASH[i]=SHA2-512(CHASH[i-1] DHASH[i])` (block chain hash).
//
void http_store( http_response::AbstractReader&, const fs::path&
, const asio::executor&, Cancel, asio::yield_context);
......@@ -97,6 +102,9 @@ reader_uptr
http_store_head_reader( const fs::path&, asio::executor
, sys::error_code&);
HashList
http_store_load_hash_list(const fs::path&, asio::executor, Cancel&, asio::yield_context);
//// High-level classes for HTTP response storage
// Store each response in a directory named `DIGEST[:2]/DIGEST[2:]` (where
......@@ -126,6 +134,9 @@ public:
std::size_t
size(Cancel, asio::yield_context) const;
HashList
load_hash_list(const std::string& key, Cancel, asio::yield_context) const;
private:
fs::path path;
asio::executor executor;
......
#pragma once
#include "http_sign.h"
#include "../logger.h"
#include "../split_string.h"
#include "../parse/number.h"
#include "../http_util.h"
#include "../util/bytes.h"
namespace ouinet { namespace cache {
class SignedHead : public http_response::Head {
private:
using Base = http_response::Head;
public:
// A simple container for a parsed block signatures HTTP header.
// Only the `hs2019` algorithm with an explicit key is supported,
// so the ready-to-use key is left in `pk`.
struct BlockSigs {
util::Ed25519PublicKey pk;
boost::string_view algorithm; // always "hs2019"
size_t size;
static
boost::optional<BlockSigs> parse(boost::string_view);
};
// The only signature algorithm supported by this implementation.
static const std::string& sig_alg_hs2019() {
static std::string ret = "hs2019";
return ret;
}