Support STREAMING-AWS4-HMAC-SHA256-PAYLOAD (#64) #156

Merged
lx merged 11 commits from KokaKiwi/garage:aws4-payload-signing into main 2022-01-17 09:55:31 +00:00
Owner

Closes #64.

The actual plan would be to add another "chunker" wrapping the received body data to handle the chunked payload.

Closes #64. The actual plan would be to add another "chunker" wrapping the received body data to handle the chunked payload.
KokaKiwi added the
scope
s3-api
label 2021-11-17 17:23:03 +00:00
KokaKiwi self-assigned this 2021-11-17 17:23:03 +00:00
KokaKiwi added spent time 2021-11-17 17:23:39 +00:00
8h
KokaKiwi force-pushed aws4-payload-signing from 6b481ef81d to 3cfc90f8c6 2021-11-26 17:52:24 +00:00 Compare
KokaKiwi force-pushed aws4-payload-signing from 3cfc90f8c6 to 6ca9f2b0e9 2021-11-26 18:38:46 +00:00 Compare
KokaKiwi force-pushed aws4-payload-signing from 6ca9f2b0e9 to ed31f42d83 2021-12-06 16:29:31 +00:00 Compare
KokaKiwi force-pushed aws4-payload-signing from ed31f42d83 to b884fa006e 2021-12-07 11:50:42 +00:00 Compare
KokaKiwi force-pushed aws4-payload-signing from b884fa006e to 7f480a3576 2021-12-14 18:19:27 +00:00 Compare
KokaKiwi force-pushed aws4-payload-signing from 7f480a3576 to 064c42c6a0 2021-12-14 18:19:45 +00:00 Compare
KokaKiwi force-pushed aws4-payload-signing from 064c42c6a0 to f91bc329e3 2021-12-15 10:10:40 +00:00 Compare
KokaKiwi changed title from WIP: Support STREAMING-AWS4-HMAC-SHA256-PAYLOAD (#64) to Support STREAMING-AWS4-HMAC-SHA256-PAYLOAD (#64) 2021-12-15 10:10:56 +00:00
KokaKiwi force-pushed aws4-payload-signing from 506ad8768d to 577750418e 2021-12-15 11:49:09 +00:00 Compare
KokaKiwi changed title from Support STREAMING-AWS4-HMAC-SHA256-PAYLOAD (#64) to WIP: Support STREAMING-AWS4-HMAC-SHA256-PAYLOAD (#64) 2021-12-15 11:49:56 +00:00
KokaKiwi force-pushed aws4-payload-signing from 5bc4a47286 to 79022f918c 2021-12-15 12:17:37 +00:00 Compare
KokaKiwi force-pushed aws4-payload-signing from 79022f918c to 44dce6dbe9 2021-12-15 12:29:42 +00:00 Compare
KokaKiwi changed title from WIP: Support STREAMING-AWS4-HMAC-SHA256-PAYLOAD (#64) to Support STREAMING-AWS4-HMAC-SHA256-PAYLOAD (#64) 2021-12-15 12:29:47 +00:00
KokaKiwi deleted spent time 2021-12-15 12:38:52 +00:00
- 8h
lx requested changes 2021-12-15 14:33:20 +00:00
lx left a comment
Owner

Thanks @KokaKiwi for the great work!

Here are some general comments:

  • We should also support streaming signatures for UploadPart. (and maybe check if any other endpoints are concerned)

  • We should change the tests to include Minio-client testing without SSL to check that this works. This consists mostly of removing the fake ssl layer that is currently done using socat in the current tests

See also my comments in the code.

Thanks @KokaKiwi for the great work! Here are some general comments: - We should also support streaming signatures for UploadPart. (and maybe check if any other endpoints are concerned) - We should change the tests to include Minio-client testing without SSL to check that this works. This consists mostly of removing the fake ssl layer that is currently done using socat in the current tests See also my comments in the code.
@ -81,2 +81,4 @@
content_sha256: Option<Hash>,
) -> Result<Response<Body>, Error> {
let content_sha256 =
content_sha256.ok_or_bad_request("Request content hash not signed, aborting.")?;
Owner

Is it mandatory to provide a signed content-sha256 header? I thought rather it was the opposite because it seems to cause issues on other endpoints (e.g. #164)

Is it mandatory to provide a signed content-sha256 header? I thought rather it was the opposite because it seems to cause issues on other endpoints (e.g. #164)
KokaKiwi marked this conversation as resolved
@ -271,0 +306,4 @@
pub struct Header {
pub size: usize,
pub signature: Box<[u8]>,
Owner

Is there a good reason to use Box<[u8]> instead of Vec<u8>? Or maybe it should be a fixed-size array ? (we have our own type for that: FixedBytes32, aka Hash)

Is there a good reason to use `Box<[u8]>` instead of `Vec<u8>`? Or maybe it should be a fixed-size array ? (we have our own type for that: `FixedBytes32`, aka `Hash`)
lx marked this conversation as resolved
@ -271,0 +307,4 @@
pub struct Header {
pub size: usize,
pub signature: Box<[u8]>,
Owner

Is there a good reason to use a boxed slice instead of either 1/ a Vec<u8>, or 2/ a constant-sized slice ? (we have the type Hash which we use everywhere for 32-byte slices, this should be a good place for it too)

Is there a good reason to use a boxed slice instead of either 1/ a `Vec<u8>`, or 2/ a constant-sized slice ? (we have the type `Hash` which we use everywhere for 32-byte slices, this should be a good place for it too)
KokaKiwi marked this conversation as resolved
@ -271,0 +337,4 @@
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Header")
.field("size", &self.size)
.field("signature", &hex::encode(&self.signature))
Owner

If signature was a Hash we would benefit from the custom impl Debug for Hash and we could just derive Debug for Header and not implement it manually

If signature was a `Hash` we would benefit from the custom `impl Debug for Hash` and we could just derive Debug for Header and not implement it manually
lx marked this conversation as resolved
@ -271,0 +338,4 @@
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Header")
.field("size", &self.size)
.field("signature", &hex::encode(&self.signature))
Owner

If signature was of type Hash it would already have its own debug that shows (part of) the bytes in hex, so we could derive Debug instead of having a custom impl.

If `signature` was of type `Hash` it would already have its own debug that shows (part of) the bytes in hex, so we could derive Debug instead of having a custom impl.
KokaKiwi marked this conversation as resolved
@ -271,0 +379,4 @@
}
#[pin_project::pin_project]
struct SignedPayloadChunker<S, E>
Owner

Why is this called a Chunker? As far as I understand, it doesn't give chunks that we control, it just gives a normal stream of (arrays of) bytes

Why is this called a Chunker? As far as I understand, it doesn't give chunks that we control, it just gives a normal stream of (arrays of) bytes
lx marked this conversation as resolved
@ -271,0 +380,4 @@
}
#[pin_project::pin_project]
struct SignedPayloadChunker<S, E>
Owner
  1. Why is this called a Chunker ? The target here is not to create chunks in a specific way but just to process a stream which happens to be made of byte slices. Shouldn't it rather be called a SignedPayloadVerifier or StreamingSignedPayloadVerifier ?

  2. I think this struct and its associated error type (and also the payload submodule) should be moved in signature/streaming.rs

1. Why is this called a Chunker ? The target here is not to create chunks in a specific way but just to process a stream which happens to be made of byte slices. Shouldn't it rather be called a `SignedPayloadVerifier` or `StreamingSignedPayloadVerifier` ? 2. I think this struct and its associated error type (and also the `payload` submodule) should be moved in `signature/streaming.rs`
KokaKiwi marked this conversation as resolved
@ -271,0 +502,4 @@
}
}
struct StreamChunker<S: Stream<Item = Result<Bytes, E>>, E> {
Owner

Is there a good reason to make this generic on error type E ? Most things in Garage aren't generic and just return an error::Error and that's simpler

Is there a good reason to make this generic on error type E ? Most things in Garage aren't generic and just return an `error::Error` and that's simpler
KokaKiwi marked this conversation as resolved
@ -271,0 +504,4 @@
}
}
struct StreamChunker<S: Stream<Item = Result<Bytes, E>>, E> {
Owner

In the current codebase, nothing is generic on the error type, we always use garage_api::error::Error. Is there a reason why StreamChunker needs a generic error type?

In the current codebase, nothing is generic on the error type, we always use `garage_api::error::Error`. Is there a reason why StreamChunker needs a generic error type?
lx marked this conversation as resolved
@ -430,2 +670,4 @@
content_sha256: Option<Hash>,
) -> Result<Response<Body>, Error> {
let content_sha256 =
content_sha256.ok_or_bad_request("Request content hash not signed, aborting.")?;
Owner

When doing #164 make sure this doesn't cause more issues, it's the same kind of errors

When doing #164 make sure this doesn't cause more issues, it's the same kind of errors
KokaKiwi marked this conversation as resolved
@ -0,0 +13,4 @@
const EMPTY_STRING_HEX_DIGEST: &str =
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
pub fn check_streaming_payload_signature(
Owner

This doesn't do a check, it returns the expected signature, so it shouldn't be called check_*

This doesn't do a check, it returns the expected signature, so it shouldn't be called `check_*`
KokaKiwi marked this conversation as resolved
@ -0,0 +39,4 @@
.ok_or_internal_error("Unable to build signing HMAC")?;
hmac.update(string_to_sign.as_bytes());
Hash::try_from(&hmac.finalize().into_bytes()).ok_or_bad_request("Invalid signature")
Owner

If hash::try_from fails here, I think it's an internal error and not a bad request (it can only happen if the hmac doesn't have the good number of bytes, which should never happen)

If `hash::try_from` fails here, I think it's an internal error and not a bad request (it can only happen if the hmac doesn't have the good number of bytes, which should never happen)
KokaKiwi marked this conversation as resolved
trinity-1686a reviewed 2021-12-15 14:53:19 +00:00
@ -0,0 +36,4 @@
]
.join("\n");
let mut hmac = signing_hmac(&date, secret_key, &garage.config.s3_api.s3_region, "s3")

instead of recalculating the signing key from date, secret_key and region each time, couldn't check_streaming_payload_signature take the precomputed signing key? It would cut down on parameters/fieds here and in SignedPayloadChunker, and be (negligeably) faster due to less hmac computation

instead of recalculating the signing key from date, secret_key and region each time, couldn't `check_streaming_payload_signature` take the precomputed signing key? It would cut down on parameters/fieds here and in `SignedPayloadChunker`, and be (negligeably) faster due to less hmac computation
KokaKiwi marked this conversation as resolved
KokaKiwi force-pushed aws4-payload-signing from 44dce6dbe9 to f7cb811943 2022-01-04 12:22:23 +00:00 Compare
KokaKiwi force-pushed aws4-payload-signing from f7cb811943 to 1b8ad53b52 2022-01-04 12:51:29 +00:00 Compare
KokaKiwi requested review from lx 2022-01-04 14:53:22 +00:00
lx approved these changes 2022-01-04 16:57:40 +00:00
lx left a comment
Owner

I think I spotted a logic error, otherwise LGTM. Will merge once we have the smoke test.

I think I spotted a logic error, otherwise LGTM. Will merge once we have the smoke test.
@ -0,0 +217,4 @@
None => {
if this.buf.is_empty() {
return Poll::Ready(None);
}
Owner

What if the payload input stream is interrupted in the middle? Do we take care of exiting the infinite loop and return an error?

What if the payload input stream is interrupted in the middle? Do we take care of exiting the infinite loop and return an error?
KokaKiwi marked this conversation as resolved
KokaKiwi force-pushed aws4-payload-signing from 34c3bac9c2 to b2db927077 2022-01-11 11:09:00 +00:00 Compare
KokaKiwi changed title from Support STREAMING-AWS4-HMAC-SHA256-PAYLOAD (#64) to WIP: Support STREAMING-AWS4-HMAC-SHA256-PAYLOAD (#64) 2022-01-11 14:23:16 +00:00
KokaKiwi changed title from WIP: Support STREAMING-AWS4-HMAC-SHA256-PAYLOAD (#64) to Support STREAMING-AWS4-HMAC-SHA256-PAYLOAD (#64) 2022-01-12 14:58:21 +00:00
KokaKiwi requested review from lx 2022-01-12 14:58:24 +00:00
trinity-1686a reviewed 2022-01-12 18:28:18 +00:00
@ -0,0 +235,4 @@
None => {
if this.buf.is_empty() {
return Poll::Ready(None);
}

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), so this check and return can be safely removed

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
KokaKiwi marked this conversation as resolved
KokaKiwi force-pushed aws4-payload-signing from 4c57fb892b to b2eda2c13e 2022-01-13 15:45:56 +00:00 Compare
KokaKiwi added 1 commit 2022-01-14 13:10:01 +00:00
garage_api(fixup): Verify Content-SHA256 header for multipart upload only when needed.
All checks were successful
continuous-integration/drone/pr Build is passing
11a1f3f6cf
KokaKiwi added a new dependency 2022-01-14 13:21:08 +00:00
lx merged commit b45dcc1925 into main 2022-01-17 09:55:31 +00:00
Sign in to join this conversation.
No reviewers
lx
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference: Deuxfleurs/garage#156
No description provided.