Unverified Commit 937ddfcb authored by Copilot's avatar Copilot Committed by GitHub
Browse files

feat(s3s): Add AWS Signature V2 POST signature support (#358)



* Initial plan

* feat: implement v2_check_post_signature for AWS Signature V2 POST requests

Co-authored-by: default avatarNugine <30099658+Nugine@users.noreply.github.com>

* refactor

---------

Co-authored-by: default avatarcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: default avatarNugine <30099658+Nugine@users.noreply.github.com>
Co-authored-by: default avatarNugine <nugine@foxmail.com>
parent 3c3d3cc8
Loading
Loading
Loading
Loading
+74 −26
Original line number Diff line number Diff line
@@ -6,9 +6,9 @@ use crate::http::{AwsChunkedStream, Body, Multipart};
use crate::http::{OrderedHeaders, OrderedQs};
use crate::protocol::TrailingHeaders;
use crate::sig_v2;
use crate::sig_v2::{AuthorizationV2, PresignedUrlV2};
use crate::sig_v2::{AuthorizationV2, PostSignatureV2, PresignedUrlV2};
use crate::sig_v4;
use crate::sig_v4::PostSignatureInfo;
use crate::sig_v4::PostSignatureV4;
use crate::sig_v4::PresignedUrlV4;
use crate::sig_v4::{AmzContentSha256, AmzDate};
use crate::sig_v4::{AuthorizationV4, CredentialV4};
@@ -89,6 +89,14 @@ fn require_auth(auth: Option<&dyn S3Auth>) -> S3Result<&dyn S3Auth> {

impl SignatureContext<'_> {
    pub async fn check(&mut self) -> S3Result<Option<CredentialsExt>> {
        if self.req_method == Method::POST {
            if let Some(ref mime) = self.mime {
                if mime.type_() == mime::MULTIPART && mime.subtype() == mime::FORM_DATA {
                    return Ok(Some(self.check_post_signature().await?));
                }
            }
        }

        if let Some(result) = self.v2_check().await {
            debug!("checked signature v2");
            return Ok(Some(result?));
@@ -103,17 +111,37 @@ impl SignatureContext<'_> {
    }

    #[tracing::instrument(skip(self))]
    pub async fn v4_check(&mut self) -> Option<S3Result<CredentialsExt>> {
        // POST auth
        if self.req_method == Method::POST {
            if let Some(ref mime) = self.mime {
                if mime.type_() == mime::MULTIPART && mime.subtype() == mime::FORM_DATA {
                    debug!("checking post signature");
                    return Some(self.v4_check_post_signature().await);
    async fn check_post_signature(&mut self) -> S3Result<CredentialsExt> {
        let multipart = {
            let mime = self.mime.as_ref().unwrap(); // assume: multipart

            let boundary = mime
                .get_param(mime::BOUNDARY)
                .ok_or_else(|| invalid_request!("missing boundary"))?;

            let body = mem::take(self.req_body);
            http::transform_multipart(body, boundary.as_str().as_bytes())
                .await
                .map_err(|e| s3_error!(e, MalformedPOSTRequest))?
        };

        debug!(?multipart);

        if multipart.find_field_value("x-amz-signature").is_some() {
            debug!("checking post signature v4");
            return self.v4_check_post_signature(multipart).await;
        }

        if multipart.find_field_value("signature").is_some() {
            debug!("checking post signature v2");
            return self.v2_check_post_signature(multipart).await;
        }

        Err(invalid_request!("unsupported post signature"))
    }

    #[tracing::instrument(skip(self))]
    pub async fn v4_check(&mut self) -> Option<S3Result<CredentialsExt>> {
        // query auth
        if let Some(qs) = self.qs {
            if qs.has("X-Amz-Signature") {
@@ -131,23 +159,10 @@ impl SignatureContext<'_> {
        None
    }

    pub async fn v4_check_post_signature(&mut self) -> S3Result<CredentialsExt> {
    pub async fn v4_check_post_signature(&mut self, multipart: Multipart) -> S3Result<CredentialsExt> {
        let auth = require_auth(self.auth)?;

        let multipart = {
            let mime = self.mime.as_ref().unwrap(); // assume: multipart

            let boundary = mime
                .get_param(mime::BOUNDARY)
                .ok_or_else(|| invalid_request!("missing boundary"))?;

            let body = mem::take(self.req_body);
            http::transform_multipart(body, boundary.as_str().as_bytes())
                .await
                .map_err(|e| s3_error!(e, MalformedPOSTRequest))?
        };

        let info = PostSignatureInfo::extract(&multipart).ok_or_else(|| invalid_request!("missing required multipart fields"))?;
        let info = PostSignatureV4::extract(&multipart).ok_or_else(|| invalid_request!("missing required multipart fields"))?;

        if is_base64_encoded(info.policy.as_bytes()).not() {
            return Err(invalid_request!("invalid field: policy"));
@@ -446,6 +461,7 @@ impl SignatureContext<'_> {

    #[tracing::instrument(skip(self))]
    pub async fn v2_check(&mut self) -> Option<S3Result<CredentialsExt>> {
        // query auth
        if let Some(qs) = self.qs {
            if qs.has("Signature") {
                debug!("checking presigned url");
@@ -453,6 +469,7 @@ impl SignatureContext<'_> {
            }
        }

        // header auth
        if let Some(auth) = self.hs.get_unique(crate::header::AUTHORIZATION) {
            if let Ok(auth) = AuthorizationV2::parse(auth) {
                debug!("checking header auth");
@@ -501,6 +518,37 @@ impl SignatureContext<'_> {
        })
    }

    pub async fn v2_check_post_signature(&mut self, multipart: Multipart) -> S3Result<CredentialsExt> {
        let auth = require_auth(self.auth)?;

        let info = PostSignatureV2::extract(&multipart).ok_or_else(|| invalid_request!("missing required multipart fields"))?;

        if is_base64_encoded(info.policy.as_bytes()).not() {
            return Err(invalid_request!("invalid field: policy"));
        }

        let access_key = info.access_key_id.to_owned();
        let secret_key = auth.get_secret_key(&access_key).await?;

        // For v2 POST signature, the string to sign is the base64-encoded policy
        let string_to_sign = info.policy;
        let signature = sig_v2::calculate_signature(&secret_key, string_to_sign);

        let expected_signature = info.signature;
        if signature != expected_signature {
            debug!(?signature, expected=?expected_signature, "signature mismatch");
            return Err(s3_error!(SignatureDoesNotMatch));
        }

        self.multipart = Some(multipart);
        Ok(CredentialsExt {
            access_key,
            secret_key,
            region: None,
            service: Some("s3".into()),
        })
    }

    pub async fn v2_check_presigned_url(&mut self) -> S3Result<CredentialsExt> {
        let qs = self.qs.unwrap(); // assume: qs has "Signature"
        let presigned_url = PresignedUrlV2::parse(qs).map_err(|err| invalid_request!(err, "missing presigned url v2 fields"))?;
+3 −0
Original line number Diff line number Diff line
@@ -9,5 +9,8 @@ pub use self::authorization_v2::*;
mod presigned_url_v2;
pub use self::presigned_url_v2::*;

mod post_signature_v2;
pub use self::post_signature_v2::*;

mod methods;
pub use self::methods::*;
+20 −0
Original line number Diff line number Diff line
use crate::http::Multipart;

pub struct PostSignatureV2<'a> {
    pub policy: &'a str,
    pub access_key_id: &'a str,
    pub signature: &'a str,
}

impl<'a> PostSignatureV2<'a> {
    pub fn extract(m: &'a Multipart) -> Option<Self> {
        let policy = m.find_field_value("policy")?;
        let access_key_id = m.find_field_value("awsaccesskeyid")?;
        let signature = m.find_field_value("signature")?;
        Some(Self {
            policy,
            access_key_id,
            signature,
        })
    }
}
+2 −2
Original line number Diff line number Diff line
@@ -17,8 +17,8 @@ pub use self::amz_content_sha256::*;
mod amz_date;
pub use self::amz_date::*;

mod post_signature;
pub use self::post_signature::*;
mod post_signature_v4;
pub use self::post_signature_v4::*;

mod methods;
pub use self::methods::*;
+2 −2
Original line number Diff line number Diff line
use crate::http::Multipart;

pub struct PostSignatureInfo<'a> {
pub struct PostSignatureV4<'a> {
    pub policy: &'a str,
    pub x_amz_algorithm: &'a str,
    pub x_amz_credential: &'a str,
@@ -8,7 +8,7 @@ pub struct PostSignatureInfo<'a> {
    pub x_amz_signature: &'a str,
}

impl<'a> PostSignatureInfo<'a> {
impl<'a> PostSignatureV4<'a> {
    pub fn extract(m: &'a Multipart) -> Option<Self> {
        let policy = m.find_field_value("policy")?;
        let x_amz_algorithm = m.find_field_value("x-amz-algorithm")?;