Skip to content

Conversation

@rcoh
Copy link
Collaborator

@rcoh rcoh commented Oct 20, 2025

SigV4 Event Stream Support for Server SDK

Problem

Clients wrap event stream messages in SigV4 envelopes with signature headers (:chunk-signature, :date), but servers couldn't parse these signed messages because they expected the raw event shape, not the envelope.

Solution

Added server-side SigV4 event stream unsigning support that automatically extracts inner messages from signed envelopes while maintaining compatibility with unsigned messages.

Implementation

Type System Changes

Event stream types are wrapped to handle both signed and unsigned messages:

  • Receiver<Events, Error>Receiver<SignedEvent<Events>, SignedEventError<Error>>
  • SignedEvent<T> provides access to both the inner message and signature information
  • SignedEventError<E> wraps both extraction errors and underlying event errors

Runtime Components

SigV4Unmarshaller: Wraps the base event stream unmarshaller to handle SigV4 extraction:

impl<T: UnmarshallMessage> UnmarshallMessage for SigV4Unmarshaller<T> {
    type Output = SignedEvent<T::Output>;
    type Error = SignedEventError<T::Error>;
    
    fn unmarshall(&self, message: &Message) -> Result<...> {
        match extract_signed_message(message) {
            Ok(Signed { message: inner, signature }) => {
                // Process inner message with base unmarshaller
                self.inner.unmarshall(&inner).map(|event| SignedEvent {
                    message: event,
                    signature: Some(signature),
                })
            }
            Ok(Unsigned) => {
                // Process unsigned message directly  
                self.inner.unmarshall(message).map(|event| SignedEvent {
                    message: event,
                    signature: None,
                })
            }
            Err(err) => Ok(SignedEventError::InvalidSignedEvent(err))
        }
    }
}

Code Generation Integration

SigV4EventStreamDecorator:

  • Detects services with @sigv4 trait and event streams
  • Wraps event stream types using SigV4EventStreamSymbolProvider
  • Generates support structures (SignedEvent, SigV4Unmarshaller, etc.)
  • Injects unmarshaller wrapping via HTTP binding customizations

HTTP Binding Customization:

  • Added BeforeCreatingEventStreamReceiver section to HttpBindingGenerator
  • Allows decorators to wrap the unmarshaller before Receiver creation
  • Generates: let unmarshaller = SigV4Unmarshaller::new(unmarshaller);

Usage

Server handlers receive SignedEvent<T> and extract the inner message:

async fn streaming_operation_handler(input: StreamingOperationInput) -> Result<...> {
    let event = input.events.recv().await?;
    if let Some(signed_event) = event {
        let actual_event = &signed_event.message;  // Extract inner message
        let signature_info = &signed_event.signature;  // Access signature if present
        // Process actual_event...
    }
}

Testing

  • Added test_sigv4_signed_event_stream that sends SigV4-wrapped events
  • Verifies both signed and unsigned messages work correctly
  • All existing event stream tests continue to pass

Architecture Benefits

  • Backward Compatible: Unsigned messages work unchanged
  • Type Safe: Compile-time guarantees about message structure
  • Extensible: Pattern can be applied to other authentication schemes
  • Minimal Impact: Only affects services with @sigv4 trait and event streams

Checklist

  • For changes to the smithy-rs codegen or runtime crates, I have created a changelog entry Markdown file in the .changelog directory, specifying "client," "server," or both in the applies_to key.
  • For changes to the AWS SDK, generated SDK code, or SDK runtime crates, I have created a changelog entry Markdown file in the .changelog directory, specifying "aws-sdk-rust" in the applies_to key.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@rcoh rcoh force-pushed the eventstream-signing branch from a97453e to 017463e Compare October 20, 2025 19:09
@github-actions
Copy link

A new generated diff is ready to view.

A new doc preview is ready to view.

@rcoh rcoh marked this pull request as ready for review October 21, 2025 10:36
@rcoh rcoh requested review from a team as code owners October 21, 2025 10:36
@rcoh rcoh force-pushed the eventstream-signing branch from 017463e to f43b21f Compare October 21, 2025 18:03
@github-actions
Copy link

A new generated diff is ready to view.

A new doc preview is ready to view.

@github-actions
Copy link

A new generated diff is ready to view.

A new doc preview is ready to view.

@rcoh rcoh force-pushed the eventstream-signing branch from f0700da to fedbf82 Compare October 22, 2025 15:24
@github-actions
Copy link

A new generated diff is ready to view.

A new doc preview is ready to view.

@rcoh rcoh requested review from drganjoo and jasgin October 22, 2025 15:56
@rcoh rcoh added the server Rust server SDK label Oct 22, 2025
@rcoh rcoh force-pushed the eventstream-signing branch from fedbf82 to 266592d Compare October 24, 2025 18:59
@github-actions
Copy link

A new generated diff is ready to view.

A new doc preview is ready to view.

use aws.auth#sigv4

@rpcv2Cbor
@sigv4(name: "rpcv2-cbor")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an existing test service without @sigv4 to verify the code generator works for unsigned services? If not, perhaps instead of adding this annotation directly to the .smithy file, we could add it as part of a separate test to ensure both signed and unsigned services are covered?

}

override fun symbolProvider(base: RustSymbolProvider): RustSymbolProvider {
// We need access to the service shape to check for SigV4 trait, but the base interface doesn't provide it.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: We can use the function transformModel to detect whether the service uses sigV4 authentication or not.

}

internal fun RustSymbolProvider.usesSigAuth(): Boolean =
ServiceIndex.of(model).getAuthSchemes(moduleProviderContext.serviceShape!!).containsKey(SigV4Trait.ID)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to consider SigV4ATrait as well?

##[derive(Debug, Clone)]
pub struct SignatureInfo {
/// The chunk signature bytes from the `:chunk-signature` header
pub chunk_signature: Vec<u8>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#{Vec} instead of Vec

/// Information extracted from a signed event stream message
##[non_exhaustive]
##[derive(Debug, Clone)]
pub struct SignatureInfo {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume the handler code is responsible for determining whether the signature is sigv4 or sigv4a based by inspecting the initial request headers?

}))
}
#{UnmarshalledMessage}::Error(err) => {
Ok(#{UnmarshalledMessage}::Error(#{SignedEventError}::Event(err)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should SignedEventError::Event include the signature when the error message itself was signed? Currently the signature is being extracted but then discarded for error messages.

match header.name().as_str() {
":chunk-signature" => {
if let #{HeaderValue}::ByteArray(bytes) = header.value() {
chunk_signature = Some(bytes.as_ref().to_vec());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there limits on event stream message/chunk sizes to prevent memory exhaustion attacks? or is this the service's responsibility via Hyper configuration? Are individual header values size-limited? or should we enforce some check here that the value is maximum 256 bytes long before we .to_vec() it?

Err(err) => Err(err),
}
}
Ok(MaybeSignedMessage::Unsigned) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a service has the @sigv4 trait applied to it:

  1. Can a client send unsigned messages?
  2. Does the service handler have to raise an error if it doesn't want to deal with unsigned messages?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

server Rust server SDK

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants