Unverified Commit 8773a704 authored by John DiSanti's avatar John DiSanti Committed by GitHub
Browse files

Simplify event stream message signer configuration (#2671)

## Motivation and Context

This PR creates a `DeferredSigner` implementation that allows for the
event stream message signer to be wired up by the signing implementation
later in the request lifecycle rather than by adding an event stream
signer method to the config.

Refactoring this brings the middleware client implementation closer to
how the orchestrator implementation will work, which unblocks the work
required to make event streams work in the orchestrator.

----

_By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice._
parent d083c6f2
Loading
Loading
Loading
Loading
+16 −0
Original line number Diff line number Diff line
@@ -34,3 +34,19 @@ message = "`SsoCredentialsProvider`, `AssumeRoleProvider`, and `WebIdentityToken
references = ["smithy-rs#2720"]
meta = { "breaking" = false, "tada" = false, "bug" = true }
author = "ysaito1001"

[[smithy-rs]]
message = """
<details>
<summary>Breaking change in how event stream signing works (click to expand more details)</summary>

This change will only impact you if you are wiring up their own event stream signing/authentication scheme. If you're using `aws-sig-auth` to use AWS SigV4 event stream signing, then this change will **not** impact you.

Previously, event stream signing was configured at codegen time by placing a `new_event_stream_signer` method on the `Config`. This function was called at serialization time to connect the signer to the streaming body. Now, instead, a special `DeferredSigner` is wired up at serialization time that relies on a signing implementation to be sent on a channel by the HTTP request signer. To do this, a `DeferredSignerSender` must be pulled out of the property bag, and its `send()` method called with the desired event stream signing implementation.

See the changes in https://github.com/awslabs/smithy-rs/pull/2671 for an example of how this was done for SigV4.
</details>
"""
references = ["smithy-rs#2671"]
meta = { "breaking" = true, "tada" = false, "bug" = false, "target" = "client" }
author = "jdisanti"
+114 −1
Original line number Diff line number Diff line
@@ -3,6 +3,9 @@
 * SPDX-License-Identifier: Apache-2.0
 */

// TODO(enableNewSmithyRuntime): Remove this blanket allow once the old implementations are deleted
#![allow(deprecated)]

use crate::middleware::Signature;
use aws_credential_types::Credentials;
use aws_sigv4::event_stream::{sign_empty_message, sign_message};
@@ -15,6 +18,115 @@ use std::time::SystemTime;

/// Event Stream SigV4 signing implementation.
#[derive(Debug)]
pub struct SigV4MessageSigner {
    last_signature: String,
    credentials: Credentials,
    signing_region: SigningRegion,
    signing_service: SigningService,
    time: Option<SystemTime>,
}

impl SigV4MessageSigner {
    pub fn new(
        last_signature: String,
        credentials: Credentials,
        signing_region: SigningRegion,
        signing_service: SigningService,
        time: Option<SystemTime>,
    ) -> Self {
        Self {
            last_signature,
            credentials,
            signing_region,
            signing_service,
            time,
        }
    }

    fn signing_params(&self) -> SigningParams<()> {
        let mut builder = SigningParams::builder()
            .access_key(self.credentials.access_key_id())
            .secret_key(self.credentials.secret_access_key())
            .region(self.signing_region.as_ref())
            .service_name(self.signing_service.as_ref())
            .time(self.time.unwrap_or_else(SystemTime::now))
            .settings(());
        builder.set_security_token(self.credentials.session_token());
        builder.build().unwrap()
    }
}

impl SignMessage for SigV4MessageSigner {
    fn sign(&mut self, message: Message) -> Result<Message, SignMessageError> {
        let (signed_message, signature) = {
            let params = self.signing_params();
            sign_message(&message, &self.last_signature, &params).into_parts()
        };
        self.last_signature = signature;
        Ok(signed_message)
    }

    fn sign_empty(&mut self) -> Option<Result<Message, SignMessageError>> {
        let (signed_message, signature) = {
            let params = self.signing_params();
            sign_empty_message(&self.last_signature, &params).into_parts()
        };
        self.last_signature = signature;
        Some(Ok(signed_message))
    }
}

#[cfg(test)]
mod tests {
    use crate::event_stream::SigV4MessageSigner;
    use aws_credential_types::Credentials;
    use aws_smithy_eventstream::frame::{HeaderValue, Message, SignMessage};
    use aws_types::region::Region;
    use aws_types::region::SigningRegion;
    use aws_types::SigningService;
    use std::time::{Duration, UNIX_EPOCH};

    fn check_send_sync<T: Send + Sync>(value: T) -> T {
        value
    }

    #[test]
    fn sign_message() {
        let region = Region::new("us-east-1");
        let mut signer = check_send_sync(SigV4MessageSigner::new(
            "initial-signature".into(),
            Credentials::for_tests(),
            SigningRegion::from(region),
            SigningService::from_static("transcribe"),
            Some(UNIX_EPOCH + Duration::new(1611160427, 0)),
        ));
        let mut signatures = Vec::new();
        for _ in 0..5 {
            let signed = signer
                .sign(Message::new(&b"identical message"[..]))
                .unwrap();
            if let HeaderValue::ByteArray(signature) = signed
                .headers()
                .iter()
                .find(|h| h.name().as_str() == ":chunk-signature")
                .unwrap()
                .value()
            {
                signatures.push(signature.clone());
            } else {
                panic!("failed to get the :chunk-signature")
            }
        }
        for i in 1..signatures.len() {
            assert_ne!(signatures[i - 1], signatures[i]);
        }
    }
}

// TODO(enableNewSmithyRuntime): Delete this old implementation that was kept around to support patch releases.
#[deprecated = "use aws_sig_auth::event_stream::SigV4MessageSigner instead (this may require upgrading the smithy-rs code generator)"]
#[derive(Debug)]
/// Event Stream SigV4 signing implementation.
pub struct SigV4Signer {
    properties: SharedPropertyBag,
    last_signature: Option<String>,
@@ -87,8 +199,9 @@ impl SignMessage for SigV4Signer {
    }
}

// TODO(enableNewSmithyRuntime): Delete this old implementation that was kept around to support patch releases.
#[cfg(test)]
mod tests {
mod old_tests {
    use crate::event_stream::SigV4Signer;
    use crate::middleware::Signature;
    use aws_credential_types::Credentials;
+66 −0
Original line number Diff line number Diff line
@@ -20,8 +20,15 @@ use crate::signer::{
    OperationSigningConfig, RequestConfig, SigV4Signer, SigningError, SigningRequirements,
};

#[cfg(feature = "sign-eventstream")]
use crate::event_stream::SigV4MessageSigner as EventStreamSigV4Signer;
#[cfg(feature = "sign-eventstream")]
use aws_smithy_eventstream::frame::DeferredSignerSender;

// TODO(enableNewSmithyRuntime): Delete `Signature` when switching to the orchestrator
/// Container for the request signature for use in the property bag.
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct Signature(String);

impl Signature {
@@ -181,6 +188,22 @@ impl MapRequest for SigV4SigningStage {
                .signer
                .sign(operation_config, &request_config, &creds, &mut req)
                .map_err(SigningStageErrorKind::SigningFailure)?;

            // If this is an event stream operation, set up the event stream signer
            #[cfg(feature = "sign-eventstream")]
            if let Some(signer_sender) = config.get::<DeferredSignerSender>() {
                let time_override = config.get::<SystemTime>().copied();
                signer_sender
                    .send(Box::new(EventStreamSigV4Signer::new(
                        signature.as_ref().into(),
                        creds,
                        request_config.region.clone(),
                        request_config.service.clone(),
                        time_override,
                    )) as _)
                    .expect("failed to send deferred signer");
            }

            config.insert(signature);
            Ok(req)
        })
@@ -234,6 +257,49 @@ mod test {
        assert!(signature.is_some());
    }

    #[cfg(feature = "sign-eventstream")]
    #[test]
    fn sends_event_stream_signer_for_event_stream_operations() {
        use crate::event_stream::SigV4MessageSigner as EventStreamSigV4Signer;
        use aws_smithy_eventstream::frame::{DeferredSigner, SignMessage};
        use std::time::SystemTime;

        let (mut deferred_signer, deferred_signer_sender) = DeferredSigner::new();
        let req = http::Request::builder()
            .uri("https://test-service.test-region.amazonaws.com/")
            .body(SdkBody::from(""))
            .unwrap();
        let region = Region::new("us-east-1");
        let req = operation::Request::new(req)
            .augment(|req, properties| {
                properties.insert(region.clone());
                properties.insert::<SystemTime>(UNIX_EPOCH + Duration::new(1611160427, 0));
                properties.insert(SigningService::from_static("kinesis"));
                properties.insert(OperationSigningConfig::default_config());
                properties.insert(Credentials::for_tests());
                properties.insert(SigningRegion::from(region.clone()));
                properties.insert(deferred_signer_sender);
                Result::<_, Infallible>::Ok(req)
            })
            .expect("succeeds");

        let signer = SigV4SigningStage::new(SigV4Signer::new());
        let _ = signer.apply(req).unwrap();

        let mut signer_for_comparison = EventStreamSigV4Signer::new(
            // This is the expected SigV4 signature for the HTTP request above
            "abac477b4afabf5651079e7b9a0aa6a1a3e356a7418a81d974cdae9d4c8e5441".into(),
            Credentials::for_tests(),
            SigningRegion::from(region),
            SigningService::from_static("kinesis"),
            Some(UNIX_EPOCH + Duration::new(1611160427, 0)),
        );

        let expected_signed_empty = signer_for_comparison.sign_empty().unwrap().unwrap();
        let actual_signed_empty = deferred_signer.sign_empty().unwrap().unwrap();
        assert_eq!(expected_signed_empty, actual_signed_empty);
    }

    // check that the endpoint middleware followed by signing middleware produce the expected result
    #[test]
    fn endpoint_plus_signer() {
+1 −1
Original line number Diff line number Diff line
@@ -3,7 +3,6 @@
 * SPDX-License-Identifier: Apache-2.0
 */

use crate::middleware::Signature;
use aws_credential_types::Credentials;
use aws_sigv4::http_request::{
    sign, PayloadChecksumKind, PercentEncodingMode, SessionTokenMode, SignableRequest,
@@ -15,6 +14,7 @@ use aws_types::SigningService;
use std::fmt;
use std::time::{Duration, SystemTime};

use crate::middleware::Signature;
pub use aws_sigv4::http_request::SignableBody;
pub type SigningError = aws_sigv4::http_request::SigningError;

+2 −2
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegen
import software.amazon.smithy.rust.codegen.client.smithy.generators.client.FluentClientCustomization
import software.amazon.smithy.rust.codegen.client.smithy.generators.client.FluentClientSection
import software.amazon.smithy.rust.codegen.client.smithy.generators.protocol.MakeOperationGenerator
import software.amazon.smithy.rust.codegen.client.smithy.protocols.ClientHttpBoundProtocolPayloadGenerator
import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter
import software.amazon.smithy.rust.codegen.core.rustlang.Writable
import software.amazon.smithy.rust.codegen.core.rustlang.docs
@@ -34,7 +35,6 @@ import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
import software.amazon.smithy.rust.codegen.core.smithy.customize.OperationCustomization
import software.amazon.smithy.rust.codegen.core.smithy.customize.OperationSection
import software.amazon.smithy.rust.codegen.core.smithy.protocols.HttpBoundProtocolPayloadGenerator
import software.amazon.smithy.rust.codegen.core.util.cloneOperation
import software.amazon.smithy.rust.codegen.core.util.expectTrait
import software.amazon.smithy.rust.codegen.core.util.hasTrait
@@ -179,7 +179,7 @@ class AwsInputPresignedMethod(
        MakeOperationGenerator(
            codegenContext,
            protocol,
            HttpBoundProtocolPayloadGenerator(codegenContext, protocol),
            ClientHttpBoundProtocolPayloadGenerator(codegenContext, protocol),
            // Prefixed with underscore to avoid colliding with modeled functions
            functionName = makeOperationFn,
            public = false,
Loading