Support STREAMING-AWS4-HMAC-SHA256-PAYLOAD (#64) #156
3 changed files with 72 additions and 22 deletions
|
@ -23,8 +23,8 @@ use garage_model::version_table::*;
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::s3_xml;
|
use crate::s3_xml;
|
||||||
use crate::signature::streaming::SignedPayloadStream;
|
use crate::signature::streaming::SignedPayloadStream;
|
||||||
use crate::signature::verify_signed_content;
|
|
||||||
use crate::signature::LONG_DATETIME;
|
use crate::signature::LONG_DATETIME;
|
||||||
|
use crate::signature::{compute_scope, verify_signed_content};
|
||||||
|
|
||||||
pub async fn handle_put(
|
pub async fn handle_put(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
|
@ -76,7 +76,16 @@ pub async fn handle_put(
|
||||||
NaiveDateTime::parse_from_str(date, LONG_DATETIME).ok_or_bad_request("Invalid date")?;
|
NaiveDateTime::parse_from_str(date, LONG_DATETIME).ok_or_bad_request("Invalid date")?;
|
||||||
let date: DateTime<Utc> = DateTime::from_utc(date, Utc);
|
let date: DateTime<Utc> = DateTime::from_utc(date, Utc);
|
||||||
|
|
||||||
SignedPayloadStream::new(body, garage.clone(), date, secret_key, signature)?
|
let scope = compute_scope(&date, &garage.config.s3_api.s3_region);
|
||||||
|
let signing_hmac = crate::signature::signing_hmac(
|
||||||
|
&date,
|
||||||
|
secret_key,
|
||||||
|
&garage.config.s3_api.s3_region,
|
||||||
|
"s3",
|
||||||
|
)
|
||||||
|
.ok_or_internal_error("Unable to build signing HMAC")?;
|
||||||
|
|
||||||
|
SignedPayloadStream::new(body, signing_hmac, date, &scope, signature)?
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
.boxed()
|
.boxed()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -23,7 +23,7 @@ pub fn verify_signed_content(expected_sha256: Hash, body: &[u8]) -> Result<(), E
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn signing_hmac(
|
pub fn signing_hmac(
|
||||||
datetime: &DateTime<Utc>,
|
datetime: &DateTime<Utc>,
|
||||||
secret_key: &str,
|
secret_key: &str,
|
||||||
region: &str,
|
region: &str,
|
||||||
|
@ -41,3 +41,7 @@ fn signing_hmac(
|
||||||
let hmac = HmacSha256::new_varkey(&signing_hmac.finalize().into_bytes())?;
|
let hmac = HmacSha256::new_varkey(&signing_hmac.finalize().into_bytes())?;
|
||||||
Ok(hmac)
|
Ok(hmac)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn compute_scope(datetime: &DateTime<Utc>, region: &str) -> String {
|
||||||
|
format!("{}/{}/s3/aws4_request", datetime.format(SHORT_DATE), region,)
|
||||||
|
}
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use futures::prelude::*;
|
use futures::prelude::*;
|
||||||
use futures::task;
|
use futures::task;
|
||||||
use hyper::body::Bytes;
|
use hyper::body::Bytes;
|
||||||
|
|
||||||
use garage_model::garage::Garage;
|
|
||||||
use garage_util::data::Hash;
|
use garage_util::data::Hash;
|
||||||
use hmac::Mac;
|
use hmac::Mac;
|
||||||
|
|
||||||
use super::sha256sum;
|
use super::sha256sum;
|
||||||
use super::HmacSha256;
|
use super::HmacSha256;
|
||||||
use super::{LONG_DATETIME, SHORT_DATE};
|
use super::LONG_DATETIME;
|
||||||
|
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
|
|
||||||
KokaKiwi marked this conversation as resolved
Outdated
|
|||||||
|
@ -21,22 +19,16 @@ const EMPTY_STRING_HEX_DIGEST: &str =
|
||||||
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
||||||
|
|
||||||
fn compute_streaming_payload_signature(
|
fn compute_streaming_payload_signature(
|
||||||
garage: &Garage,
|
|
||||||
signing_hmac: &HmacSha256,
|
signing_hmac: &HmacSha256,
|
||||||
date: DateTime<Utc>,
|
date: DateTime<Utc>,
|
||||||
|
scope: &str,
|
||||||
previous_signature: Hash,
|
previous_signature: Hash,
|
||||||
content_sha256: Hash,
|
content_sha256: Hash,
|
||||||
) -> Result<Hash, Error> {
|
) -> Result<Hash, Error> {
|
||||||
let scope = format!(
|
|
||||||
"{}/{}/s3/aws4_request",
|
|
||||||
date.format(SHORT_DATE),
|
|
||||||
garage.config.s3_api.s3_region
|
|
||||||
);
|
|
||||||
|
|
||||||
let string_to_sign = [
|
let string_to_sign = [
|
||||||
"AWS4-HMAC-SHA256-PAYLOAD",
|
"AWS4-HMAC-SHA256-PAYLOAD",
|
||||||
&date.format(LONG_DATETIME).to_string(),
|
&date.format(LONG_DATETIME).to_string(),
|
||||||
&scope,
|
scope,
|
||||||
&hex::encode(previous_signature),
|
&hex::encode(previous_signature),
|
||||||
EMPTY_STRING_HEX_DIGEST,
|
EMPTY_STRING_HEX_DIGEST,
|
||||||
&hex::encode(content_sha256),
|
&hex::encode(content_sha256),
|
||||||
|
@ -104,6 +96,7 @@ mod payload {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum SignedPayloadStreamError {
|
pub enum SignedPayloadStreamError {
|
||||||
Stream(Error),
|
Stream(Error),
|
||||||
InvalidSignature,
|
InvalidSignature,
|
||||||
|
@ -150,8 +143,8 @@ where
|
||||||
#[pin]
|
#[pin]
|
||||||
stream: S,
|
stream: S,
|
||||||
buf: bytes::BytesMut,
|
buf: bytes::BytesMut,
|
||||||
garage: Arc<Garage>,
|
|
||||||
datetime: DateTime<Utc>,
|
datetime: DateTime<Utc>,
|
||||||
|
scope: String,
|
||||||
signing_hmac: HmacSha256,
|
signing_hmac: HmacSha256,
|
||||||
previous_signature: Hash,
|
previous_signature: Hash,
|
||||||
}
|
}
|
||||||
|
@ -162,20 +155,20 @@ where
|
||||||
{
|
{
|
||||||
pub fn new(
|
pub fn new(
|
||||||
stream: S,
|
stream: S,
|
||||||
garage: Arc<Garage>,
|
signing_hmac: HmacSha256,
|
||||||
datetime: DateTime<Utc>,
|
datetime: DateTime<Utc>,
|
||||||
secret_key: &str,
|
scope: &str,
|
||||||
seed_signature: Hash,
|
seed_signature: Hash,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
let signing_hmac =
|
// let signing_hmac =
|
||||||
super::signing_hmac(&datetime, secret_key, &garage.config.s3_api.s3_region, "s3")
|
// super::signing_hmac(&datetime, secret_key, &garage.config.s3_api.s3_region, "s3")
|
||||||
.ok_or_internal_error("Could not compute signing HMAC")?;
|
// .ok_or_internal_error("Could not compute signing HMAC")?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
stream,
|
stream,
|
||||||
buf: bytes::BytesMut::new(),
|
buf: bytes::BytesMut::new(),
|
||||||
garage,
|
|
||||||
datetime,
|
datetime,
|
||||||
|
scope: scope.into(),
|
||||||
signing_hmac,
|
signing_hmac,
|
||||||
previous_signature: seed_signature,
|
previous_signature: seed_signature,
|
||||||
})
|
})
|
||||||
|
@ -217,6 +210,10 @@ where
|
||||||
None => {
|
None => {
|
||||||
if this.buf.is_empty() {
|
if this.buf.is_empty() {
|
||||||
return Poll::Ready(None);
|
return Poll::Ready(None);
|
||||||
|
} else {
|
||||||
|
return Poll::Ready(Some(Err(SignedPayloadStreamError::message(
|
||||||
|
"Unexpected EOF",
|
||||||
|
))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -238,9 +235,9 @@ where
|
||||||
let data_sha256sum = sha256sum(&data);
|
let data_sha256sum = sha256sum(&data);
|
||||||
|
|
||||||
let expected_signature = compute_streaming_payload_signature(
|
let expected_signature = compute_streaming_payload_signature(
|
||||||
this.garage,
|
|
||||||
this.signing_hmac,
|
this.signing_hmac,
|
||||||
KokaKiwi marked this conversation as resolved
Outdated
trinity-1686a
commented
this bit has an invalid edge case : by cutting the stream just before a new chunk header, an attacker can truncate the file without it being rejected. Getting here (inner stream returns None and this.buf is empy) is either such a truncation, or a call to SignedPayloadStream::poll_next after it returned Ok(Ready(None)) once, which is a contract error ( this bit has an invalid edge case : by cutting the stream just before a new chunk header, an attacker can truncate the file without it being rejected. Getting here (inner stream returns None and this.buf is empy) is either such a truncation, or a call to SignedPayloadStream::poll_next after it returned Ok(Ready(None)) once, which is a contract error ([`Ok(Ready(None)) means that the stream has terminated, and poll_next should not be invoked again`](https://docs.rs/futures/0.2.0/futures/stream/trait.Stream.html#return-value)), so this check and return can be safely removed
|
|||||||
*this.datetime,
|
*this.datetime,
|
||||||
|
this.scope,
|
||||||
*this.previous_signature,
|
*this.previous_signature,
|
||||||
data_sha256sum,
|
data_sha256sum,
|
||||||
)
|
)
|
||||||
|
@ -263,3 +260,43 @@ where
|
||||||
self.stream.size_hint()
|
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);
|
||||||
|
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.as_ref().into())));
|
||||||
|
|
||||||
|
let seed_signature = Hash::default();
|
||||||
|
|
||||||
|
let mut stream =
|
||||||
|
SignedPayloadStream::new(body, signing_hmac, datetime, &scope, seed_signature).unwrap();
|
||||||
|
|
||||||
|
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
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue
This doesn't do a check, it returns the expected signature, so it shouldn't be called
check_*