Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify event stream message signer configuration #2671

Merged
merged 3 commits into from
May 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.next.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,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"
115 changes: 114 additions & 1 deletion aws/rust-runtime/aws-sig-auth/src/event_stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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>,
Expand Down Expand Up @@ -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;
Expand Down
66 changes: 66 additions & 0 deletions aws/rust-runtime/aws-sig-auth/src/middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
})
Expand Down Expand Up @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion aws/rust-runtime/aws-sig-auth/src/signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import software.amazon.smithy.model.traits.OptionalAuthTrait
import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext
import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator
import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ConfigCustomization
import software.amazon.smithy.rust.codegen.client.smithy.generators.config.EventStreamSigningConfig
import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ServiceConfig
import software.amazon.smithy.rust.codegen.core.rustlang.Writable
import software.amazon.smithy.rust.codegen.core.rustlang.rust
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
Expand Down Expand Up @@ -77,17 +77,17 @@ class SigV4SigningDecorator : ClientCodegenDecorator {
}

class SigV4SigningConfig(
runtimeConfig: RuntimeConfig,
private val runtimeConfig: RuntimeConfig,
private val serviceHasEventStream: Boolean,
private val sigV4Trait: SigV4Trait,
) : EventStreamSigningConfig(runtimeConfig) {
private val codegenScope = arrayOf(
"SigV4Signer" to AwsRuntimeType.awsSigAuthEventStream(runtimeConfig).resolve("event_stream::SigV4Signer"),
)

override fun configImplSection(): Writable {
return writable {
rustTemplate(
) : ConfigCustomization() {
override fun section(section: ServiceConfig): Writable = writable {
if (section is ServiceConfig.ConfigImpl) {
if (serviceHasEventStream) {
// enable the aws-sig-auth `sign-eventstream` feature
addDependency(AwsRuntimeType.awsSigAuthEventStream(runtimeConfig).toSymbol())
}
rust(
"""
/// The signature version 4 service signing name to use in the credential scope when signing requests.
///
Expand All @@ -97,24 +97,7 @@ class SigV4SigningConfig(
${sigV4Trait.name.dq()}
}
""",
*codegenScope,
)
if (serviceHasEventStream) {
rustTemplate(
"#{signerFn:W}",
"signerFn" to
renderEventStreamSignerFn { propertiesName ->
writable {
rustTemplate(
"""
#{SigV4Signer}::new($propertiesName)
""",
*codegenScope,
)
}
},
)
}
}
}
}
Expand Down Expand Up @@ -209,5 +192,3 @@ class SigV4SigningFeature(
}
}
}

fun RuntimeConfig.sigAuth() = awsRuntimeCrate("aws-sig-auth")
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import software.amazon.smithy.rust.codegen.core.testutil.TestWorkspace
import software.amazon.smithy.rust.codegen.core.testutil.compileAndTest
import software.amazon.smithy.rust.codegen.core.testutil.unitTest

internal class SigV4SigningCustomizationTest {
internal class SigV4SigningDecoratorTest {
@Test
fun `generates a valid config`() {
val project = stubConfigProject(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import software.amazon.smithy.rust.codegen.client.smithy.customizations.HttpAuth
import software.amazon.smithy.rust.codegen.client.smithy.customizations.HttpConnectorConfigDecorator
import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator
import software.amazon.smithy.rust.codegen.client.smithy.customize.CombinedClientCodegenDecorator
import software.amazon.smithy.rust.codegen.client.smithy.customize.NoOpEventStreamSigningDecorator
import software.amazon.smithy.rust.codegen.client.smithy.customize.RequiredCustomizations
import software.amazon.smithy.rust.codegen.client.smithy.endpoint.EndpointParamsDecorator
import software.amazon.smithy.rust.codegen.client.smithy.endpoint.EndpointsDecorator
Expand Down Expand Up @@ -62,7 +61,6 @@ class RustClientCodegenPlugin : ClientDecoratableBuildPlugin() {
FluentClientDecorator(),
EndpointsDecorator(),
EndpointParamsDecorator(),
NoOpEventStreamSigningDecorator(),
ApiKeyAuthDecorator(),
HttpAuthDecorator(),
HttpConnectorConfigDecorator(),
Expand Down
Loading