diff options
Diffstat (limited to 'src/api/signature')
-rw-r--r-- | src/api/signature/error.rs | 32 | ||||
-rw-r--r-- | src/api/signature/mod.rs | 78 | ||||
-rw-r--r-- | src/api/signature/payload.rs | 562 | ||||
-rw-r--r-- | src/api/signature/streaming.rs | 373 |
4 files changed, 0 insertions, 1045 deletions
diff --git a/src/api/signature/error.rs b/src/api/signature/error.rs deleted file mode 100644 index 2d92a072..00000000 --- a/src/api/signature/error.rs +++ /dev/null @@ -1,32 +0,0 @@ -use err_derive::Error; - -use crate::common_error::CommonError; -pub use crate::common_error::{CommonErrorDerivative, OkOrBadRequest, OkOrInternalError}; - -/// Errors of this crate -#[derive(Debug, Error)] -pub enum Error { - #[error(display = "{}", _0)] - /// Error from common error - Common(CommonError), - - /// Authorization Header Malformed - #[error(display = "Authorization header malformed, unexpected scope: {}", _0)] - AuthorizationHeaderMalformed(String), - - // Category: bad request - /// The request contained an invalid UTF-8 sequence in its path or in other parameters - #[error(display = "Invalid UTF-8: {}", _0)] - InvalidUtf8Str(#[error(source)] std::str::Utf8Error), -} - -impl<T> From<T> for Error -where - CommonError: From<T>, -{ - fn from(err: T) -> Self { - Error::Common(CommonError::from(err)) - } -} - -impl CommonErrorDerivative for Error {} diff --git a/src/api/signature/mod.rs b/src/api/signature/mod.rs deleted file mode 100644 index 6514da43..00000000 --- a/src/api/signature/mod.rs +++ /dev/null @@ -1,78 +0,0 @@ -use chrono::{DateTime, Utc}; -use hmac::{Hmac, Mac}; -use sha2::Sha256; - -use hyper::{body::Incoming as IncomingBody, Request}; - -use garage_model::garage::Garage; -use garage_model::key_table::Key; -use garage_util::data::{sha256sum, Hash}; - -use error::*; - -pub mod error; -pub mod payload; -pub mod streaming; - -pub const SHORT_DATE: &str = "%Y%m%d"; -pub const LONG_DATETIME: &str = "%Y%m%dT%H%M%SZ"; - -type HmacSha256 = Hmac<Sha256>; - -pub async fn verify_request( - garage: &Garage, - mut req: Request<IncomingBody>, - service: &'static str, -) -> Result<(Request<streaming::ReqBody>, Key, Option<Hash>), Error> { - let (api_key, mut content_sha256) = - payload::check_payload_signature(&garage, &mut req, service).await?; - let api_key = - api_key.ok_or_else(|| Error::forbidden("Garage does not support anonymous access yet"))?; - - let req = streaming::parse_streaming_body( - &api_key, - req, - &mut content_sha256, - &garage.config.s3_api.s3_region, - service, - )?; - - Ok((req, api_key, content_sha256)) -} - -pub fn verify_signed_content(expected_sha256: Hash, body: &[u8]) -> Result<(), Error> { - if expected_sha256 != sha256sum(body) { - return Err(Error::bad_request( - "Request content hash does not match signed hash".to_string(), - )); - } - Ok(()) -} - -pub fn signing_hmac( - datetime: &DateTime<Utc>, - secret_key: &str, - region: &str, - service: &str, -) -> Result<HmacSha256, crypto_common::InvalidLength> { - let secret = String::from("AWS4") + secret_key; - let mut date_hmac = HmacSha256::new_from_slice(secret.as_bytes())?; - date_hmac.update(datetime.format(SHORT_DATE).to_string().as_bytes()); - let mut region_hmac = HmacSha256::new_from_slice(&date_hmac.finalize().into_bytes())?; - region_hmac.update(region.as_bytes()); - let mut service_hmac = HmacSha256::new_from_slice(®ion_hmac.finalize().into_bytes())?; - service_hmac.update(service.as_bytes()); - let mut signing_hmac = HmacSha256::new_from_slice(&service_hmac.finalize().into_bytes())?; - signing_hmac.update(b"aws4_request"); - let hmac = HmacSha256::new_from_slice(&signing_hmac.finalize().into_bytes())?; - Ok(hmac) -} - -pub fn compute_scope(datetime: &DateTime<Utc>, region: &str, service: &str) -> String { - format!( - "{}/{}/{}/aws4_request", - datetime.format(SHORT_DATE), - region, - service - ) -} diff --git a/src/api/signature/payload.rs b/src/api/signature/payload.rs deleted file mode 100644 index d6ff62f0..00000000 --- a/src/api/signature/payload.rs +++ /dev/null @@ -1,562 +0,0 @@ -use std::collections::HashMap; -use std::convert::TryFrom; - -use chrono::{DateTime, Duration, NaiveDateTime, TimeZone, Utc}; -use hmac::Mac; -use hyper::header::{HeaderMap, HeaderName, HeaderValue, AUTHORIZATION, HOST}; -use hyper::{body::Incoming as IncomingBody, Method, Request}; -use sha2::{Digest, Sha256}; - -use garage_table::*; -use garage_util::data::Hash; - -use garage_model::garage::Garage; -use garage_model::key_table::*; - -use super::LONG_DATETIME; -use super::{compute_scope, signing_hmac}; - -use crate::encoding::uri_encode; -use crate::signature::error::*; - -pub const X_AMZ_ALGORITHM: HeaderName = HeaderName::from_static("x-amz-algorithm"); -pub const X_AMZ_CREDENTIAL: HeaderName = HeaderName::from_static("x-amz-credential"); -pub const X_AMZ_DATE: HeaderName = HeaderName::from_static("x-amz-date"); -pub const X_AMZ_EXPIRES: HeaderName = HeaderName::from_static("x-amz-expires"); -pub const X_AMZ_SIGNEDHEADERS: HeaderName = HeaderName::from_static("x-amz-signedheaders"); -pub const X_AMZ_SIGNATURE: HeaderName = HeaderName::from_static("x-amz-signature"); -pub const X_AMZ_CONTENT_SH256: HeaderName = HeaderName::from_static("x-amz-content-sha256"); - -pub const AWS4_HMAC_SHA256: &str = "AWS4-HMAC-SHA256"; -pub const UNSIGNED_PAYLOAD: &str = "UNSIGNED-PAYLOAD"; -pub const STREAMING_AWS4_HMAC_SHA256_PAYLOAD: &str = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"; - -pub type QueryMap = HeaderMap<QueryValue>; -pub struct QueryValue { - /// Original key with potential uppercase characters, - /// for use in signature calculation - key: String, - value: String, -} - -pub async fn check_payload_signature( - garage: &Garage, - request: &mut Request<IncomingBody>, - service: &'static str, -) -> Result<(Option<Key>, Option<Hash>), Error> { - let query = parse_query_map(request.uri())?; - - if query.contains_key(&X_AMZ_ALGORITHM) { - // We check for presigned-URL-style authentification first, because - // the browser or someting else could inject an Authorization header - // that is totally unrelated to AWS signatures. - check_presigned_signature(garage, service, request, query).await - } else if request.headers().contains_key(AUTHORIZATION) { - check_standard_signature(garage, service, request, query).await - } else { - // Unsigned (anonymous) request - let content_sha256 = request - .headers() - .get("x-amz-content-sha256") - .filter(|c| c.as_bytes() != UNSIGNED_PAYLOAD.as_bytes()); - if let Some(content_sha256) = content_sha256 { - let sha256 = hex::decode(content_sha256) - .ok() - .and_then(|bytes| Hash::try_from(&bytes)) - .ok_or_bad_request("Invalid content sha256 hash")?; - Ok((None, Some(sha256))) - } else { - Ok((None, None)) - } - } -} - -async fn check_standard_signature( - garage: &Garage, - service: &'static str, - request: &Request<IncomingBody>, - query: QueryMap, -) -> Result<(Option<Key>, Option<Hash>), Error> { - let authorization = Authorization::parse_header(request.headers())?; - - // Verify that all necessary request headers are included in signed_headers - // The following must be included for all signatures: - // - the Host header (mandatory) - // - all x-amz-* headers used in the request - // AWS also indicates that the Content-Type header should be signed if - // it is used, but Minio client doesn't sign it so we don't check it for compatibility. - let signed_headers = split_signed_headers(&authorization)?; - verify_signed_headers(request.headers(), &signed_headers)?; - - let canonical_request = canonical_request( - service, - request.method(), - request.uri().path(), - &query, - request.headers(), - &signed_headers, - &authorization.content_sha256, - )?; - let string_to_sign = string_to_sign( - &authorization.date, - &authorization.scope, - &canonical_request, - ); - - trace!("canonical request:\n{}", canonical_request); - trace!("string to sign:\n{}", string_to_sign); - - let key = verify_v4(garage, service, &authorization, string_to_sign.as_bytes()).await?; - - let content_sha256 = if authorization.content_sha256 == UNSIGNED_PAYLOAD { - None - } else if authorization.content_sha256 == STREAMING_AWS4_HMAC_SHA256_PAYLOAD { - let bytes = hex::decode(authorization.signature).ok_or_bad_request("Invalid signature")?; - Some(Hash::try_from(&bytes).ok_or_bad_request("Invalid signature")?) - } else { - let bytes = hex::decode(authorization.content_sha256) - .ok_or_bad_request("Invalid content sha256 hash")?; - Some(Hash::try_from(&bytes).ok_or_bad_request("Invalid content sha256 hash")?) - }; - - Ok((Some(key), content_sha256)) -} - -async fn check_presigned_signature( - garage: &Garage, - service: &'static str, - request: &mut Request<IncomingBody>, - mut query: QueryMap, -) -> Result<(Option<Key>, Option<Hash>), Error> { - let algorithm = query.get(&X_AMZ_ALGORITHM).unwrap(); - let authorization = Authorization::parse_presigned(&algorithm.value, &query)?; - - // Verify that all necessary request headers are included in signed_headers - // For AWSv4 pre-signed URLs, the following must be incldued: - // - the Host header (mandatory) - // - all x-amz-* headers used in the request - let signed_headers = split_signed_headers(&authorization)?; - verify_signed_headers(request.headers(), &signed_headers)?; - - // The X-Amz-Signature value is passed as a query parameter, - // but the signature cannot be computed from a string that contains itself. - // AWS specifies that all query params except X-Amz-Signature are included - // in the canonical request. - query.remove(&X_AMZ_SIGNATURE); - let canonical_request = canonical_request( - service, - request.method(), - request.uri().path(), - &query, - request.headers(), - &signed_headers, - &authorization.content_sha256, - )?; - let string_to_sign = string_to_sign( - &authorization.date, - &authorization.scope, - &canonical_request, - ); - - trace!("canonical request (presigned url):\n{}", canonical_request); - trace!("string to sign (presigned url):\n{}", string_to_sign); - - let key = verify_v4(garage, service, &authorization, string_to_sign.as_bytes()).await?; - - // In the page on presigned URLs, AWS specifies that if a signed query - // parameter and a signed header of the same name have different values, - // then an InvalidRequest error is raised. - let headers_mut = request.headers_mut(); - for (name, value) in query.iter() { - if let Some(existing) = headers_mut.get(name) { - if signed_headers.contains(&name) && existing.as_bytes() != value.value.as_bytes() { - return Err(Error::bad_request(format!( - "Conflicting values for `{}` in query parameters and request headers", - name - ))); - } - } - if name.as_str().starts_with("x-amz-") { - // Query parameters that start by x-amz- are actually intended to stand in for - // headers that can't be added at the time the request is made. - // What we do is just add them to the Request object as regular headers, - // that will be handled downstream as if they were included like in a normal request. - // (Here we allow such query parameters to override headers with the same name - // that are not signed, however there is not much reason that this would happen) - headers_mut.insert( - name, - HeaderValue::from_bytes(value.value.as_bytes()) - .ok_or_bad_request("invalid query parameter value")?, - ); - } - } - - // Presigned URLs always use UNSIGNED-PAYLOAD, - // so there is no sha256 hash to return. - Ok((Some(key), None)) -} - -pub fn parse_query_map(uri: &http::uri::Uri) -> Result<QueryMap, Error> { - let mut query = QueryMap::with_capacity(0); - if let Some(query_str) = uri.query() { - let query_pairs = url::form_urlencoded::parse(query_str.as_bytes()); - for (key, val) in query_pairs { - let name = - HeaderName::from_bytes(key.as_bytes()).ok_or_bad_request("Invalid header name")?; - - let value = QueryValue { - key: key.to_string(), - value: val.into_owned(), - }; - - if query.insert(name, value).is_some() { - return Err(Error::bad_request(format!( - "duplicate query parameter: `{}`", - key - ))); - } - } - } - Ok(query) -} - -fn parse_credential(cred: &str) -> Result<(String, String), Error> { - let first_slash = cred - .find('/') - .ok_or_bad_request("Credentials does not contain '/' in authorization field")?; - let (key_id, scope) = cred.split_at(first_slash); - Ok(( - key_id.to_string(), - scope.trim_start_matches('/').to_string(), - )) -} - -fn split_signed_headers(authorization: &Authorization) -> Result<Vec<HeaderName>, Error> { - let mut signed_headers = authorization - .signed_headers - .split(';') - .map(HeaderName::try_from) - .collect::<Result<Vec<HeaderName>, _>>() - .ok_or_bad_request("invalid header name")?; - signed_headers.sort_by(|h1, h2| h1.as_str().cmp(h2.as_str())); - Ok(signed_headers) -} - -fn verify_signed_headers(headers: &HeaderMap, signed_headers: &[HeaderName]) -> Result<(), Error> { - if !signed_headers.contains(&HOST) { - return Err(Error::bad_request("Header `Host` should be signed")); - } - for (name, _) in headers.iter() { - if name.as_str().starts_with("x-amz-") { - if !signed_headers.contains(name) { - return Err(Error::bad_request(format!( - "Header `{}` should be signed", - name - ))); - } - } - } - Ok(()) -} - -pub fn string_to_sign(datetime: &DateTime<Utc>, scope_string: &str, canonical_req: &str) -> String { - let mut hasher = Sha256::default(); - hasher.update(canonical_req.as_bytes()); - [ - AWS4_HMAC_SHA256, - &datetime.format(LONG_DATETIME).to_string(), - scope_string, - &hex::encode(hasher.finalize().as_slice()), - ] - .join("\n") -} - -pub fn canonical_request( - service: &'static str, - method: &Method, - canonical_uri: &str, - query: &QueryMap, - headers: &HeaderMap, - signed_headers: &[HeaderName], - content_sha256: &str, -) -> Result<String, Error> { - // There seems to be evidence that in AWSv4 signatures, the path component is url-encoded - // a second time when building the canonical request, as specified in this documentation page: - // -> https://docs.aws.amazon.com/rolesanywhere/latest/userguide/authentication-sign-process.html - // However this documentation page is for a specific service ("roles anywhere"), and - // in the S3 service we know for a fact that there is no double-urlencoding, because all of - // the tests we made with external software work without it. - // - // The theory is that double-urlencoding occurs for all services except S3, - // which is what is implemented in rusoto_signature: - // -> https://docs.rs/rusoto_signature/latest/src/rusoto_signature/signature.rs.html#464 - // - // Digging into the code of the official AWS Rust SDK, we learn that double-URI-encoding can - // be set or unset on a per-request basis (the signature crates, aws-sigv4 and aws-sig-auth, - // are agnostic to this). Grepping the codebase confirms that S3 is the only API for which - // double_uri_encode is set to false, meaning it is true (its default value) for all other - // AWS services. We will therefore implement this behavior in Garage as well. - // - // Note that this documentation page, which is touted as the "authoritative reference" on - // AWSv4 signatures, makes no mention of either single- or double-urlencoding: - // -> https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html - // This page of the S3 documentation does also not mention anything specific: - // -> https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html - // - // Note that there is also the issue of path normalization, which I hope is unrelated to the - // one of URI-encoding. At least in aws-sigv4 both parameters can be set independently, - // and rusoto_signature does not seem to do any effective path normalization, even though - // it mentions it in the comments (same link to the souce code as above). - // We make the explicit choice of NOT normalizing paths in the K2V API because doing so - // would make non-normalized paths invalid K2V partition keys, and we don't want that. - let canonical_uri: std::borrow::Cow<str> = if service != "s3" { - uri_encode(canonical_uri, false).into() - } else { - canonical_uri.into() - }; - - // Canonical query string from passed HeaderMap - let canonical_query_string = { - let mut items = Vec::with_capacity(query.len()); - for (_, QueryValue { key, value }) in query.iter() { - items.push(uri_encode(&key, true) + "=" + &uri_encode(&value, true)); - } - items.sort(); - items.join("&") - }; - - // Canonical header string calculated from signed headers - let canonical_header_string = signed_headers - .iter() - .map(|name| { - let value = headers - .get(name) - .ok_or_bad_request(format!("signed header `{}` is not present", name))?; - let value = std::str::from_utf8(value.as_bytes())?; - Ok(format!("{}:{}", name.as_str(), value.trim())) - }) - .collect::<Result<Vec<String>, Error>>()? - .join("\n"); - let signed_headers = signed_headers.join(";"); - - let list = [ - method.as_str(), - &canonical_uri, - &canonical_query_string, - &canonical_header_string, - "", - &signed_headers, - content_sha256, - ]; - Ok(list.join("\n")) -} - -pub fn parse_date(date: &str) -> Result<DateTime<Utc>, Error> { - let date: NaiveDateTime = - NaiveDateTime::parse_from_str(date, LONG_DATETIME).ok_or_bad_request("Invalid date")?; - Ok(Utc.from_utc_datetime(&date)) -} - -pub async fn verify_v4( - garage: &Garage, - service: &str, - auth: &Authorization, - payload: &[u8], -) -> Result<Key, Error> { - let scope_expected = compute_scope(&auth.date, &garage.config.s3_api.s3_region, service); - if auth.scope != scope_expected { - return Err(Error::AuthorizationHeaderMalformed(auth.scope.to_string())); - } - - let key = garage - .key_table - .get(&EmptyKey, &auth.key_id) - .await? - .filter(|k| !k.state.is_deleted()) - .ok_or_else(|| Error::forbidden(format!("No such key: {}", &auth.key_id)))?; - let key_p = key.params().unwrap(); - - let mut hmac = signing_hmac( - &auth.date, - &key_p.secret_key, - &garage.config.s3_api.s3_region, - service, - ) - .ok_or_internal_error("Unable to build signing HMAC")?; - hmac.update(payload); - let signature = - hex::decode(&auth.signature).map_err(|_| Error::forbidden("Invalid signature"))?; - if hmac.verify_slice(&signature).is_err() { - return Err(Error::forbidden("Invalid signature")); - } - - Ok(key) -} - -// ============ Authorization header, or X-Amz-* query params ========= - -pub struct Authorization { - key_id: String, - scope: String, - signed_headers: String, - signature: String, - content_sha256: String, - date: DateTime<Utc>, -} - -impl Authorization { - fn parse_header(headers: &HeaderMap) -> Result<Self, Error> { - let authorization = headers - .get(AUTHORIZATION) - .ok_or_bad_request("Missing authorization header")? - .to_str()?; - - let (auth_kind, rest) = authorization - .split_once(' ') - .ok_or_bad_request("Authorization field to short")?; - - if auth_kind != AWS4_HMAC_SHA256 { - return Err(Error::bad_request("Unsupported authorization method")); - } - - let mut auth_params = HashMap::new(); - for auth_part in rest.split(',') { - let auth_part = auth_part.trim(); - let eq = auth_part - .find('=') - .ok_or_bad_request("Field without value in authorization header")?; - let (key, value) = auth_part.split_at(eq); - auth_params.insert(key.to_string(), value.trim_start_matches('=').to_string()); - } - - let cred = auth_params - .get("Credential") - .ok_or_bad_request("Could not find Credential in Authorization field")?; - let signed_headers = auth_params - .get("SignedHeaders") - .ok_or_bad_request("Could not find SignedHeaders in Authorization field")? - .to_string(); - let signature = auth_params - .get("Signature") - .ok_or_bad_request("Could not find Signature in Authorization field")? - .to_string(); - - let content_sha256 = headers - .get(X_AMZ_CONTENT_SH256) - .ok_or_bad_request("Missing X-Amz-Content-Sha256 field")?; - - let date = headers - .get(X_AMZ_DATE) - .ok_or_bad_request("Missing X-Amz-Date field") - .map_err(Error::from)? - .to_str()?; - let date = parse_date(date)?; - - if Utc::now() - date > Duration::hours(24) { - return Err(Error::bad_request("Date is too old".to_string())); - } - - let (key_id, scope) = parse_credential(cred)?; - let auth = Authorization { - key_id, - scope, - signed_headers, - signature, - content_sha256: content_sha256.to_str()?.to_string(), - date, - }; - Ok(auth) - } - - fn parse_presigned(algorithm: &str, query: &QueryMap) -> Result<Self, Error> { - if algorithm != AWS4_HMAC_SHA256 { - return Err(Error::bad_request( - "Unsupported authorization method".to_string(), - )); - } - - let cred = query - .get(&X_AMZ_CREDENTIAL) - .ok_or_bad_request("X-Amz-Credential not found in query parameters")?; - let signed_headers = query - .get(&X_AMZ_SIGNEDHEADERS) - .ok_or_bad_request("X-Amz-SignedHeaders not found in query parameters")?; - let signature = query - .get(&X_AMZ_SIGNATURE) - .ok_or_bad_request("X-Amz-Signature not found in query parameters")?; - - let duration = query - .get(&X_AMZ_EXPIRES) - .ok_or_bad_request("X-Amz-Expires not found in query parameters")? - .value - .parse() - .map_err(|_| Error::bad_request("X-Amz-Expires is not a number".to_string()))?; - - if duration > 7 * 24 * 3600 { - return Err(Error::bad_request( - "X-Amz-Expires may not exceed a week".to_string(), - )); - } - - let date = query - .get(&X_AMZ_DATE) - .ok_or_bad_request("Missing X-Amz-Date field")?; - let date = parse_date(&date.value)?; - - if Utc::now() - date > Duration::seconds(duration) { - return Err(Error::bad_request("Date is too old".to_string())); - } - - let (key_id, scope) = parse_credential(&cred.value)?; - Ok(Authorization { - key_id, - scope, - signed_headers: signed_headers.value.clone(), - signature: signature.value.clone(), - content_sha256: UNSIGNED_PAYLOAD.to_string(), - date, - }) - } - - pub(crate) fn parse_form(params: &HeaderMap) -> Result<Self, Error> { - let algorithm = params - .get(X_AMZ_ALGORITHM) - .ok_or_bad_request("Missing X-Amz-Algorithm header")? - .to_str()?; - if algorithm != AWS4_HMAC_SHA256 { - return Err(Error::bad_request( - "Unsupported authorization method".to_string(), - )); - } - - let credential = params - .get(X_AMZ_CREDENTIAL) - .ok_or_else(|| Error::forbidden("Garage does not support anonymous access yet"))? - .to_str()?; - let signature = params - .get(X_AMZ_SIGNATURE) - .ok_or_bad_request("No signature was provided")? - .to_str()? - .to_string(); - let date = params - .get(X_AMZ_DATE) - .ok_or_bad_request("No date was provided")? - .to_str()?; - let date = parse_date(date)?; - - if Utc::now() - date > Duration::hours(24) { - return Err(Error::bad_request("Date is too old".to_string())); - } - - let (key_id, scope) = parse_credential(credential)?; - let auth = Authorization { - key_id, - scope, - signed_headers: "".to_string(), - signature, - content_sha256: UNSIGNED_PAYLOAD.to_string(), - date, - }; - Ok(auth) - } -} diff --git a/src/api/signature/streaming.rs b/src/api/signature/streaming.rs deleted file mode 100644 index e223d1b1..00000000 --- a/src/api/signature/streaming.rs +++ /dev/null @@ -1,373 +0,0 @@ -use std::pin::Pin; - -use chrono::{DateTime, NaiveDateTime, TimeZone, Utc}; -use futures::prelude::*; -use futures::task; -use garage_model::key_table::Key; -use hmac::Mac; -use http_body_util::StreamBody; -use hyper::body::{Bytes, Incoming as IncomingBody}; -use hyper::Request; - -use garage_util::data::Hash; - -use super::{compute_scope, sha256sum, HmacSha256, LONG_DATETIME}; - -use crate::helpers::*; -use crate::signature::error::*; -use crate::signature::payload::{ - STREAMING_AWS4_HMAC_SHA256_PAYLOAD, X_AMZ_CONTENT_SH256, X_AMZ_DATE, -}; - -pub const AWS4_HMAC_SHA256_PAYLOAD: &str = "AWS4-HMAC-SHA256-PAYLOAD"; - -pub type ReqBody = BoxBody<Error>; - -pub fn parse_streaming_body( - api_key: &Key, - req: Request<IncomingBody>, - content_sha256: &mut Option<Hash>, - region: &str, - service: &str, -) -> Result<Request<ReqBody>, Error> { - match req.headers().get(X_AMZ_CONTENT_SH256) { - Some(header) if header == STREAMING_AWS4_HMAC_SHA256_PAYLOAD => { - let signature = content_sha256 - .take() - .ok_or_bad_request("No signature provided")?; - - let secret_key = &api_key - .state - .as_option() - .ok_or_internal_error("Deleted key state")? - .secret_key; - - let date = req - .headers() - .get(X_AMZ_DATE) - .ok_or_bad_request("Missing X-Amz-Date field")? - .to_str()?; - let date: NaiveDateTime = NaiveDateTime::parse_from_str(date, LONG_DATETIME) - .ok_or_bad_request("Invalid date")?; - let date: DateTime<Utc> = Utc.from_utc_datetime(&date); - - let scope = compute_scope(&date, region, service); - let signing_hmac = crate::signature::signing_hmac(&date, secret_key, region, service) - .ok_or_internal_error("Unable to build signing HMAC")?; - - Ok(req.map(move |body| { - let stream = body_stream::<_, Error>(body); - let signed_payload_stream = - SignedPayloadStream::new(stream, signing_hmac, date, &scope, signature) - .map(|x| x.map(hyper::body::Frame::data)) - .map_err(Error::from); - ReqBody::new(StreamBody::new(signed_payload_stream)) - })) - } - _ => Ok(req.map(|body| ReqBody::new(http_body_util::BodyExt::map_err(body, Error::from)))), - } -} - -/// Result of `sha256("")` -const EMPTY_STRING_HEX_DIGEST: &str = - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; - -fn compute_streaming_payload_signature( - signing_hmac: &HmacSha256, - date: DateTime<Utc>, - scope: &str, - previous_signature: Hash, - content_sha256: Hash, -) -> Result<Hash, Error> { - let string_to_sign = [ - AWS4_HMAC_SHA256_PAYLOAD, - &date.format(LONG_DATETIME).to_string(), - scope, - &hex::encode(previous_signature), - EMPTY_STRING_HEX_DIGEST, - &hex::encode(content_sha256), - ] - .join("\n"); - - let mut hmac = signing_hmac.clone(); - hmac.update(string_to_sign.as_bytes()); - - Ok(Hash::try_from(&hmac.finalize().into_bytes()).ok_or_internal_error("Invalid signature")?) -} - -mod payload { - use garage_util::data::Hash; - - pub enum Error<I> { - Parser(nom::error::Error<I>), - BadSignature, - } - - impl<I> Error<I> { - pub fn description(&self) -> &str { - match *self { - Error::Parser(ref e) => e.code.description(), - Error::BadSignature => "Bad signature", - } - } - } - - #[derive(Debug, Clone)] - pub struct Header { - pub size: usize, - pub signature: Hash, - } - - impl Header { - pub fn parse(input: &[u8]) -> nom::IResult<&[u8], Self, Error<&[u8]>> { - use nom::bytes::streaming::tag; - use nom::character::streaming::hex_digit1; - use nom::combinator::map_res; - use nom::number::streaming::hex_u32; - - macro_rules! try_parse { - ($expr:expr) => { - $expr.map_err(|e| e.map(Error::Parser))? - }; - } - - let (input, size) = try_parse!(hex_u32(input)); - let (input, _) = try_parse!(tag(";")(input)); - - let (input, _) = try_parse!(tag("chunk-signature=")(input)); - let (input, data) = try_parse!(map_res(hex_digit1, hex::decode)(input)); - let signature = Hash::try_from(&data).ok_or(nom::Err::Failure(Error::BadSignature))?; - - let (input, _) = try_parse!(tag("\r\n")(input)); - - let header = Header { - size: size as usize, - signature, - }; - - Ok((input, header)) - } - } -} - -#[derive(Debug)] -pub enum SignedPayloadStreamError { - Stream(Error), - InvalidSignature, - Message(String), -} - -impl SignedPayloadStreamError { - fn message(msg: &str) -> Self { - SignedPayloadStreamError::Message(msg.into()) - } -} - -impl From<SignedPayloadStreamError> for Error { - fn from(err: SignedPayloadStreamError) -> Self { - match err { - SignedPayloadStreamError::Stream(e) => e, - SignedPayloadStreamError::InvalidSignature => { - Error::bad_request("Invalid payload signature") - } - SignedPayloadStreamError::Message(e) => { - Error::bad_request(format!("Chunk format error: {}", e)) - } - } - } -} - -impl<I> From<payload::Error<I>> for SignedPayloadStreamError { - fn from(err: payload::Error<I>) -> Self { - Self::message(err.description()) - } -} - -impl<I> From<nom::error::Error<I>> for SignedPayloadStreamError { - fn from(err: nom::error::Error<I>) -> Self { - Self::message(err.code.description()) - } -} - -struct SignedPayload { - header: payload::Header, - data: Bytes, -} - -#[pin_project::pin_project] -pub struct SignedPayloadStream<S> -where - S: Stream<Item = Result<Bytes, Error>>, -{ - #[pin] - stream: S, - buf: bytes::BytesMut, - datetime: DateTime<Utc>, - scope: String, - signing_hmac: HmacSha256, - previous_signature: Hash, -} - -impl<S> SignedPayloadStream<S> -where - S: Stream<Item = Result<Bytes, Error>>, -{ - pub fn new( - stream: S, - signing_hmac: HmacSha256, - datetime: DateTime<Utc>, - scope: &str, - seed_signature: Hash, - ) -> Self { - Self { - stream, - buf: bytes::BytesMut::new(), - datetime, - scope: scope.into(), - signing_hmac, - previous_signature: seed_signature, - } - } - - fn parse_next(input: &[u8]) -> nom::IResult<&[u8], SignedPayload, SignedPayloadStreamError> { - use nom::bytes::streaming::{tag, take}; - - macro_rules! try_parse { - ($expr:expr) => { - $expr.map_err(nom::Err::convert)? - }; - } - - let (input, header) = try_parse!(payload::Header::parse(input)); - - // 0-sized chunk is the last - if header.size == 0 { - return Ok(( - input, - SignedPayload { - header, - data: Bytes::new(), - }, - )); - } - - let (input, data) = try_parse!(take::<_, _, nom::error::Error<_>>(header.size)(input)); - let (input, _) = try_parse!(tag::<_, _, nom::error::Error<_>>("\r\n")(input)); - - let data = Bytes::from(data.to_vec()); - - Ok((input, SignedPayload { header, data })) - } -} - -impl<S> Stream for SignedPayloadStream<S> -where - S: Stream<Item = Result<Bytes, Error>> + Unpin, -{ - type Item = Result<Bytes, SignedPayloadStreamError>; - - fn poll_next( - self: Pin<&mut Self>, - cx: &mut task::Context<'_>, - ) -> task::Poll<Option<Self::Item>> { - use std::task::Poll; - - let mut this = self.project(); - - loop { - let (input, payload) = match Self::parse_next(this.buf) { - Ok(res) => res, - Err(nom::Err::Incomplete(_)) => { - match futures::ready!(this.stream.as_mut().poll_next(cx)) { - Some(Ok(bytes)) => { - this.buf.extend(bytes); - continue; - } - Some(Err(e)) => { - return Poll::Ready(Some(Err(SignedPayloadStreamError::Stream(e)))) - } - None => { - return Poll::Ready(Some(Err(SignedPayloadStreamError::message( - "Unexpected EOF", - )))); - } - } - } - Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) => { - return Poll::Ready(Some(Err(e))) - } - }; - - // 0-sized chunk is the last - if payload.data.is_empty() { - return Poll::Ready(None); - } - - let data_sha256sum = sha256sum(&payload.data); - - let expected_signature = compute_streaming_payload_signature( - this.signing_hmac, - *this.datetime, - this.scope, - *this.previous_signature, - data_sha256sum, - ) - .map_err(|e| { - SignedPayloadStreamError::Message(format!("Could not build signature: {}", e)) - })?; - - if payload.header.signature != expected_signature { - return Poll::Ready(Some(Err(SignedPayloadStreamError::InvalidSignature))); - } - - *this.buf = input.into(); - *this.previous_signature = payload.header.signature; - - return Poll::Ready(Some(Ok(payload.data))); - } - } - - fn size_hint(&self) -> (usize, Option<usize>) { - self.stream.size_hint() - } -} - -#[cfg(test)] -mod tests { - use futures::prelude::*; - - use super::{SignedPayloadStream, SignedPayloadStreamError}; - - #[tokio::test] - async fn test_interrupted_signed_payload_stream() { - use chrono::{DateTime, Utc}; - - use garage_util::data::Hash; - - let datetime = DateTime::parse_from_rfc3339("2021-12-13T13:12:42+01:00") // TODO UNIX 0 - .unwrap() - .with_timezone(&Utc); - let secret_key = "test"; - let region = "test"; - let scope = crate::signature::compute_scope(&datetime, region, "s3"); - let signing_hmac = - crate::signature::signing_hmac(&datetime, secret_key, region, "s3").unwrap(); - - let data: &[&[u8]] = &[b"1"]; - let body = futures::stream::iter(data.iter().map(|block| Ok(block.to_vec().into()))); - - let seed_signature = Hash::default(); - - let mut stream = - SignedPayloadStream::new(body, signing_hmac, datetime, &scope, seed_signature); - - assert!(stream.try_next().await.is_err()); - match stream.try_next().await { - Err(SignedPayloadStreamError::Message(msg)) if msg == "Unexpected EOF" => {} - item => panic!( - "Unexpected result, expected early EOF error, got {:?}", - item - ), - } - } -} |