Unverified Commit 27e00dd0 authored by Russell Cohen's avatar Russell Cohen Committed by GitHub
Browse files

SigV4 Signing Middleware (#193)

* wip

* Add SigV4 Signing Middleware

Utilizing `MapRequest` and the `Credentials` machinery, this diff adds SigV4SigningStage, a middleware stage that can sign `SigV4Requests`.

The signing behavior is driven by several fields that may be present in the property bag.
parent 5b4515c4
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -94,6 +94,8 @@ impl Error for CredentialsError {
    }
}

pub type CredentialsProvider = Arc<dyn ProvideCredentials>;

/// A Credentials Provider
///
/// This interface is intentionally NOT async. Credential providers should provide a separate
+21 −0
Original line number Diff line number Diff line
[package]
name = "aws-sig-auth"
version = "0.1.0"
authors = ["Russell Cohen <rcoh@amazon.com>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
http = "0.2.2"
# Renaming to clearly indicate that this is not a permanent signing solution
aws-sigv4-poc = { package = "aws-sigv4", git = "https://github.com/rcoh/sigv4", rev = "05f90abc02a868cb570ed3006d950947cc0898b0" }
aws-auth = { path = "../aws-auth" }
aws-types = { path = "../aws-types" }
smithy-http = { path = "../../../rust-runtime/smithy-http" }
# Trying this out as an experiment. thiserror can be removed and replaced with hand written error
# implementations and it is not a breaking change.
thiserror = "1"

[dev-dependencies]
aws-endpoint = { path = "../aws-endpoint" }
+6 −0
Original line number Diff line number Diff line
//! AWS Signature Authentication Package
//!
//! In the future, additional signature algorithms can be enabled as Cargo Features.

pub mod middleware;
pub mod signer;
+187 −0
Original line number Diff line number Diff line
/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0.
 */

use crate::signer::{OperationSigningConfig, RequestConfig, SigV4Signer, SigningError};
use aws_auth::{Credentials, CredentialsError, CredentialsProvider};
use aws_types::{SigningRegion, SigningService};
use smithy_http::middleware::MapRequest;
use smithy_http::operation::Request;
use smithy_http::property_bag::PropertyBag;
use std::time::SystemTime;

/// Middleware stage to sign requests with SigV4
///
/// SigV4RequestSignerStage will load configuration from the request property bag and add
/// a signature.
///
/// Prior to signing, the following fields MUST be present in the property bag:
/// - [`SigningRegion`](SigningRegion): The region used when signing the request, eg. `us-east-1`
/// - [`SigningService`](SigningService): The name of the service to use when signing the request, eg. `dynamodb`
/// - [`CredentialsProvider`](CredentialsProvider): A credentials provider to retrieve credentials
/// - [`OperationSigningConfig`](OperationSigningConfig): Operation specific signing configuration, eg.
///   changes to URL encoding behavior, or headers that must be omitted.
/// If any of these fields are missing, the middleware will return an error.
///
/// The following fields MAY be present in the property bag:
/// - [`SystemTime`](SystemTime): The timestamp to use when signing the request. If this field is not present
///   [`SystemTime::now`](SystemTime::now) will be used.
#[derive(Clone)]
pub struct SigV4SigningStage {
    signer: SigV4Signer,
}

impl SigV4SigningStage {
    pub fn new(signer: SigV4Signer) -> Self {
        Self { signer }
    }
}

use thiserror::Error;

#[derive(Debug, Error)]
pub enum SigningStageError {
    #[error("No credentials provider in the property bag")]
    MissingCredentialsProvider,
    #[error("No signing region in the property bag")]
    MissingSigningRegion,
    #[error("No signing service in the property bag")]
    MissingSigningService,
    #[error("No signing configuration in the property bag")]
    MissingSigningConfig,
    #[error("The request body could not be signed by this configuration")]
    InvalidBodyType,
    #[error("Signing failed")]
    SigningFailure(#[from] SigningError),
    #[error("Failed to load credentials from the credentials provider")]
    CredentialsLoadingError(#[from] CredentialsError),
}

/// Extract a signing config from a [`PropertyBag`](smithy_http::property_bag::PropertyBag)
fn signing_config(
    config: &PropertyBag,
) -> Result<(&OperationSigningConfig, RequestConfig, Credentials), SigningStageError> {
    let operation_config = config
        .get::<OperationSigningConfig>()
        .ok_or(SigningStageError::MissingSigningConfig)?;
    let cred_provider = config
        .get::<CredentialsProvider>()
        .ok_or(SigningStageError::MissingCredentialsProvider)?;
    let creds = cred_provider.credentials()?;
    let region = config
        .get::<SigningRegion>()
        .ok_or(SigningStageError::MissingSigningRegion)?;
    let signing_service = config
        .get::<SigningService>()
        .ok_or(SigningStageError::MissingSigningService)?;
    let request_config = RequestConfig {
        request_ts: config
            .get::<SystemTime>()
            .copied()
            .unwrap_or_else(SystemTime::now),
        region: region.into(),
        service: signing_service,
    };
    Ok((operation_config, request_config, creds))
}

impl MapRequest for SigV4SigningStage {
    type Error = SigningStageError;

    fn apply(&self, req: Request) -> Result<Request, Self::Error> {
        req.augment(|req, config| {
            let (operation_config, request_config, creds) = signing_config(config)?;

            // A short dance is required to extract a signable body from an SdkBody, which
            // amounts to verifying that it a strict body based on a `Bytes` and not a stream.
            // Streams must be signed with a different signing mode. Separate support will be added for
            // this at a later date.
            let (parts, body) = req.into_parts();
            let signable_body = body.bytes().ok_or(SigningStageError::InvalidBodyType)?;
            let mut signable_request = http::Request::from_parts(parts, signable_body);

            self.signer
                .sign(
                    &operation_config,
                    &request_config,
                    &creds,
                    &mut signable_request,
                )
                .map_err(|err| SigningStageError::SigningFailure(err))?;
            let (signed_parts, _) = signable_request.into_parts();
            Ok(http::Request::from_parts(signed_parts, body))
        })
    }
}

#[cfg(test)]
mod test {
    use crate::middleware::{SigV4SigningStage, SigningStageError};
    use crate::signer::{OperationSigningConfig, SigV4Signer};
    use aws_auth::CredentialsProvider;
    use aws_endpoint::{set_endpoint_resolver, AwsEndpointStage, DefaultAwsEndpointResolver};
    use aws_types::Region;
    use http::header::AUTHORIZATION;
    use smithy_http::body::SdkBody;
    use smithy_http::middleware::MapRequest;
    use smithy_http::operation;
    use std::convert::Infallible;
    use std::sync::Arc;
    use std::time::{Duration, UNIX_EPOCH};

    // check that the endpoint middleware followed by signing middleware produce the expected result
    #[test]
    fn endpoint_plus_signer() {
        let provider = Arc::new(DefaultAwsEndpointResolver::for_service("kinesis"));
        let req = http::Request::new(SdkBody::from(""));
        let region = Region::new("us-east-1");
        let req = operation::Request::new(req)
            .augment(|req, conf| {
                conf.insert(region.clone());
                conf.insert(UNIX_EPOCH + Duration::new(1611160427, 0));
                set_endpoint_resolver(provider, conf);
                Result::<_, Infallible>::Ok(req)
            })
            .expect("succeeds");

        let endpoint = AwsEndpointStage;
        let signer = SigV4SigningStage::new(SigV4Signer::new());
        let mut req = endpoint.apply(req).expect("add endpoint should succeed");
        let mut errs = vec![];
        errs.push(
            signer
                .apply(req.try_clone().expect("can clone"))
                .expect_err("no signing config"),
        );
        req.config_mut()
            .insert(OperationSigningConfig::default_config());
        errs.push(
            signer
                .apply(req.try_clone().expect("can clone"))
                .expect_err("no cred provider"),
        );
        let cred_provider: CredentialsProvider =
            Arc::new(aws_auth::Credentials::from_keys("AKIAfoo", "bar", None));
        req.config_mut().insert(cred_provider);
        let req = signer.apply(req).expect("signing succeeded");
        // make sure we got the correct error types in any order
        assert!(errs.iter().all(|el| matches!(
            el,
            SigningStageError::MissingCredentialsProvider | SigningStageError::MissingSigningConfig
        )));

        let (req, _) = req.into_parts();
        assert_eq!(
            req.headers()
                .get("x-amz-date")
                .expect("x-amz-date must be present"),
            "20210120T163347Z"
        );
        let auth_header = req
            .headers()
            .get(AUTHORIZATION)
            .expect("auth header must be present");
        assert_eq!(auth_header, "AWS4-HMAC-SHA256 Credential=AKIAfoo/20210120/us-east-1/kinesis/aws4_request, SignedHeaders=, Signature=bf3af0f70e58cb7f70cc545f1b2e83293b3e9860880bf8aef3fae0f3de324427");
    }
}
+131 −0
Original line number Diff line number Diff line
/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0.
 */

use aws_auth::Credentials;
use aws_types::{SigningRegion, SigningService};
use std::error::Error;
use std::time::SystemTime;

#[derive(Eq, PartialEq, Clone, Copy)]
pub enum SigningAlgorithm {
    SigV4,
}

#[derive(Eq, PartialEq, Clone, Copy)]
pub enum HttpSignatureType {
    /// A signature for a full http request should be computed, with header updates applied to the signing result.
    HttpRequestHeaders,
    /* Currently Unsupported
    /// A signature for a full http request should be computed, with query param updates applied to the signing result.
    ///
    /// This is typically used for presigned URLs & is currently unsupported.
    HttpRequestQueryParams,
     */
}

/// Signing Configuration for an Operation
///
/// Although these fields MAY be customized on a per request basis, they are generally static
/// for a given operation
#[derive(Clone, PartialEq, Eq)]
pub struct OperationSigningConfig {
    pub algorithm: SigningAlgorithm,
    pub signature_type: HttpSignatureType,
    pub signing_options: SigningOptions,
}

impl OperationSigningConfig {
    /// Placeholder method to provide a the signing configuration used for most operation
    ///
    /// In the future, we will code-generate a default configuration for each service
    pub fn default_config() -> Self {
        OperationSigningConfig {
            algorithm: SigningAlgorithm::SigV4,
            signature_type: HttpSignatureType::HttpRequestHeaders,
            signing_options: SigningOptions { _private: () },
        }
    }
}

#[derive(Clone, Eq, PartialEq)]
pub struct SigningOptions {
    _private: (),
    /*
    Currently unsupported:
    pub double_uri_encode: bool,
    pub normalize_uri_path: bool,
    pub omit_session_token: bool,
     */
}

/// Signing Configuration for an individual Request
///
/// These fields may vary on a per-request basis
#[derive(Clone, PartialEq, Eq)]
pub struct RequestConfig<'a> {
    pub request_ts: SystemTime,
    pub region: &'a SigningRegion,
    pub service: &'a SigningService,
}

#[derive(Clone)]
pub struct SigV4Signer {
    // In the future, the SigV4Signer will use the CRT signer. This will require constructing
    // and holding an instance of the signer, so prevent people from constructing a SigV4Signer without
    // going through the constructor.
    _private: (),
}

pub type SigningError = Box<dyn Error + Send + Sync>;

impl SigV4Signer {
    pub fn new() -> Self {
        SigV4Signer { _private: () }
    }

    /// Sign a request using the SigV4 Protocol
    ///
    /// Although the direct signing implementation MAY be used directly. End users will not typically
    /// interact with this code. It is generally used via middleware in the request pipeline. See [`SigV4SigningStage`](crate::middleware::SigV4SigningStage).
    pub fn sign<B>(
        &self,
        // There is currently only 1 way to sign, so operation level configuration is unused
        _operation_config: &OperationSigningConfig,
        request_config: &RequestConfig<'_>,
        credentials: &Credentials,
        request: &mut http::Request<B>,
    ) -> Result<(), SigningError>
    where
        B: AsRef<[u8]>,
    {
        let sigv4_creds = aws_sigv4_poc::Credentials {
            access_key: credentials.access_key_id().to_string(),
            secret_key: credentials.secret_access_key().to_string(),
            security_token: credentials.session_token().map(|s| s.to_string()),
        };
        let date = request_config.request_ts;
        for (key, value) in aws_sigv4_poc::sign_core(
            request,
            &sigv4_creds,
            request_config.region.as_ref(),
            request_config.service.as_ref(),
            date,
        ) {
            request
                .headers_mut()
                .append(key.header_name(), value.parse()?);
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}
Loading