1use 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";
19const P256_PRIVATE_KEY_SIZE: usize = 32;
21const P256_PRIVATE_KEY_MIN_SIZE: usize = 24;
23
24static BIG_N_MINUS_2: LazyLock<U256> = LazyLock::new(|| {
25 const ORDER: U256 =
29 U256::from_be_hex("ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551");
30 ORDER.checked_sub(&U256::from(2u32)).unwrap()
31});
32
33pub 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 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
85pub fn generate_signing_key(access_key: &str, secret_access_key: &str) -> impl AsRef<[u8]> {
87 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 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 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 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#[derive(Debug)]
137#[non_exhaustive]
138pub struct SigningParams<'a, S> {
139 pub(crate) identity: &'a Identity,
141
142 pub(crate) region_set: &'a str,
144 pub(crate) name: &'a str,
148 pub(crate) time: SystemTime,
150
151 pub(crate) settings: S,
153}
154
155pub(crate) const ECDSA_256: &str = "AWS4-ECDSA-P256-SHA256";
156
157impl<S> SigningParams<'_, S> {
158 pub fn region_set(&self) -> &str {
160 self.region_set
161 }
162
163 pub fn name(&self) -> &str {
165 self.name
166 }
167
168 pub fn algorithm(&self) -> &'static str {
170 ECDSA_256
171 }
172}
173
174impl<'a, S: Default> SigningParams<'a, S> {
175 pub fn builder() -> signing_params::Builder<'a, S> {
177 Default::default()
178 }
179}
180
181pub 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 #[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 #[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 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 #[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 #[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 #[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 #[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}