Support for (request-target) in HTTP client signatures (#625)

This commit is contained in:
Julien Maffre 2019-12-10 17:24:56 +00:00 коммит произвёл Amaury Chamayou
Родитель 5624c7d68a
Коммит 3f0edba271
4 изменённых файлов: 326 добавлений и 283 удалений

Просмотреть файл

@ -86,8 +86,9 @@ public:
headers_to_sign.emplace_back(k);
}
const auto signing_string =
enclave::construct_raw_signed_string(headers, headers_to_sign);
std::string query = "";
const auto signing_string = enclave::http::construct_raw_signed_string(
http_method_str(HTTP_POST), method, query, headers, headers_to_sign);
if (!signing_string.has_value())
{
throw std::logic_error(fmt::format("Error constructing signed string"));

Просмотреть файл

@ -132,7 +132,7 @@ namespace enclave
}
void handle_message(
http_method method,
http_method verb,
const std::string& path,
const std::string& query,
const http::HeaderMap& headers,
@ -140,7 +140,7 @@ namespace enclave
{
LOG_INFO_FMT(
"Processing msg({}, {}, {}, [{} bytes])",
http_method_str(method),
http_method_str(verb),
path,
query,
body.size());
@ -206,7 +206,8 @@ namespace enclave
// TODO: For now, set this here as parse_rpc_context() resets
// rpc_ctx.signed_request for a HTTP endpoint.
auto http_sig_v = HttpSignatureVerifier(headers, body);
auto http_sig_v = http::HttpSignatureVerifier(
std::string(http_method_str(verb)), path, query, headers, body);
auto signed_req = http_sig_v.parse();
if (signed_req.has_value())
{

Просмотреть файл

@ -13,297 +13,335 @@
namespace enclave
{
// All HTTP headers are expected to be lowercase
static constexpr auto HTTP_HEADER_AUTHORIZATION = "authorization";
static constexpr auto HTTP_HEADER_DIGEST = "digest";
static constexpr auto DIGEST_SHA256 = "SHA-256";
static constexpr auto AUTH_SCHEME = "Signature";
static constexpr auto SIGN_PARAMS_KEYID = "keyId";
static constexpr auto SIGN_PARAMS_SIGNATURE = "signature";
static constexpr auto SIGN_PARAMS_ALGORITHM = "algorithm";
static constexpr auto SIGN_PARAMS_HEADERS = "headers";
static constexpr auto SIGN_ALGORITHM = "ecdsa-sha256";
static constexpr auto SIGN_PARAMS_DELIMITER = ",";
static constexpr auto SIGN_PARAMS_HEADERS_DELIMITER = " ";
std::optional<std::vector<uint8_t>> construct_raw_signed_string(
const http::HeaderMap& headers,
const std::vector<std::string_view>& headers_to_sign)
namespace http
{
std::string signed_string = {};
// All HTTP headers are expected to be lowercase
static constexpr auto HTTP_HEADER_AUTHORIZATION = "authorization";
static constexpr auto HTTP_HEADER_DIGEST = "digest";
bool first = true;
bool has_digest = false;
static constexpr auto DIGEST_SHA256 = "SHA-256";
for (const auto f : headers_to_sign)
static constexpr auto AUTH_SCHEME = "Signature";
static constexpr auto SIGN_PARAMS_KEYID = "keyId";
static constexpr auto SIGN_PARAMS_SIGNATURE = "signature";
static constexpr auto SIGN_PARAMS_ALGORITHM = "algorithm";
static constexpr auto SIGN_PARAMS_HEADERS = "headers";
static constexpr auto SIGN_ALGORITHM = "ecdsa-sha256";
static constexpr auto SIGN_HEADER_REQUEST_TARGET = "(request-target)";
static constexpr auto SIGN_PARAMS_DELIMITER = ",";
static constexpr auto SIGN_PARAMS_HEADERS_DELIMITER = " ";
std::optional<std::vector<uint8_t>> construct_raw_signed_string(
std::string verb,
const std::string& path,
const std::string& query,
const http::HeaderMap& headers,
const std::vector<std::string_view>& headers_to_sign)
{
const auto h = headers.find(f);
if (h == headers.end())
std::string signed_string = {};
std::string value = {};
bool has_digest = false;
bool first = true;
for (const auto f : headers_to_sign)
{
LOG_FAIL_FMT("Signed header {} does not exist", f);
return {};
}
// Digest field should be signed.
if (f == HTTP_HEADER_DIGEST)
{
has_digest = true;
}
if (!first)
{
signed_string.append("\n");
}
first = false;
signed_string.append(f);
signed_string.append(": ");
signed_string.append(h->second);
}
if (!has_digest)
{
LOG_FAIL_FMT("{} is not signed", HTTP_HEADER_DIGEST);
return {};
}
auto ret =
std::vector<uint8_t>({signed_string.begin(), signed_string.end()});
return ret;
}
// Implements verification of "Signature" scheme from
// https://tools.ietf.org/html/draft-cavage-http-signatures-12
//
// Tested with RequestClient in tests/infra/clients.py
//
// TODO:
// - Only supports public key crytography (i.e. no HMAC)
// - Only supports SHA-256 as digest algorithm
// - Only supports ecdsa-sha256 as signature algorithm
// - keyId is ignored
class HttpSignatureVerifier
{
private:
const http::HeaderMap& headers;
const std::vector<uint8_t>& body;
struct SignatureParams
{
std::string_view signature = {};
std::string_view signature_algorithm = {};
std::vector<std::string_view> signed_headers;
};
bool parse_auth_scheme(std::string_view& auth_header_value)
{
auto next_space = auth_header_value.find(" ");
if (next_space == std::string::npos)
{
LOG_FAIL_FMT("Authorization header only contains one field!");
return false;
}
auto auth_scheme = auth_header_value.substr(0, next_space);
if (auth_scheme != AUTH_SCHEME)
{
LOG_FAIL_FMT("{} is the only supported scheme", AUTH_SCHEME);
return false;
}
auth_header_value = auth_header_value.substr(next_space + 1);
return true;
}
bool verify_digest()
{
// First, retrieve digest from header
auto digest = headers.find(HTTP_HEADER_DIGEST);
if (digest == headers.end())
{
LOG_FAIL_FMT("HTTP header does not contain {}", HTTP_HEADER_DIGEST);
return false;
}
auto equal_pos = digest->second.find("=");
if (equal_pos == std::string::npos)
{
LOG_FAIL_FMT(
"{} header does not contain key=value", HTTP_HEADER_DIGEST);
return false;
}
auto sha_key = digest->second.substr(0, equal_pos);
if (sha_key != DIGEST_SHA256)
{
LOG_FAIL_FMT("Only {} digest is supported", DIGEST_SHA256);
return false;
}
auto raw_digest = tls::raw_from_b64(digest->second.substr(equal_pos + 1));
// Then, hash the request body
tls::HashBytes body_digest;
tls::do_hash(body.data(), body.size(), body_digest, MBEDTLS_MD_SHA256);
if (raw_digest != body_digest)
{
LOG_FAIL_FMT(
"Request body does not match {} header", HTTP_HEADER_DIGEST);
return false;
}
return true;
}
// Parses a delimited string with no delimiter at the end
// (e.g. "foo,bar,baz") and returns a vector parsed string views (e.g.
// ["foo", "bar", "baz"])
std::vector<std::string_view> parse_delimited_string(
std::string_view& s, const std::string& delimiter)
{
std::vector<std::string_view> strings;
bool last_string = false;
auto next_delimiter = s.find(delimiter);
while (next_delimiter != std::string::npos || !last_string)
{
auto token = s.substr(0, next_delimiter);
if (next_delimiter == std::string::npos)
if (f == SIGN_HEADER_REQUEST_TARGET)
{
last_string = true;
}
strings.emplace_back(token);
if (!last_string)
{
s = s.substr(next_delimiter + 1);
next_delimiter = s.find(delimiter);
}
}
return strings;
}
std::optional<SignatureParams> parse_signature_params(
std::string_view& auth_header_value)
{
SignatureParams sig_params = {};
auto parsed_params =
parse_delimited_string(auth_header_value, SIGN_PARAMS_DELIMITER);
for (auto& p : parsed_params)
{
auto eq_pos = p.find("=");
if (eq_pos != std::string::npos)
{
auto k = p.substr(0, eq_pos);
auto v = p.substr(eq_pos + 1);
// Remove inverted commas around value
v.remove_prefix(v.find_first_of("\"") + 1);
v.remove_suffix(v.size() - v.find_last_of("\""));
if (k == SIGN_PARAMS_KEYID)
// Store verb as lowercase
std::transform(
verb.begin(), verb.end(), verb.begin(), [](unsigned char c) {
return std::tolower(c);
});
value = fmt::format("{} {}", verb, path);
if (!query.empty())
{
// keyId is ignored
}
else if (k == SIGN_PARAMS_ALGORITHM)
{
sig_params.signature_algorithm = v;
if (v != SIGN_ALGORITHM)
{
LOG_FAIL_FMT("Signature algorithm {} is not supported", v);
return {};
}
}
else if (k == SIGN_PARAMS_SIGNATURE)
{
sig_params.signature = v;
}
else if (k == SIGN_PARAMS_HEADERS)
{
auto parsed_signed_headers =
parse_delimited_string(v, SIGN_PARAMS_HEADERS_DELIMITER);
if (parsed_signed_headers.size() == 0)
{
LOG_FAIL_FMT(
"No headers specified in {} field", SIGN_PARAMS_HEADERS);
return {};
}
for (const auto& h : parsed_signed_headers)
{
sig_params.signed_headers.emplace_back(h);
}
value.append(fmt::format("?{}", query));
}
}
else
{
LOG_FAIL_FMT("Authorization parameter {} does not contain \"=\"", p);
return {};
const auto h = headers.find(f);
if (h == headers.end())
{
LOG_FAIL_FMT("Signed header {} does not exist", f);
return {};
}
value = h->second;
// Digest field should be signed.
if (f == HTTP_HEADER_DIGEST)
{
has_digest = true;
}
}
if (!first)
{
signed_string.append("\n");
}
first = false;
signed_string.append(f);
signed_string.append(": ");
signed_string.append(value);
}
return sig_params;
}
public:
HttpSignatureVerifier(
const http::HeaderMap& headers_, const std::vector<uint8_t>& body_) :
headers(headers_),
body(body_)
{}
std::optional<ccf::SignedReq> parse()
{
auto auth = headers.find(HTTP_HEADER_AUTHORIZATION);
if (auth != headers.end())
if (!has_digest)
{
std::string_view authz_header = auth->second;
if (!parse_auth_scheme(authz_header))
{
throw std::logic_error(fmt::format(
"Error parsing {} scheme. Only {} is supported",
HTTP_HEADER_AUTHORIZATION,
AUTH_SCHEME));
}
if (!verify_digest())
{
throw std::logic_error(
fmt::format("Error verifying HTTP {} header", HTTP_HEADER_DIGEST));
}
auto parsed_sign_params = parse_signature_params(authz_header);
if (!parsed_sign_params.has_value())
{
throw std::logic_error(
fmt::format("Error parsing {} fields", HTTP_HEADER_AUTHORIZATION));
}
auto signed_raw = construct_raw_signed_string(
headers, parsed_sign_params->signed_headers);
if (!signed_raw.has_value())
{
throw std::logic_error(
fmt::format("Error constructing signed string"));
}
auto sig_raw = tls::raw_from_b64(parsed_sign_params->signature);
auto raw_req = std::vector<uint8_t>({body.begin(), body.end()});
ccf::SignedReq ret = {
sig_raw, signed_raw.value(), raw_req, MBEDTLS_MD_SHA256};
return ret;
LOG_FAIL_FMT("{} is not signed", HTTP_HEADER_DIGEST);
return {};
}
// The request does not contain the Authorization header
return {};
auto ret =
std::vector<uint8_t>({signed_string.begin(), signed_string.end()});
return ret;
}
};
// Implements verification of "Signature" scheme from
// https://tools.ietf.org/html/draft-cavage-http-signatures-12
//
// Tested with RequestClient in tests/infra/clients.py
//
// TODO:
// - Only supports public key crytography (i.e. no HMAC)
// - Only supports SHA-256 as digest algorithm
// - Only supports ecdsa-sha256 as signature algorithm
// - keyId is ignored
class HttpSignatureVerifier
{
private:
const std::string& verb;
const std::string& path;
const std::string& query;
const http::HeaderMap& headers;
const std::vector<uint8_t>& body;
struct SignatureParams
{
std::string_view signature = {};
std::string_view signature_algorithm = {};
std::vector<std::string_view> signed_headers;
};
bool parse_auth_scheme(std::string_view& auth_header_value)
{
auto next_space = auth_header_value.find(" ");
if (next_space == std::string::npos)
{
LOG_FAIL_FMT("Authorization header only contains one field!");
return false;
}
auto auth_scheme = auth_header_value.substr(0, next_space);
if (auth_scheme != AUTH_SCHEME)
{
LOG_FAIL_FMT("{} is the only supported scheme", AUTH_SCHEME);
return false;
}
auth_header_value = auth_header_value.substr(next_space + 1);
return true;
}
bool verify_digest()
{
// First, retrieve digest from header
auto digest = headers.find(HTTP_HEADER_DIGEST);
if (digest == headers.end())
{
LOG_FAIL_FMT("HTTP header does not contain {}", HTTP_HEADER_DIGEST);
return false;
}
auto equal_pos = digest->second.find("=");
if (equal_pos == std::string::npos)
{
LOG_FAIL_FMT(
"{} header does not contain key=value", HTTP_HEADER_DIGEST);
return false;
}
auto sha_key = digest->second.substr(0, equal_pos);
if (sha_key != DIGEST_SHA256)
{
LOG_FAIL_FMT("Only {} digest is supported", DIGEST_SHA256);
return false;
}
auto raw_digest =
tls::raw_from_b64(digest->second.substr(equal_pos + 1));
// Then, hash the request body
tls::HashBytes body_digest;
tls::do_hash(body.data(), body.size(), body_digest, MBEDTLS_MD_SHA256);
if (raw_digest != body_digest)
{
LOG_FAIL_FMT(
"Request body does not match {} header", HTTP_HEADER_DIGEST);
return false;
}
return true;
}
// Parses a delimited string with no delimiter at the end
// (e.g. "foo,bar,baz") and returns a vector parsed string views (e.g.
// ["foo", "bar", "baz"])
std::vector<std::string_view> parse_delimited_string(
std::string_view& s, const std::string& delimiter)
{
std::vector<std::string_view> strings;
bool last_string = false;
auto next_delimiter = s.find(delimiter);
while (next_delimiter != std::string::npos || !last_string)
{
auto token = s.substr(0, next_delimiter);
if (next_delimiter == std::string::npos)
{
last_string = true;
}
strings.emplace_back(token);
if (!last_string)
{
s = s.substr(next_delimiter + 1);
next_delimiter = s.find(delimiter);
}
}
return strings;
}
std::optional<SignatureParams> parse_signature_params(
std::string_view& auth_header_value)
{
SignatureParams sig_params = {};
auto parsed_params =
parse_delimited_string(auth_header_value, SIGN_PARAMS_DELIMITER);
for (auto& p : parsed_params)
{
auto eq_pos = p.find("=");
if (eq_pos != std::string::npos)
{
auto k = p.substr(0, eq_pos);
auto v = p.substr(eq_pos + 1);
// Remove inverted commas around value
v.remove_prefix(v.find_first_of("\"") + 1);
v.remove_suffix(v.size() - v.find_last_of("\""));
if (k == SIGN_PARAMS_KEYID)
{
// keyId is ignored
}
else if (k == SIGN_PARAMS_ALGORITHM)
{
sig_params.signature_algorithm = v;
if (v != SIGN_ALGORITHM)
{
LOG_FAIL_FMT("Signature algorithm {} is not supported", v);
return {};
}
}
else if (k == SIGN_PARAMS_SIGNATURE)
{
sig_params.signature = v;
}
else if (k == SIGN_PARAMS_HEADERS)
{
auto parsed_signed_headers =
parse_delimited_string(v, SIGN_PARAMS_HEADERS_DELIMITER);
if (parsed_signed_headers.size() == 0)
{
LOG_FAIL_FMT(
"No headers specified in {} field", SIGN_PARAMS_HEADERS);
return {};
}
for (const auto& h : parsed_signed_headers)
{
sig_params.signed_headers.emplace_back(h);
}
}
}
else
{
LOG_FAIL_FMT(
"Authorization parameter {} does not contain \"=\"", p);
return {};
}
}
return sig_params;
}
public:
HttpSignatureVerifier(
const std::string& verb_,
const std::string& path_,
const std::string& query_,
const http::HeaderMap& headers_,
const std::vector<uint8_t>& body_) :
verb(verb_),
path(path_),
query(query_),
headers(headers_),
body(body_)
{}
std::optional<ccf::SignedReq> parse()
{
auto auth = headers.find(HTTP_HEADER_AUTHORIZATION);
if (auth != headers.end())
{
std::string_view authz_header = auth->second;
if (!parse_auth_scheme(authz_header))
{
throw std::logic_error(fmt::format(
"Error parsing {} scheme. Only {} is supported",
HTTP_HEADER_AUTHORIZATION,
AUTH_SCHEME));
}
if (!verify_digest())
{
throw std::logic_error(fmt::format(
"Error verifying HTTP {} header", HTTP_HEADER_DIGEST));
}
auto parsed_sign_params = parse_signature_params(authz_header);
if (!parsed_sign_params.has_value())
{
throw std::logic_error(fmt::format(
"Error parsing {} fields", HTTP_HEADER_AUTHORIZATION));
}
auto signed_raw = construct_raw_signed_string(
verb, path, query, headers, parsed_sign_params->signed_headers);
if (!signed_raw.has_value())
{
throw std::logic_error(
fmt::format("Error constructing signed string"));
}
auto sig_raw = tls::raw_from_b64(parsed_sign_params->signature);
auto raw_req = std::vector<uint8_t>({body.begin(), body.end()});
ccf::SignedReq ret = {
sig_raw, signed_raw.value(), raw_req, MBEDTLS_MD_SHA256};
return ret;
}
// The request does not contain the Authorization header
return {};
}
};
}
}

Просмотреть файл

@ -379,7 +379,7 @@ class RequestClient:
def request(self, request):
rep = requests.post(
f"https://{self.host}:{self.port}/{request.method}",
json=request.to_dict(), # TODO: For REST queries, use data= instead
json=request.to_dict(),
cert=(self.cert, self.key),
verify=self.ca,
timeout=self.request_timeout,
@ -391,13 +391,16 @@ class RequestClient:
with open(self.key, "rb") as k:
rep = requests.post(
f"https://{self.host}:{self.port}/{request.method}",
json=request.to_dict(), # TODO: For REST queries, use data= instead
json=request.to_dict(),
cert=(self.cert, self.key),
verify=self.ca,
timeout=self.request_timeout,
# key_id needs to be specified but is unused
auth=HTTPSignatureAuth(
algorithm="ecdsa-sha256", key=k.read(), key_id="tls",
algorithm="ecdsa-sha256",
key=k.read(),
key_id="tls",
headers=["(request-target)", "Date"],
),
)
self.stream.update(rep.content)