aboutsummaryrefslogtreecommitdiff
path: root/src/api
diff options
context:
space:
mode:
authorAlex Auvolat <lx@deuxfleurs.fr>2025-02-16 18:25:35 +0100
committerAlex Auvolat <lx@deuxfleurs.fr>2025-02-16 18:25:35 +0100
commit44a896f9b50680a1fdaeb6aaad18a6bf07e9f3c3 (patch)
tree78adb23edbfefcc378eb19ecbf4eb38564d28ed6 /src/api
parentcee7560fc1c3e885dc80dfee233211f54ac9db7d (diff)
downloadgarage-44a896f9b50680a1fdaeb6aaad18a6bf07e9f3c3.tar.gz
garage-44a896f9b50680a1fdaeb6aaad18a6bf07e9f3c3.zip
api: add logic to parse x-amz-content-sha256
Diffstat (limited to 'src/api')
-rw-r--r--src/api/common/signature/checksum.rs2
-rw-r--r--src/api/common/signature/mod.rs51
-rw-r--r--src/api/common/signature/payload.rs90
-rw-r--r--src/api/common/signature/streaming.rs41
-rw-r--r--src/api/k2v/api_server.rs4
-rw-r--r--src/api/k2v/error.rs4
-rw-r--r--src/api/s3/api_server.rs11
7 files changed, 136 insertions, 67 deletions
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<Sha256>;
// 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<TrailerHeader>,
- algorithm: Option<SigningAlgorithm>,
- },
+ Sha256Hash(Hash),
+ StreamingPayload { trailer: bool, signed: bool },
}
-pub enum SigningAlgorithm {
- AwsHmacSha256,
-}
+// ---- top-level functions ----
-pub enum TrailerHeader {
- XAmzChecksumCrc32,
- XAmzChecksumCrc32c,
- XAmzChecksumCrc64Nvme,
+pub struct VerifiedRequest {
+ pub request: Request<streaming::ReqBody>,
+ pub access_key: Key,
+ pub content_sha256_header: ContentSha256Header,
+ // TODO: oneshot chans to retrieve hashes after reading all body
}
-// ---- top-level functions ----
-
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,
+) -> Result<VerifiedRequest, Error> {
+ let checked_signature = payload::check_payload_signature(&garage, &mut req, service).await?;
+ eprintln!("checked signature: {:?}", checked_signature);
+
+ 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<Key>,
+ pub content_sha256_header: ContentSha256Header,
+ pub signature_header: Option<String>,
+}
+
pub async fn check_payload_signature(
garage: &Garage,
request: &mut Request<IncomingBody>,
service: &'static str,
-) -> Result<(Option<Key>, Option<Hash>), Error> {
+) -> Result<CheckedSignature, Error> {
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<ContentSha256Header, Error> {
+ 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<IncomingBody>,
query: QueryMap,
-) -> Result<(Option<Key>, Option<Hash>), Error> {
+) -> Result<CheckedSignature, Error> {
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<IncomingBody>,
mut query: QueryMap,
-) -> Result<(Option<Key>, Option<Hash>), Error> {
+) -> Result<CheckedSignature, Error> {
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<QueryMap, Error> {
@@ -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<Error>;
pub fn parse_streaming_body(
- api_key: &Key,
req: Request<IncomingBody>,
- content_sha256: &mut Option<Hash>,
+ checked_signature: &CheckedSignature,
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")?;
+ 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> = 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 => {