Skip to main content

aws_sigv4/sign/
v4a.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use aws_smithy_runtime_api::client::identity::Identity;
7use bytes::{BufMut, BytesMut};
8use crypto_bigint::{CheckedAdd, CheckedSub, Encoding, U256};
9use ecdsa::hazmat::sign_prehashed_rfc6979;
10use hmac::{digest::FixedOutput, Hmac, KeyInit, Mac};
11use p256::{NistP256, NonZeroScalar};
12use sha2::{Digest as _, Sha256};
13use std::io::Write;
14use std::sync::LazyLock;
15use std::time::SystemTime;
16use zeroize::Zeroizing;
17
18const ALGORITHM: &[u8] = b"AWS4-ECDSA-P256-SHA256";
19/// Size of a P-256 private scalar in bytes.
20const P256_PRIVATE_KEY_SIZE: usize = 32;
21/// Shortest input [`calculate_signature`] accepts, mirroring `elliptic_curve::SecretKey::MIN_SIZE`.
22const P256_PRIVATE_KEY_MIN_SIZE: usize = 24;
23
24static BIG_N_MINUS_2: LazyLock<U256> = LazyLock::new(|| {
25    // The N value from section 3.2.1.3 of https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-186.pdf
26    // Used as the N value for the algorithm described in section A.2.2 of https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-5.pdf
27    // *(Basically a prime number blessed by the NSA for use in p256)*
28    const ORDER: U256 =
29        U256::from_be_hex("ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551");
30    ORDER.checked_sub(&U256::from(2u32)).unwrap()
31});
32
33/// Calculates a Sigv4a signature
34///
35/// `signing_key` is a big-endian P-256 private scalar, as produced by
36/// [`generate_signing_key`].
37///
38/// Shorter input is accepted down to 24 bytes and left-padded with zeroes, which is what the
39/// `SecretKey` this used to go through did.
40///
41/// # Panics
42/// Panics if `signing_key` is not a valid P-256 private key.
43pub fn calculate_signature(signing_key: impl AsRef<[u8]>, string_to_sign: &[u8]) -> String {
44    let signing_key = signing_key.as_ref();
45    assert!(
46        (P256_PRIVATE_KEY_MIN_SIZE..=P256_PRIVATE_KEY_SIZE).contains(&signing_key.len()),
47        "a P-256 private scalar is {P256_PRIVATE_KEY_MIN_SIZE} to {P256_PRIVATE_KEY_SIZE} bytes, got {}",
48        signing_key.len()
49    );
50
51    // Sign from the private scalar rather than from a `SigningKey`. Constructing one derives the
52    // verifying key, and that costs a P-256 scalar multiplication as expensive as the signature
53    // itself while signing never reads it. `NonZeroScalar::from_repr` range-checks the scalar
54    // exactly as `SecretKey` does on the `SigningKey` path -- both reject zero and anything at or
55    // above the group order. `from_repr` does differ in taking a fixed 32 bytes where
56    // `SecretKey::from_slice` takes 24 to 32 and left-pads anything short, so the copy below pads
57    // the same way rather than rejecting short input, which would have changed behaviour for a
58    // caller passing its own key. The length check above is what keeps the two accepted ranges
59    // identical.
60    //
61    // This is what `ecdsa`'s `PrehashSigner for SigningKey` does with that derivation left out,
62    // so the signature is unchanged: same RFC 6979 deterministic nonce, same empty additional
63    // data.
64    //
65    // Both copies this function owns, `repr` and `scalar`, are wiped on the way out; the
66    // `signing_key` slice is the caller's and is left alone. The old code got the wipe for free:
67    // `SigningKey` implements `ZeroizeOnDrop` and its `Drop` zeroizes the secret scalar, whereas
68    // `NonZeroScalar` implements `Zeroize` but has no `Drop`, so dropping one leaves the scalar
69    // in memory. `Zeroizing` supplies the `Drop` that type is missing. The `FieldBytes` that
70    // `from_repr` takes by value is still an unwiped stack copy; that one is not reachable
71    // through this API.
72    let mut repr = Zeroizing::new([0u8; P256_PRIVATE_KEY_SIZE]);
73    repr[P256_PRIVATE_KEY_SIZE - signing_key.len()..].copy_from_slice(signing_key);
74    let scalar = Zeroizing::new(
75        Option::<NonZeroScalar>::from(NonZeroScalar::from_repr((*repr).into()))
76            .expect("signing key is a valid P-256 private scalar"),
77    );
78
79    let z = Sha256::digest(string_to_sign);
80    let (signature, _recovery_id) = sign_prehashed_rfc6979::<NistP256, Sha256>(&scalar, &z, &[]);
81
82    hex::encode(signature.to_der().as_bytes())
83}
84
85/// Generates a signing key for Sigv4a signing.
86pub fn generate_signing_key(access_key: &str, secret_access_key: &str) -> impl AsRef<[u8]> {
87    // Capacity is the secret access key length plus the length of "AWS4A"
88    let mut input_key = Zeroizing::new(Vec::with_capacity(secret_access_key.len() + 5));
89    write!(input_key, "AWS4A{secret_access_key}").unwrap();
90
91    // Capacity is the access key length plus the counter byte
92    let mut kdf_context = Zeroizing::new(Vec::with_capacity(access_key.len() + 1));
93    let mut counter = Zeroizing::new(1u8);
94    let key = loop {
95        write!(kdf_context, "{access_key}").unwrap();
96        kdf_context.push(*counter);
97
98        let mut fis = ALGORITHM.to_vec();
99        fis.push(0);
100        fis.append(&mut kdf_context);
101        fis.put_i32(256);
102
103        let mut mac =
104            Hmac::<Sha256>::new_from_slice(&input_key).expect("HMAC can take key of any size");
105
106        let mut buf = BytesMut::new();
107        buf.put_i32(1);
108        buf.put_slice(&fis);
109        mac.update(&buf);
110        let k0 = U256::from_be_bytes(mac.finalize_fixed().into());
111
112        // It would be more secure for this to be a constant time comparison, but because this
113        // is for client usage, that's not as big a deal.
114        if k0 <= *BIG_N_MINUS_2 {
115            let pk = k0
116                .checked_add(&U256::ONE)
117                .expect("k0 is always less than U256::MAX");
118            // Return the scalar itself rather than round-tripping it through a `SigningKey`.
119            // Building one derives the verifying key, which costs a P-256 scalar
120            // multiplication that nothing here uses: the only caller hands these bytes to
121            // `calculate_signature`, and the loop's bound on `k0` already makes `pk` a valid
122            // private scalar. Staying in `Zeroizing` also keeps the key out of a plain
123            // `FieldBytes` that would not be wiped on drop.
124            break Zeroizing::new(pk.to_be_bytes());
125        }
126
127        *counter = counter
128            .checked_add(1)
129            .expect("counter will never get to 255");
130    };
131
132    key
133}
134
135/// Parameters to use when signing.
136#[derive(Debug)]
137#[non_exhaustive]
138pub struct SigningParams<'a, S> {
139    /// The identity to use when signing a request
140    pub(crate) identity: &'a Identity,
141
142    /// Region set to sign for.
143    pub(crate) region_set: &'a str,
144    /// Service Name to sign for.
145    ///
146    /// NOTE: Endpoint resolution rules may specify a name that differs from the typical service name.
147    pub(crate) name: &'a str,
148    /// Timestamp to use in the signature (should be `SystemTime::now()` unless testing).
149    pub(crate) time: SystemTime,
150
151    /// Additional signing settings. These differ between HTTP and Event Stream.
152    pub(crate) settings: S,
153}
154
155pub(crate) const ECDSA_256: &str = "AWS4-ECDSA-P256-SHA256";
156
157impl<S> SigningParams<'_, S> {
158    /// Returns the region that will be used to sign SigV4a requests
159    pub fn region_set(&self) -> &str {
160        self.region_set
161    }
162
163    /// Returns the service name that will be used to sign requests
164    pub fn name(&self) -> &str {
165        self.name
166    }
167
168    /// Return the name of the algorithm used to sign requests
169    pub fn algorithm(&self) -> &'static str {
170        ECDSA_256
171    }
172}
173
174impl<'a, S: Default> SigningParams<'a, S> {
175    /// Returns a builder that can create new `SigningParams`.
176    pub fn builder() -> signing_params::Builder<'a, S> {
177        Default::default()
178    }
179}
180
181/// Builder and error for creating [`SigningParams`]
182pub mod signing_params {
183    use super::SigningParams;
184    use aws_smithy_runtime_api::client::identity::Identity;
185    use std::error::Error;
186    use std::fmt;
187    use std::time::SystemTime;
188
189    /// [`SigningParams`] builder error
190    #[derive(Debug)]
191    pub struct BuildError {
192        reason: &'static str,
193    }
194    impl BuildError {
195        fn new(reason: &'static str) -> Self {
196            Self { reason }
197        }
198    }
199
200    impl fmt::Display for BuildError {
201        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202            write!(f, "{}", self.reason)
203        }
204    }
205
206    impl Error for BuildError {}
207
208    /// Builder that can create new [`SigningParams`]
209    #[derive(Debug, Default)]
210    pub struct Builder<'a, S> {
211        identity: Option<&'a Identity>,
212        region_set: Option<&'a str>,
213        name: Option<&'a str>,
214        time: Option<SystemTime>,
215        settings: Option<S>,
216    }
217
218    impl<'a, S> Builder<'a, S> {
219        builder_methods!(
220            set_identity,
221            identity,
222            &'a Identity,
223            "Sets the identity (required)",
224            set_region_set,
225            region_set,
226            &'a str,
227            "Sets the region set (required)",
228            set_name,
229            name,
230            &'a str,
231            "Sets the name (required)",
232            set_time,
233            time,
234            SystemTime,
235            "Sets the time to be used in the signature (required)",
236            set_settings,
237            settings,
238            S,
239            "Sets additional signing settings (required)"
240        );
241
242        /// Builds an instance of [`SigningParams`]. Will yield a [`BuildError`] if
243        /// a required argument was not given.
244        pub fn build(self) -> Result<SigningParams<'a, S>, BuildError> {
245            Ok(SigningParams {
246                identity: self
247                    .identity
248                    .ok_or_else(|| BuildError::new("identity is required"))?,
249                region_set: self
250                    .region_set
251                    .ok_or_else(|| BuildError::new("region_set is required"))?,
252                name: self
253                    .name
254                    .ok_or_else(|| BuildError::new("name is required"))?,
255                time: self
256                    .time
257                    .ok_or_else(|| BuildError::new("time is required"))?,
258                settings: self
259                    .settings
260                    .ok_or_else(|| BuildError::new("settings are required"))?,
261            })
262        }
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::{
269        calculate_signature, generate_signing_key, P256_PRIVATE_KEY_MIN_SIZE, P256_PRIVATE_KEY_SIZE,
270    };
271    use p256::ecdsa::signature::Signer;
272    use p256::ecdsa::{DerSignature, SigningKey};
273    use p256::elliptic_curve::sec1::ToSec1Point;
274
275    /// `elliptic_curve::SecretKey::MIN_SIZE` is a private associated constant, so the 24 in
276    /// [`P256_PRIVATE_KEY_MIN_SIZE`] is a copy this crate cannot reference. Pin both ends of the
277    /// range against the `SigningKey` path: if upstream moves that constant, the length check
278    /// above stops matching what `SecretKey::from_slice` accepts, and this fails rather than
279    /// silently diverging.
280    #[test]
281    fn the_accepted_length_range_matches_secret_key() {
282        let key = generate_signing_key("AKIDEXAMPLE", "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY");
283        let full = key.as_ref();
284
285        assert!(
286            SigningKey::from_slice(&full[..P256_PRIVATE_KEY_MIN_SIZE]).is_ok(),
287            "{P256_PRIVATE_KEY_MIN_SIZE} bytes should be the shortest accepted key"
288        );
289        assert!(
290            SigningKey::from_slice(&full[..P256_PRIVATE_KEY_MIN_SIZE - 1]).is_err(),
291            "one byte below {P256_PRIVATE_KEY_MIN_SIZE} should be refused"
292        );
293
294        let too_long = [full, full].concat();
295        assert!(
296            SigningKey::from_slice(&too_long[..P256_PRIVATE_KEY_SIZE + 1]).is_err(),
297            "one byte above {P256_PRIVATE_KEY_SIZE} should be refused"
298        );
299    }
300
301    /// `SecretKey::from_slice` accepts 24 to 32 bytes and left-pads anything short, so the
302    /// `SigningKey` path signed a short key rather than rejecting it. Signing from the scalar
303    /// takes a fixed 32 bytes, so this function pads instead. Every accepted length has to still
304    /// produce what the old path produced, since a caller may pass its own key.
305    #[test]
306    fn short_keys_match_the_signing_key_path() {
307        let key = generate_signing_key("AKIDEXAMPLE", "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY");
308        let string_to_sign = b"AWS4-ECDSA-P256-SHA256\n20260101T000000Z";
309
310        for len in P256_PRIVATE_KEY_MIN_SIZE..=P256_PRIVATE_KEY_SIZE {
311            let short = &key.as_ref()[..len];
312            let expected: DerSignature = SigningKey::from_slice(short)
313                .expect("a left-padded short key is a valid P-256 private key")
314                .sign(string_to_sign);
315            assert_eq!(
316                hex::encode(expected.as_bytes()),
317                calculate_signature(short, string_to_sign),
318                "signature diverged for a {len}-byte key"
319            );
320        }
321    }
322
323    const CREDENTIALS: &[(&str, &str)] = &[
324        (
325            "AKIAIOSFODNN7EXAMPLE",
326            "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
327        ),
328        ("AKIDEXAMPLE", "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"),
329        (
330            "AKIAIOSFODNN7EXAMPLF",
331            "je7MtGbClwBF/2Zp9Utk/h3yCo8nvbEXAMPLEKEY",
332        ),
333    ];
334
335    const STRINGS_TO_SIGN: &[&[u8]] = &[
336        b"",
337        b"AWS4-ECDSA-P256-SHA256\n20260101T000000Z\n20260101/lambda/aws4_request\n\
338          a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90",
339        &[0xff; 64],
340    ];
341
342    /// Signing from the private scalar has to agree with signing through a `SigningKey`, byte for
343    /// byte, across every credential and string to sign below. That equality is the whole
344    /// argument for skipping the verifying-key derivation: the only thing dropped is a scalar
345    /// multiplication whose result signing never reads.
346    ///
347    /// It says nothing about the derivation itself, since both sides start from the same
348    /// `generate_signing_key` output and a change there moves both;
349    /// `derived_key_matches_the_published_test_vector` is what pins that. Nor does it cover short
350    /// keys, which `short_keys_match_the_signing_key_path` does.
351    #[test]
352    fn signature_matches_the_signing_key_path() {
353        for (access_key, secret_key) in CREDENTIALS {
354            let key = generate_signing_key(access_key, secret_key);
355            for string_to_sign in STRINGS_TO_SIGN {
356                let expected: DerSignature = SigningKey::from_slice(key.as_ref())
357                    .expect("derived key is a valid P-256 private key")
358                    .sign(string_to_sign);
359                assert_eq!(
360                    hex::encode(expected.as_bytes()),
361                    calculate_signature(&key, string_to_sign),
362                    "signature diverged for access key {access_key}"
363                );
364            }
365        }
366    }
367
368    /// Pins the key derivation to the suite's published key.
369    ///
370    /// Every `aws-signing-test-suite/v4a/*/public-key.json` in this crate holds the same public
371    /// key, and the scalar the KDF derives for `AKIDEXAMPLE` is its private half. The X and Y
372    /// asserted below are the values in those files; the private scalar is not published
373    /// anywhere, so it is asserted as the value that derives them.
374    ///
375    /// Nothing else in the crate pins this. The suite tests in `http_request::test` verify each
376    /// signature with a key derived from this same output, so a change to the derivation moves
377    /// both sides and they still pass; that file carries a standing `TODO(sigv4a)` about using
378    /// `public-key.json` as the verifying key instead. A round trip through
379    /// `SigningKey::from_slice` and `to_bytes` cannot pin it either: that pair returns what it is
380    /// given, so it compares the key against itself.
381    #[test]
382    fn derived_key_matches_the_published_test_vector() {
383        let key = generate_signing_key("AKIDEXAMPLE", "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY");
384
385        assert_eq!(
386            "7efc8c0e65a324242818c5a50c891c6060b6a00717b7ba3cbe3c5d765be9259c",
387            hex::encode(key.as_ref()),
388            "the derived signing key changed"
389        );
390
391        let point = SigningKey::from_slice(key.as_ref())
392            .expect("derived key is a valid P-256 private key")
393            .verifying_key()
394            .as_affine()
395            .to_sec1_point(false);
396        assert_eq!(
397            "b6618f6a65740a99e650b33b6b4b5bd0d43b176d721a3edfea7e7d2d56d936b1",
398            hex::encode(point.x().expect("uncompressed point has an x coordinate")),
399            "public key no longer matches the test suite's public-key.json"
400        );
401        assert_eq!(
402            "865ed22a7eadc9c5cb9d2cbaca1b3699139fedc5043dc6661864218330c8e518",
403            hex::encode(point.y().expect("uncompressed point has a y coordinate")),
404            "public key no longer matches the test suite's public-key.json"
405        );
406    }
407}