aws_sigv4/
event_stream.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Utilities to sign Event Stream messages.
7//!
8//! # Example: Signing an event stream message
9//!
10//! ```rust
11//! use aws_sigv4::event_stream::sign_message;
12//! use aws_smithy_types::event_stream::{Header, HeaderValue, Message};
13//! use std::time::SystemTime;
14//! use aws_credential_types::Credentials;
15//! use aws_smithy_runtime_api::client::identity::Identity;
16//! use aws_sigv4::sign::v4;
17//!
18//! // The `last_signature` argument is the previous message's signature, or
19//! // the signature of the initial HTTP request if a message hasn't been signed yet.
20//! let last_signature = "example298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
21//!
22//! let message_to_sign = Message::new(&b"example"[..]).add_header(Header::new(
23//!     "some-header",
24//!     HeaderValue::String("value".into()),
25//! ));
26//!
27//! let identity = Credentials::new(
28//!     "AKIDEXAMPLE",
29//!     "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
30//!     None,
31//!     None,
32//!     "hardcoded-credentials"
33//! ).into();
34//! let params = v4::SigningParams::builder()
35//!     .identity(&identity)
36//!     .region("us-east-1")
37//!     .name("exampleservice")
38//!     .time(SystemTime::now())
39//!     .settings(())
40//!     .build()
41//!     .unwrap();
42//!
43//! // Use the returned `signature` to sign the next message.
44//! let (signed, signature) = sign_message(&message_to_sign, &last_signature, &params)
45//!     .expect("signing should succeed")
46//!     .into_parts();
47//! ```
48
49use crate::date_time::{format_date, format_date_time, truncate_subsecs};
50use crate::http_request::SigningError;
51use crate::sign::v4::{calculate_signature, generate_signing_key, sha256_hex_string};
52use crate::SigningOutput;
53use aws_credential_types::Credentials;
54use aws_smithy_eventstream::frame::{write_headers_to, write_message_to};
55use aws_smithy_types::event_stream::{Header, HeaderValue, Message};
56use bytes::Bytes;
57use std::io::Write;
58use std::time::SystemTime;
59
60/// Event stream signing parameters
61pub type SigningParams<'a> = crate::sign::v4::SigningParams<'a, ()>;
62
63/// Creates a string to sign for an Event Stream message.
64fn calculate_string_to_sign(
65    message_payload: &[u8],
66    last_signature: &str,
67    time: SystemTime,
68    params: &SigningParams<'_>,
69) -> Vec<u8> {
70    // Event Stream string to sign format is documented here:
71    // https://docs.aws.amazon.com/transcribe/latest/dg/how-streaming.html
72    let date_time_str = format_date_time(time);
73    let date_str = format_date(time);
74
75    let mut sts: Vec<u8> = Vec::new();
76    writeln!(sts, "AWS4-HMAC-SHA256-PAYLOAD").unwrap();
77    writeln!(sts, "{}", date_time_str).unwrap();
78    writeln!(
79        sts,
80        "{}/{}/{}/aws4_request",
81        date_str, params.region, params.name
82    )
83    .unwrap();
84    writeln!(sts, "{}", last_signature).unwrap();
85
86    let date_header = Header::new(":date", HeaderValue::Timestamp(time.into()));
87    let mut date_buffer = Vec::new();
88    write_headers_to(&[date_header], &mut date_buffer).unwrap();
89    writeln!(sts, "{}", sha256_hex_string(&date_buffer)).unwrap();
90    write!(sts, "{}", sha256_hex_string(message_payload)).unwrap();
91    sts
92}
93
94/// Signs an Event Stream message with the given `credentials`.
95///
96/// Each message's signature incorporates the signature of the previous message (`last_signature`).
97/// The very first message incorporates the signature of the top-level request
98/// for both HTTP 2 and WebSocket.
99pub fn sign_message<'a>(
100    message: &'a Message,
101    last_signature: &'a str,
102    params: &'a SigningParams<'a>,
103) -> Result<SigningOutput<Message>, SigningError> {
104    let message_payload = {
105        let mut payload = Vec::new();
106        write_message_to(message, &mut payload).unwrap();
107        payload
108    };
109    sign_payload(Some(message_payload), last_signature, params)
110}
111
112/// Returns a signed empty message
113///
114/// Empty signed event stream messages differ from normal signed event stream
115/// in that the payload is 0-bytes rather than a nested message. There is no way
116/// to create a signed empty message using [`sign_message`].
117pub fn sign_empty_message<'a>(
118    last_signature: &'a str,
119    params: &'a SigningParams<'a>,
120) -> Result<SigningOutput<Message>, SigningError> {
121    sign_payload(None, last_signature, params)
122}
123
124fn sign_payload<'a>(
125    message_payload: Option<Vec<u8>>,
126    last_signature: &'a str,
127    params: &'a SigningParams<'a>,
128) -> Result<SigningOutput<Message>, SigningError> {
129    // Truncate the sub-seconds up front since the timestamp written to the signed message header
130    // needs to exactly match the string formatted timestamp, which doesn't include sub-seconds.
131    let time = truncate_subsecs(params.time);
132    let creds = params
133        .identity
134        .data::<Credentials>()
135        .ok_or_else(SigningError::unsupported_identity_type)?;
136
137    let signing_key =
138        generate_signing_key(creds.secret_access_key(), time, params.region, params.name);
139    let string_to_sign = calculate_string_to_sign(
140        message_payload.as_ref().map(|v| &v[..]).unwrap_or(&[]),
141        last_signature,
142        time,
143        params,
144    );
145    let signature = calculate_signature(signing_key, &string_to_sign);
146    tracing::trace!(canonical_request = ?message_payload, string_to_sign = ?string_to_sign, "calculated signing parameters");
147
148    // Generate the signed wrapper event frame
149    Ok(SigningOutput::new(
150        Message::new(message_payload.map(Bytes::from).unwrap_or_default())
151            .add_header(Header::new(
152                ":chunk-signature",
153                HeaderValue::ByteArray(hex::decode(&signature).unwrap().into()),
154            ))
155            .add_header(Header::new(":date", HeaderValue::Timestamp(time.into()))),
156        signature,
157    ))
158}
159
160#[cfg(test)]
161mod tests {
162    use crate::event_stream::{calculate_string_to_sign, sign_message, SigningParams};
163    use crate::sign::v4::sha256_hex_string;
164    use aws_credential_types::Credentials;
165    use aws_smithy_eventstream::frame::write_message_to;
166    use aws_smithy_types::event_stream::{Header, HeaderValue, Message};
167    use std::time::{Duration, UNIX_EPOCH};
168
169    #[test]
170    fn string_to_sign() {
171        let message_to_sign = Message::new(&b"test payload"[..]).add_header(Header::new(
172            "some-header",
173            HeaderValue::String("value".into()),
174        ));
175        let mut message_payload = Vec::new();
176        write_message_to(&message_to_sign, &mut message_payload).unwrap();
177
178        let params = SigningParams {
179            identity: &Credentials::for_tests().into(),
180            region: "us-east-1",
181            name: "testservice",
182            time: (UNIX_EPOCH + Duration::new(123_456_789_u64, 1234u32)),
183            settings: (),
184        };
185
186        let expected = "\
187            AWS4-HMAC-SHA256-PAYLOAD\n\
188            19731129T213309Z\n\
189            19731129/us-east-1/testservice/aws4_request\n\
190            be1f8c7d79ef8e1abc5254a2c70e4da3bfaf4f07328f527444e1fc6ea67273e2\n\
191            0c0e3b3bf66b59b976181bd7d401927bbd624107303c713fd1e5f3d3c8dd1b1e\n\
192            f2eba0f2e95967ee9fbc6db5e678d2fd599229c0d04b11e4fc8e0f2a02a806c6\
193        ";
194
195        let last_signature = sha256_hex_string(b"last message sts");
196        assert_eq!(
197            expected,
198            std::str::from_utf8(&calculate_string_to_sign(
199                &message_payload,
200                &last_signature,
201                params.time,
202                &params
203            ))
204            .unwrap()
205        );
206    }
207
208    #[test]
209    fn sign() {
210        let message_to_sign = Message::new(&b"test payload"[..]).add_header(Header::new(
211            "some-header",
212            HeaderValue::String("value".into()),
213        ));
214        let params = SigningParams {
215            identity: &Credentials::for_tests().into(),
216            region: "us-east-1",
217            name: "testservice",
218            time: (UNIX_EPOCH + Duration::new(123_456_789_u64, 1234u32)),
219            settings: (),
220        };
221
222        let last_signature = sha256_hex_string(b"last message sts");
223        let (signed, signature) = sign_message(&message_to_sign, &last_signature, &params)
224            .unwrap()
225            .into_parts();
226        assert_eq!(":chunk-signature", signed.headers()[0].name().as_str());
227        if let HeaderValue::ByteArray(bytes) = signed.headers()[0].value() {
228            assert_eq!(signature, hex::encode(bytes));
229        } else {
230            panic!("expected byte array for :chunk-signature header");
231        }
232        assert_eq!(":date", signed.headers()[1].name().as_str());
233        if let HeaderValue::Timestamp(value) = signed.headers()[1].value() {
234            assert_eq!(123_456_789_i64, value.secs());
235            // The subseconds should have been truncated off
236            assert_eq!(0, value.subsec_nanos());
237        } else {
238            panic!("expected timestamp for :date header");
239        }
240    }
241}