diff --git a/src/api/common/signature/checksum.rs b/src/api/common/signature/checksum.rs index c6beb33f..432ed44d 100644 --- a/src/api/common/signature/checksum.rs +++ b/src/api/common/signature/checksum.rs @@ -12,7 +12,7 @@ use http::HeaderName; use garage_util::data::*; -use garage_model::s3::object_table::*; +use garage_model::s3::object_table::{ChecksumAlgorithm, ChecksumValue}; use super::error::*; diff --git a/src/api/common/signature/mod.rs b/src/api/common/signature/mod.rs index 08b0aa7e..2421d696 100644 --- a/src/api/common/signature/mod.rs +++ b/src/api/common/signature/mod.rs @@ -27,7 +27,7 @@ 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 X_AMZ_CONTENT_SHA256: HeaderName = HeaderName::from_static("x-amz-content-sha256"); pub const X_AMZ_TRAILER: HeaderName = HeaderName::from_static("x-amz-trailer"); /// Result of `sha256("")` @@ -40,6 +40,7 @@ type HmacSha256 = Hmac; // Possible values for x-amz-content-sha256, in addition to the actual sha256 pub const UNSIGNED_PAYLOAD: &str = "UNSIGNED-PAYLOAD"; +pub const STREAMING_UNSIGNED_PAYLOAD_TRAILER: &str = "STREAMING-UNSIGNED-PAYLOAD-TRAILER"; pub const STREAMING_AWS4_HMAC_SHA256_PAYLOAD: &str = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"; // Used in the computation of StringToSign @@ -47,46 +48,46 @@ pub const AWS4_HMAC_SHA256_PAYLOAD: &str = "AWS4-HMAC-SHA256-PAYLOAD"; // ---- enums to describe stuff going on in signature calculation ---- +#[derive(Debug)] pub enum ContentSha256Header { UnsignedPayload, - Sha256Hash(String), - StreamingPayload { - trailer: Option, - algorithm: Option, - }, -} - -pub enum SigningAlgorithm { - AwsHmacSha256, -} - -pub enum TrailerHeader { - XAmzChecksumCrc32, - XAmzChecksumCrc32c, - XAmzChecksumCrc64Nvme, + Sha256Hash(Hash), + StreamingPayload { trailer: bool, signed: bool }, } // ---- top-level functions ---- +pub struct VerifiedRequest { + pub request: Request, + pub access_key: Key, + pub content_sha256_header: ContentSha256Header, + // TODO: oneshot chans to retrieve hashes after reading all body +} + pub async fn verify_request( garage: &Garage, mut req: Request, service: &'static str, -) -> Result<(Request, Key, Option), 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"))?; +) -> Result { + let checked_signature = payload::check_payload_signature(&garage, &mut req, service).await?; + eprintln!("checked signature: {:?}", checked_signature); - let req = streaming::parse_streaming_body( - &api_key, + let request = streaming::parse_streaming_body( req, - &mut content_sha256, + &checked_signature, &garage.config.s3_api.s3_region, service, )?; - Ok((req, api_key, content_sha256)) + let access_key = checked_signature + .key + .ok_or_else(|| Error::forbidden("Garage does not support anonymous access yet"))?; + + Ok(VerifiedRequest { + request, + access_key, + content_sha256_header: checked_signature.content_sha256_header, + }) } pub fn verify_signed_content(expected_sha256: Hash, body: &[u8]) -> Result<(), Error> { diff --git a/src/api/common/signature/payload.rs b/src/api/common/signature/payload.rs index 0b501853..ccc55c90 100644 --- a/src/api/common/signature/payload.rs +++ b/src/api/common/signature/payload.rs @@ -25,11 +25,18 @@ pub struct QueryValue { value: String, } +#[derive(Debug)] +pub struct CheckedSignature { + pub key: Option, + pub content_sha256_header: ContentSha256Header, + pub signature_header: Option, +} + pub async fn check_payload_signature( garage: &Garage, request: &mut Request, service: &'static str, -) -> Result<(Option, Option), Error> { +) -> Result { let query = parse_query_map(request.uri())?; if query.contains_key(&X_AMZ_ALGORITHM) { @@ -43,17 +50,51 @@ pub async fn check_payload_signature( // 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))) + .get(X_AMZ_CONTENT_SHA256) + .map(|x| x.to_str()) + .transpose()?; + Ok(CheckedSignature { + key: None, + content_sha256_header: parse_x_amz_content_sha256(content_sha256)?, + signature_header: None, + }) + } +} + +fn parse_x_amz_content_sha256(header: Option<&str>) -> Result { + let header = match header { + Some(x) => x, + None => return Ok(ContentSha256Header::UnsignedPayload), + }; + if header == UNSIGNED_PAYLOAD { + Ok(ContentSha256Header::UnsignedPayload) + } else if let Some(rest) = header.strip_prefix("STREAMING-") { + let (trailer, algo) = if let Some(rest2) = rest.strip_suffix("-TRAILER") { + (true, rest2) } else { - Ok((None, None)) + (false, rest) + }; + if algo == AWS4_HMAC_SHA256_PAYLOAD { + Ok(ContentSha256Header::StreamingPayload { + trailer, + signed: true, + }) + } else if algo == UNSIGNED_PAYLOAD { + Ok(ContentSha256Header::StreamingPayload { + trailer, + signed: false, + }) + } else { + Err(Error::bad_request( + "invalid or unsupported x-amz-content-sha256", + )) } + } else { + let sha256 = hex::decode(header) + .ok() + .and_then(|bytes| Hash::try_from(&bytes)) + .ok_or_bad_request("Invalid content sha256 hash")?; + Ok(ContentSha256Header::Sha256Hash(sha256)) } } @@ -62,7 +103,7 @@ async fn check_standard_signature( service: &'static str, request: &Request, query: QueryMap, -) -> Result<(Option, Option), Error> { +) -> Result { let authorization = Authorization::parse_header(request.headers())?; // Verify that all necessary request headers are included in signed_headers @@ -94,18 +135,13 @@ async fn check_standard_signature( 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")?) - }; + let content_sha256_header = parse_x_amz_content_sha256(Some(&authorization.content_sha256))?; - Ok((Some(key), content_sha256)) + Ok(CheckedSignature { + key: Some(key), + content_sha256_header, + signature_header: Some(authorization.signature), + }) } async fn check_presigned_signature( @@ -113,7 +149,7 @@ async fn check_presigned_signature( service: &'static str, request: &mut Request, mut query: QueryMap, -) -> Result<(Option, Option), Error> { +) -> Result { let algorithm = query.get(&X_AMZ_ALGORITHM).unwrap(); let authorization = Authorization::parse_presigned(&algorithm.value, &query)?; @@ -179,7 +215,11 @@ async fn check_presigned_signature( // Presigned URLs always use UNSIGNED-PAYLOAD, // so there is no sha256 hash to return. - Ok((Some(key), None)) + Ok(CheckedSignature { + key: Some(key), + content_sha256_header: ContentSha256Header::UnsignedPayload, + signature_header: Some(authorization.signature), + }) } pub fn parse_query_map(uri: &http::uri::Uri) -> Result { @@ -428,7 +468,7 @@ impl Authorization { .to_string(); let content_sha256 = headers - .get(X_AMZ_CONTENT_SH256) + .get(X_AMZ_CONTENT_SHA256) .ok_or_bad_request("Missing X-Amz-Content-Sha256 field")?; let date = headers diff --git a/src/api/common/signature/streaming.rs b/src/api/common/signature/streaming.rs index e08a4750..98079ffb 100644 --- a/src/api/common/signature/streaming.rs +++ b/src/api/common/signature/streaming.rs @@ -3,7 +3,6 @@ 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}; @@ -14,27 +13,47 @@ use garage_util::data::Hash; use super::*; use crate::helpers::*; +use crate::signature::payload::CheckedSignature; pub type ReqBody = BoxBody; pub fn parse_streaming_body( - api_key: &Key, req: Request, - content_sha256: &mut Option, + checked_signature: &CheckedSignature, region: &str, service: &str, ) -> Result, 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")?; + match checked_signature.content_sha256_header { + ContentSha256Header::StreamingPayload { signed, trailer } => { + if trailer { + return Err(Error::bad_request( + "STREAMING-*-TRAILER is not supported by Garage", + )); + } + if !signed { + return Err(Error::bad_request( + "STREAMING-UNSIGNED-PAYLOAD-* is not supported by Garage", + )); + } - let secret_key = &api_key + let signature = checked_signature + .signature_header + .clone() + .ok_or_bad_request("No signature provided")?; + let signature = hex::decode(signature) + .ok() + .and_then(|bytes| Hash::try_from(&bytes)) + .ok_or_bad_request("Invalid signature")?; + + let secret_key = checked_signature + .key + .as_ref() + .ok_or_bad_request("Cannot sign streaming payload without signing key")? .state .as_option() .ok_or_internal_error("Deleted key state")? - .secret_key; + .secret_key + .to_string(); let date = req .headers() @@ -46,7 +65,7 @@ pub fn parse_streaming_body( let date: DateTime = Utc.from_utc_datetime(&date); let scope = compute_scope(&date, region, service); - let signing_hmac = crate::signature::signing_hmac(&date, secret_key, 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| { diff --git a/src/api/k2v/api_server.rs b/src/api/k2v/api_server.rs index eb276f5b..de5775da 100644 --- a/src/api/k2v/api_server.rs +++ b/src/api/k2v/api_server.rs @@ -81,7 +81,9 @@ impl ApiHandler for K2VApiServer { return Ok(options_res.map(|_empty_body: EmptyBody| empty_body())); } - let (req, api_key, _content_sha256) = verify_request(&garage, req, "k2v").await?; + let verified_request = verify_request(&garage, req, "k2v").await?; + let req = verified_request.request; + let api_key = verified_request.access_key; let bucket_id = garage .bucket_helper() diff --git a/src/api/k2v/error.rs b/src/api/k2v/error.rs index b7ca5aa4..257ff893 100644 --- a/src/api/k2v/error.rs +++ b/src/api/k2v/error.rs @@ -76,7 +76,7 @@ impl Error { Error::InvalidBase64(_) => "InvalidBase64", Error::InvalidUtf8Str(_) => "InvalidUtf8String", Error::InvalidCausalityToken => "CausalityToken", - Error::InvalidDigest(_) => "InvalidDigest", + Error::InvalidDigest(_) => "InvalidDigest", } } } @@ -91,7 +91,7 @@ impl ApiError for Error { Error::AuthorizationHeaderMalformed(_) | Error::InvalidBase64(_) | Error::InvalidUtf8Str(_) - | Error::InvalidDigest(_) + | Error::InvalidDigest(_) | Error::InvalidCausalityToken => StatusCode::BAD_REQUEST, } } diff --git a/src/api/s3/api_server.rs b/src/api/s3/api_server.rs index 14fd03c3..0fdaab70 100644 --- a/src/api/s3/api_server.rs +++ b/src/api/s3/api_server.rs @@ -15,7 +15,7 @@ use garage_model::key_table::Key; use garage_api_common::cors::*; use garage_api_common::generic_server::*; use garage_api_common::helpers::*; -use garage_api_common::signature::verify_request; +use garage_api_common::signature::{verify_request, ContentSha256Header}; use crate::bucket::*; use crate::copy::*; @@ -121,7 +121,14 @@ impl ApiHandler for S3ApiServer { return Ok(options_res.map(|_empty_body: EmptyBody| empty_body())); } - let (req, api_key, content_sha256) = verify_request(&garage, req, "s3").await?; + let verified_request = verify_request(&garage, req, "s3").await?; + let req = verified_request.request; + let api_key = verified_request.access_key; + let content_sha256 = match verified_request.content_sha256_header { + ContentSha256Header::Sha256Hash(h) => Some(h), + // TODO take into account streaming/trailer checksums, etc. + _ => None, + }; let bucket_name = match bucket_name { None => { diff --git a/src/garage/tests/common/custom_requester.rs b/src/garage/tests/common/custom_requester.rs index 2db72e9f..99fd4385 100644 --- a/src/garage/tests/common/custom_requester.rs +++ b/src/garage/tests/common/custom_requester.rs @@ -192,10 +192,7 @@ impl<'a> RequestBuilder<'a> { .collect::(); let date = now.format(signature::LONG_DATETIME).to_string(); - all_headers.insert( - signature::payload::X_AMZ_DATE, - HeaderValue::from_str(&date).unwrap(), - ); + all_headers.insert(signature::X_AMZ_DATE, HeaderValue::from_str(&date).unwrap()); all_headers.insert(HOST, HeaderValue::from_str(&host).unwrap()); let body_sha = match self.body_signature { @@ -227,7 +224,7 @@ impl<'a> RequestBuilder<'a> { } }; all_headers.insert( - signature::payload::X_AMZ_CONTENT_SH256, + signature::X_AMZ_CONTENT_SHA256, HeaderValue::from_str(&body_sha).unwrap(), );