mirror of
https://github.com/deuxfleurs-org/garage.git
synced 2026-08-22 02:36:38 +00:00
80f9335950
## Summary Garage's SigV4 canonical-request builder trims leading/trailing whitespace from signed header values but does not collapse sequential internal whitespace, which the SigV4 spec requires: > Convert sequential spaces to a single space. — https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html AWS SDKs apply this normalization before computing the signature, but transmit the raw value on the wire. The receiver must therefore apply the same normalization when reconstructing the canonical request, otherwise the recomputed hash differs and the request is rejected as `Invalid signature`. Same class of canonicalization-drift bug as #1155 / !1382, but on the canonical-headers axis rather than the canonical-URI axis. ## Reproduction Surfaces in practice with `gitlab-runner`'s S3 cache uploader. I was in the midst of migrating my runner cache from AWS S3 to garage, but I noticed some shared runner caches were no longer uploading. I was using `sha256sum | sha256sum` to compute my cache keys, which leaves a trailing ` -` on the value. Once GitLab appends `-protected` for protected branches the resulting `x-amz-meta-cachekey` header value contains internal sequential whitespace and triggers the mismatch: ``` x-amz-meta-cachekey:php- --protected ^^ two spaces, preserved by Garage ``` Without the fix the included regression test (`test_presigned_put_with_user_metadata`) fails with HTTP 403; with the fix it returns 200. `aws-cli` is unaffected because it signs `Content-Type` rather than user metadata, so the specific code path with whitespace-bearing signed header values isn't exercised. ## Fix In `canonical_request` (`src/api/common/signature/payload.rs`), replace the `.trim()` call on the joined header value with the full SigV4 normalization — `split_whitespace().collect::<Vec<_>>().join(" ")` — which both trims edges and collapses internal runs. ## Tests * New regression test `test_presigned_put_with_user_metadata` covering a presigned PUT whose `x-amz-meta-*` value contains internal sequential whitespace. * Full integration suite passes: `40 passed; 0 failed; 2 ignored`. * `garage_api_common` unit tests pass: `18 passed; 0 failed`. ## Notes * Backwards-compatible: any signature that validated before still validates, because clients are spec-required to collapse on their side; Garage was only rejecting requests where the client had collapsed correctly but Garage hadn't. * No config or migration changes. * Fix applies to both presigned-URL and Authorization-header code paths since they share the canonical-request builder. Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1424 Reviewed-by: Alex <lx@deuxfleurs.fr>
620 lines
20 KiB
Rust
620 lines
20 KiB
Rust
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_util::time::now_msec;
|
|
|
|
use garage_model::garage::Garage;
|
|
use garage_model::key_table::*;
|
|
|
|
use super::*;
|
|
|
|
use crate::encoding::uri_encode;
|
|
|
|
pub type QueryMap = HeaderMap<QueryValue>;
|
|
pub struct QueryValue {
|
|
/// Original key with potential uppercase characters,
|
|
/// for use in signature calculation
|
|
key: String,
|
|
value: String,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct CheckedSignature {
|
|
pub key: Option<Key>,
|
|
pub content_sha256_header: ContentSha256Header,
|
|
pub signature_header: Option<String>,
|
|
}
|
|
|
|
pub fn check_payload_signature(
|
|
garage: &Garage,
|
|
request: &mut Request<IncomingBody>,
|
|
service: &'static str,
|
|
) -> Result<CheckedSignature, Error> {
|
|
let query = parse_query_map(request.uri())?;
|
|
|
|
if query.contains_key(&X_AMZ_ALGORITHM) {
|
|
// We check for presigned-URL-style authentication first, because
|
|
// the browser or something else could inject an Authorization header
|
|
// that is totally unrelated to AWS signatures.
|
|
check_presigned_signature(garage, service, request, query)
|
|
} else if request.headers().contains_key(AUTHORIZATION) {
|
|
check_standard_signature(garage, service, request, query)
|
|
} else {
|
|
// Unsigned (anonymous) request
|
|
let content_sha256 = request
|
|
.headers()
|
|
.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 {
|
|
(false, rest)
|
|
};
|
|
let signed = match algo {
|
|
AWS4_HMAC_SHA256_PAYLOAD => true,
|
|
UNSIGNED_PAYLOAD => false,
|
|
_ => {
|
|
return Err(Error::bad_request(
|
|
"invalid or unsupported x-amz-content-sha256",
|
|
));
|
|
}
|
|
};
|
|
Ok(ContentSha256Header::StreamingPayload { trailer, signed })
|
|
} else {
|
|
let sha256 = hex::decode(header)
|
|
.ok()
|
|
.and_then(|bytes| Hash::try_from(&bytes))
|
|
.ok_or_bad_request("Invalid content sha256 hash")?;
|
|
Ok(ContentSha256Header::Sha256Checksum(sha256))
|
|
}
|
|
}
|
|
|
|
fn check_standard_signature(
|
|
garage: &Garage,
|
|
service: &'static str,
|
|
request: &Request<IncomingBody>,
|
|
query: QueryMap,
|
|
) -> Result<CheckedSignature, 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 (except x-amz-content-sha256)
|
|
// 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())?;
|
|
|
|
let content_sha256_header = parse_x_amz_content_sha256(Some(&authorization.content_sha256))?;
|
|
|
|
Ok(CheckedSignature {
|
|
key: Some(key),
|
|
content_sha256_header,
|
|
signature_header: Some(authorization.signature),
|
|
})
|
|
}
|
|
|
|
fn check_presigned_signature(
|
|
garage: &Garage,
|
|
service: &'static str,
|
|
request: &mut Request<IncomingBody>,
|
|
mut query: QueryMap,
|
|
) -> Result<CheckedSignature, 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 included:
|
|
// - the Host header (mandatory)
|
|
// - all x-amz-* headers used in the request (except x-amz-content-sha256)
|
|
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())?;
|
|
|
|
// 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(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> {
|
|
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() {
|
|
// Enforce signature of some headers
|
|
if header_should_be_signed(name) && !signed_headers.contains(name) {
|
|
return Err(Error::bad_request(format!(
|
|
"Header `{}` should be signed",
|
|
name
|
|
)));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
// Indicates whether a header is required to be signed
|
|
fn header_should_be_signed(name: &HeaderName) -> bool {
|
|
// Enforce signature of all x-amz-* headers, except x-amz-content-sh256
|
|
// because it is included in the canonical request in all cases
|
|
name.as_str().starts_with("x-amz-") && name != X_AMZ_CONTENT_SHA256
|
|
}
|
|
|
|
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 source 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 {
|
|
//TODO: decode is already do for construct Api::EndPoint, should be better to be able to keep it instead of compute it again.
|
|
let key = percent_encoding::percent_decode_str(canonical_uri)
|
|
.decode_utf8()
|
|
.unwrap();
|
|
uri_encode(&key, false).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.
|
|
//
|
|
// Per the SigV4 spec, signed header values must have sequential
|
|
// internal whitespace collapsed to a single space, in addition to
|
|
// being trimmed. AWS SDKs do this before computing the signature
|
|
// but transmit the raw value on the wire, so we must match.
|
|
// -> https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html
|
|
let canonical_header_string = signed_headers
|
|
.iter()
|
|
.map(|name| {
|
|
let all_values = headers.get_all(name);
|
|
let mut iter_values = all_values.iter();
|
|
let base_value = iter_values
|
|
.next()
|
|
.ok_or_bad_request(format!("signed header `{}` is not present", name))?;
|
|
let mut built_string = std::str::from_utf8(base_value.as_bytes())?.to_string();
|
|
for extend_value in iter_values {
|
|
let extend_string = std::str::from_utf8(extend_value.as_bytes())?;
|
|
built_string.push(',');
|
|
built_string.push_str(extend_string);
|
|
}
|
|
let normalized = built_string
|
|
.split_whitespace()
|
|
.collect::<Vec<_>>()
|
|
.join(" ");
|
|
Ok(format!("{}:{}", name.as_str(), normalized))
|
|
})
|
|
.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 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 {
|
|
unexpected: auth.scope.to_string(),
|
|
expected: scope_expected,
|
|
});
|
|
}
|
|
|
|
let key = garage
|
|
.key_table
|
|
.get_local(&EmptyKey, &auth.key_id)?
|
|
.filter(|k| !k.state.is_deleted())
|
|
.ok_or_else(|| Error::forbidden(format!("No such key: {}", &auth.key_id)))?;
|
|
let key_p = key.params().unwrap();
|
|
|
|
if key_p.is_expired(now_msec()) {
|
|
return Err(Error::forbidden(format!(
|
|
"Access key {} has expired",
|
|
key.key_id
|
|
)));
|
|
}
|
|
|
|
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 {
|
|
pub key_id: String,
|
|
scope: String,
|
|
signed_headers: String,
|
|
signature: String,
|
|
content_sha256: String,
|
|
date: DateTime<Utc>,
|
|
}
|
|
|
|
impl Authorization {
|
|
pub 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_SHA256)
|
|
.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")?
|
|
.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 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)
|
|
}
|
|
}
|