From c34377e8c6a74dfeaf9e06fb150d643c4c3c9b51 Mon Sep 17 00:00:00 2001 From: Justin Plock Date: Wed, 3 Jun 2026 22:03:15 +0100 Subject: [PATCH] Add aws-lc-rs and fips features to aws-sigv4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Route SigV4 HMAC-SHA256 / SHA-256 and (when combined with sigv4a) SigV4a ECDSA-P256 through aws-lc-rs (or aws-lc-fips-sys) when the new features are enabled, mirroring the additive __rustls / rustls-aws-lc / rustls-aws-lc-fips pattern in aws-smithy-http-client and the companion PR #4690 for aws-smithy-checksums. The default build is unchanged and keeps the existing hmac / sha2 / p256 (RustCrypto) path; the new features are strictly opt-in. This is the aws-sigv4 half of smithy-rs#4681. With both halves landed, FIPS-conscious customers can route TLS, request checksums, and request signing end-to-end through aws-lc-rs without forking the runtime crates. An internal __aws-lc-rs feature gates every swapped code path; the public aws-lc-rs and fips features each select the aws-lc-rs backend explicitly (aws-lc-sys vs aws-lc-fips-sys). Source-level cfgs ensure that when __aws-lc-rs is active, no RustCrypto cryptographic code path is executed for SigV4 or SigV4a, even when sigv4a is also enabled. For the SigV4a ECDSA path, the existing 32-byte deterministic scalar contract from generate_signing_key is preserved. calculate_signature wraps the scalar in a minimal 51-byte RFC 5915 SEC1 ECPrivateKey DER (publicKey field intentionally omitted) and hands it to EcdsaKeyPair::from_private_key_der, which lets AWS-LC derive the public point internally — no p256 code path is required. Verified on macOS (aarch64-apple-darwin): - cargo test -p aws-sigv4 (default, RustCrypto) 89 ok - cargo test -p aws-sigv4 --features sigv4a 124 ok - cargo test -p aws-sigv4 --features aws-lc-rs 89 ok - cargo test -p aws-sigv4 --features "aws-lc-rs sigv4a" 124 ok - cargo build -p aws-sigv4 --features "fips sigv4a" ok - cargo clippy -p aws-sigv4 --features "aws-lc-rs sigv4a" --all-targets -- -D warnings clean --- .changelog/aws-sigv4-aws-lc-rs.md | 9 + aws/rust-runtime/Cargo.lock | 175 ++++++++++++++++-- aws/rust-runtime/aws-sigv4/Cargo.toml | 14 +- aws/rust-runtime/aws-sigv4/benches/hmac.rs | 14 ++ .../aws-sigv4/src/http_request/test.rs | 33 +++- aws/rust-runtime/aws-sigv4/src/sign/v4.rs | 39 ++++ aws/rust-runtime/aws-sigv4/src/sign/v4a.rs | 87 ++++++++- 7 files changed, 338 insertions(+), 33 deletions(-) create mode 100644 .changelog/aws-sigv4-aws-lc-rs.md diff --git a/.changelog/aws-sigv4-aws-lc-rs.md b/.changelog/aws-sigv4-aws-lc-rs.md new file mode 100644 index 00000000000..ab6b61e1431 --- /dev/null +++ b/.changelog/aws-sigv4-aws-lc-rs.md @@ -0,0 +1,9 @@ +--- +applies_to: ["client", "aws-sdk-rust"] +authors: ["jplock"] +references: ["smithy-rs#4681"] +breaking: false +new_feature: true +bug_fix: false +--- +Add optional `aws-lc-rs` and `fips` cargo features to `aws-sigv4`. When `aws-lc-rs` is enabled, the SigV4 HMAC-SHA256 / SHA-256 primitives — and, when combined with `sigv4a`, the SigV4a ECDSA-P256 signing path — are routed through `aws-lc-rs` instead of RustCrypto's `hmac` / `sha2` / `p256`. The `fips` feature additionally activates `aws-lc-rs/fips`, routing those primitives through `aws-lc-fips-sys`. The default build is unchanged. diff --git a/aws/rust-runtime/Cargo.lock b/aws/rust-runtime/Cargo.lock index fb1ef471a11..488eadabab3 100644 --- a/aws/rust-runtime/Cargo.lock +++ b/aws/rust-runtime/Cargo.lock @@ -76,7 +76,7 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-credential-types" -version = "1.2.14" +version = "1.3.0" dependencies = [ "async-trait", "aws-smithy-async", @@ -121,9 +121,46 @@ dependencies = [ "url", ] +[[package]] +name = "aws-lc-fips-sys" +version = "0.13.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d619165468401dec3caa3366ebffbcb83f2f31883e5b3932f8e2dec2ddc568" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "regex", +] + +[[package]] +name = "aws-lc-rs" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +dependencies = [ + "aws-lc-fips-sys", + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "aws-runtime" -version = "1.7.5" +version = "1.8.0" dependencies = [ "arbitrary", "aws-credential-types", @@ -161,13 +198,14 @@ dependencies = [ [[package]] name = "aws-runtime-api" -version = "1.1.12" +version = "1.2.0" [[package]] name = "aws-sigv4" -version = "1.4.5" +version = "1.5.0" dependencies = [ "aws-credential-types", + "aws-lc-rs", "aws-smithy-eventstream", "aws-smithy-http", "aws-smithy-runtime-api", @@ -198,7 +236,7 @@ dependencies = [ [[package]] name = "aws-smithy-async" -version = "1.2.14" +version = "1.3.0" dependencies = [ "futures-util", "pin-project-lite", @@ -207,7 +245,7 @@ dependencies = [ [[package]] name = "aws-smithy-checksums" -version = "0.64.8" +version = "0.65.0" dependencies = [ "aws-smithy-http", "aws-smithy-types", @@ -226,7 +264,7 @@ dependencies = [ [[package]] name = "aws-smithy-eventstream" -version = "0.60.21" +version = "0.61.1" dependencies = [ "aws-smithy-types", "bytes", @@ -235,7 +273,7 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.63.6" +version = "0.64.0" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", @@ -254,7 +292,7 @@ dependencies = [ [[package]] name = "aws-smithy-http-client" -version = "1.1.13" +version = "1.2.0" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -274,14 +312,14 @@ dependencies = [ [[package]] name = "aws-smithy-observability" -version = "0.2.6" +version = "0.3.0" dependencies = [ "aws-smithy-runtime-api", ] [[package]] name = "aws-smithy-protocol-test" -version = "0.63.14" +version = "0.64.0" dependencies = [ "assert-json-diff", "aws-smithy-runtime-api", @@ -298,7 +336,7 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.11.3" +version = "1.12.0" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -322,7 +360,7 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.12.3" +version = "1.13.0" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api-macros", @@ -338,7 +376,7 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api-macros" -version = "1.0.0" +version = "1.1.0" dependencies = [ "proc-macro2", "quote", @@ -347,7 +385,7 @@ dependencies = [ [[package]] name = "aws-smithy-schema" -version = "0.1.0" +version = "0.1.1" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", @@ -356,7 +394,7 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.5.1" +version = "1.6.0" dependencies = [ "base64-simd", "bytes", @@ -380,7 +418,7 @@ dependencies = [ [[package]] name = "aws-types" -version = "1.3.16" +version = "1.4.0" dependencies = [ "aws-credential-types", "aws-smithy-async", @@ -419,6 +457,26 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex 1.3.0", + "syn", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -521,7 +579,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", - "shlex", + "jobserver", + "libc", + "shlex 2.0.1", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", ] [[package]] @@ -566,6 +635,17 @@ dependencies = [ "half", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.6.1" @@ -591,6 +671,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + [[package]] name = "cmov" version = "0.5.4" @@ -862,6 +951,12 @@ dependencies = [ "syn", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "ecdsa" version = "0.16.9" @@ -967,6 +1062,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures-channel" version = "0.3.32" @@ -1053,6 +1154,12 @@ dependencies = [ "wasip3", ] +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "group" version = "0.13.0" @@ -1444,6 +1551,16 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.100" @@ -1473,6 +1590,16 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1991,6 +2118,12 @@ dependencies = [ "xmlparser", ] +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + [[package]] name = "rustc_version" version = "0.4.1" @@ -2248,6 +2381,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "shlex" version = "2.0.1" diff --git a/aws/rust-runtime/aws-sigv4/Cargo.toml b/aws/rust-runtime/aws-sigv4/Cargo.toml index 5387a8b1168..c254cc61789 100644 --- a/aws/rust-runtime/aws-sigv4/Cargo.toml +++ b/aws/rust-runtime/aws-sigv4/Cargo.toml @@ -17,8 +17,19 @@ sign-http = ["dep:http0", "dep:percent-encoding", "dep:form_urlencoded"] sign-eventstream = ["dep:aws-smithy-eventstream"] sigv4a = ["dep:p256", "dep:crypto-bigint", "dep:subtle", "dep:zeroize"] +# Internal feature used to gate code that depends on aws-lc-rs being linked. +# Users should not enable this directly; choose `aws-lc-rs` or `fips` instead. +__aws-lc-rs = ["dep:aws-lc-rs"] +# Route SigV4 (and, with `sigv4a`, SigV4a) cryptographic primitives through +# `aws-lc-rs` (non-FIPS, backed by `aws-lc-sys`) instead of RustCrypto's +# `hmac` / `sha2` / `p256`. +aws-lc-rs = ["__aws-lc-rs", "aws-lc-rs?/aws-lc-sys"] +# Same as `aws-lc-rs`, but backed by the FIPS-validated `aws-lc-fips-sys`. +fips = ["__aws-lc-rs", "aws-lc-rs?/fips"] + [dependencies] aws-credential-types = { path = "../aws-credential-types" } +aws-lc-rs = { version = "1", default-features = false, optional = true } aws-smithy-eventstream = { path = "../../../rust-runtime/aws-smithy-eventstream", optional = true } aws-smithy-http = { path = "../../../rust-runtime/aws-smithy-http" } aws-smithy-runtime-api = { path = "../../../rust-runtime/aws-smithy-runtime-api", features = ["client"] } @@ -63,7 +74,8 @@ harness = false required-features = ["sigv4a"] [package.metadata.docs.rs] -all-features = true +all-features = false +features = ["aws-lc-rs", "sigv4a", "sign-http", "http1", "sign-eventstream", "http0-compat"] targets = ["x86_64-unknown-linux-gnu"] cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] rustdoc-args = ["--cfg", "docsrs"] diff --git a/aws/rust-runtime/aws-sigv4/benches/hmac.rs b/aws/rust-runtime/aws-sigv4/benches/hmac.rs index 2b7620c1ee3..8f310346e26 100644 --- a/aws/rust-runtime/aws-sigv4/benches/hmac.rs +++ b/aws/rust-runtime/aws-sigv4/benches/hmac.rs @@ -4,10 +4,14 @@ */ use criterion::{criterion_group, criterion_main, Criterion}; +#[cfg(not(feature = "__aws-lc-rs"))] use hmac::digest::FixedOutput; +#[cfg(not(feature = "__aws-lc-rs"))] use hmac::{Hmac, KeyInit, Mac}; +#[cfg(not(feature = "__aws-lc-rs"))] use sha2::Sha256; +#[cfg(not(feature = "__aws-lc-rs"))] pub fn hmac(c: &mut Criterion) { c.bench_function("hmac", |b| { b.iter(|| { @@ -19,6 +23,16 @@ pub fn hmac(c: &mut Criterion) { }); } +#[cfg(feature = "__aws-lc-rs")] +pub fn hmac(c: &mut Criterion) { + c.bench_function("hmac", |b| { + b.iter(|| { + let key = aws_lc_rs::hmac::Key::new(aws_lc_rs::hmac::HMAC_SHA256, b"secret"); + aws_lc_rs::hmac::sign(&key, b"hello, world") + }) + }); +} + criterion_group! { name = benches; diff --git a/aws/rust-runtime/aws-sigv4/src/http_request/test.rs b/aws/rust-runtime/aws-sigv4/src/http_request/test.rs index ee365508259..afecd8ebb20 100644 --- a/aws/rust-runtime/aws-sigv4/src/http_request/test.rs +++ b/aws/rust-runtime/aws-sigv4/src/http_request/test.rs @@ -310,7 +310,9 @@ pub(crate) mod v4a { sign, PayloadChecksumKind, SessionTokenMode, SignatureLocation, SigningSettings, }; use crate::sign::v4a; + #[cfg(not(feature = "__aws-lc-rs"))] use p256::ecdsa::signature::Verifier; + #[cfg(not(feature = "__aws-lc-rs"))] use p256::ecdsa::{DerSignature, SigningKey}; use std::time::Duration; use time::format_description::well_known::Rfc3339; @@ -369,12 +371,33 @@ pub(crate) mod v4a { let creds = params.credentials().unwrap(); let signing_key = v4a::generate_signing_key(creds.access_key_id(), creds.secret_access_key()); - let sig = DerSignature::try_from(hex::decode(out.signature).unwrap().as_slice()).unwrap(); - - let signing_key = SigningKey::from_slice(signing_key.as_ref()).unwrap(); - let peer_public_key = signing_key.verifying_key(); + let sig_bytes = hex::decode(out.signature).unwrap(); let sts = actual_string_to_sign.as_bytes(); - peer_public_key.verify(sts, &sig).unwrap(); + + #[cfg(not(feature = "__aws-lc-rs"))] + { + let sig = DerSignature::try_from(sig_bytes.as_slice()).unwrap(); + let signing_key = SigningKey::from_slice(signing_key.as_ref()).unwrap(); + let peer_public_key = signing_key.verifying_key(); + peer_public_key.verify(sts, &sig).unwrap(); + } + #[cfg(feature = "__aws-lc-rs")] + { + use aws_lc_rs::signature::KeyPair as _; + let scalar: &[u8; 32] = signing_key.as_ref().try_into().unwrap(); + let der = v4a::scalar_to_sec1_p256_der(scalar); + let key_pair = aws_lc_rs::signature::EcdsaKeyPair::from_private_key_der( + &aws_lc_rs::signature::ECDSA_P256_SHA256_ASN1_SIGNING, + &der, + ) + .unwrap(); + let pub_key = key_pair.public_key().as_ref(); + let upk = aws_lc_rs::signature::UnparsedPublicKey::new( + &aws_lc_rs::signature::ECDSA_P256_SHA256_ASN1, + pub_key, + ); + upk.verify(sts, &sig_bytes).unwrap(); + } // TODO(sigv4a) - use public.key.json as verifying key? } diff --git a/aws/rust-runtime/aws-sigv4/src/sign/v4.rs b/aws/rust-runtime/aws-sigv4/src/sign/v4.rs index 42f59801d68..bfc315f49f0 100644 --- a/aws/rust-runtime/aws-sigv4/src/sign/v4.rs +++ b/aws/rust-runtime/aws-sigv4/src/sign/v4.rs @@ -11,19 +11,30 @@ use crate::{ use aws_credential_types::Credentials; use aws_smithy_runtime_api::{client::identity::Identity, http::Headers}; use bytes::Bytes; +#[cfg(not(feature = "__aws-lc-rs"))] use hmac::{digest::FixedOutput, Hmac, KeyInit, Mac}; +#[cfg(not(feature = "__aws-lc-rs"))] use sha2::{Digest, Sha256}; use std::time::SystemTime; /// HashedPayload = Lowercase(HexEncode(Hash(requestPayload))) #[allow(dead_code)] // Unused when compiling without certain features +#[cfg(not(feature = "__aws-lc-rs"))] pub(crate) fn sha256_hex_string(bytes: impl AsRef<[u8]>) -> String { let mut hasher = Sha256::new(); hasher.update(bytes); hex::encode(hasher.finalize_fixed()) } +/// HashedPayload = Lowercase(HexEncode(Hash(requestPayload))) +#[allow(dead_code)] // Unused when compiling without certain features +#[cfg(feature = "__aws-lc-rs")] +pub(crate) fn sha256_hex_string(bytes: impl AsRef<[u8]>) -> String { + hex::encode(aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, bytes.as_ref()).as_ref()) +} + /// Calculates a Sigv4 signature +#[cfg(not(feature = "__aws-lc-rs"))] pub fn calculate_signature(signing_key: impl AsRef<[u8]>, string_to_sign: &[u8]) -> String { let mut mac = Hmac::::new_from_slice(signing_key.as_ref()) .expect("HMAC can take key of any size"); @@ -31,7 +42,15 @@ pub fn calculate_signature(signing_key: impl AsRef<[u8]>, string_to_sign: &[u8]) hex::encode(mac.finalize_fixed()) } +/// Calculates a Sigv4 signature +#[cfg(feature = "__aws-lc-rs")] +pub fn calculate_signature(signing_key: impl AsRef<[u8]>, string_to_sign: &[u8]) -> String { + let key = aws_lc_rs::hmac::Key::new(aws_lc_rs::hmac::HMAC_SHA256, signing_key.as_ref()); + hex::encode(aws_lc_rs::hmac::sign(&key, string_to_sign).as_ref()) +} + /// Generates a signing key for Sigv4 +#[cfg(not(feature = "__aws-lc-rs"))] pub fn generate_signing_key( secret: &str, time: SystemTime, @@ -66,6 +85,26 @@ pub fn generate_signing_key( mac.finalize_fixed() } +/// Generates a signing key for Sigv4 +#[cfg(feature = "__aws-lc-rs")] +pub fn generate_signing_key( + secret: &str, + time: SystemTime, + region: &str, + service: &str, +) -> impl AsRef<[u8]> { + use aws_lc_rs::hmac::{sign, Key, HMAC_SHA256}; + + let secret = format!("AWS4{secret}"); + let tag = sign( + &Key::new(HMAC_SHA256, secret.as_bytes()), + format_date(time).as_bytes(), + ); + let tag = sign(&Key::new(HMAC_SHA256, tag.as_ref()), region.as_bytes()); + let tag = sign(&Key::new(HMAC_SHA256, tag.as_ref()), service.as_bytes()); + sign(&Key::new(HMAC_SHA256, tag.as_ref()), b"aws4_request") +} + /// Parameters to use when signing. #[derive(Debug)] #[non_exhaustive] diff --git a/aws/rust-runtime/aws-sigv4/src/sign/v4a.rs b/aws/rust-runtime/aws-sigv4/src/sign/v4a.rs index 6067d33ca1d..d89a251176c 100644 --- a/aws/rust-runtime/aws-sigv4/src/sign/v4a.rs +++ b/aws/rust-runtime/aws-sigv4/src/sign/v4a.rs @@ -6,9 +6,13 @@ use aws_smithy_runtime_api::client::identity::Identity; use bytes::{BufMut, BytesMut}; use crypto_bigint::{CheckedAdd, CheckedSub, Encoding, U256}; +#[cfg(not(feature = "__aws-lc-rs"))] use hmac::{digest::FixedOutput, Hmac, KeyInit, Mac}; +#[cfg(not(feature = "__aws-lc-rs"))] use p256::ecdsa::signature::Signer; +#[cfg(not(feature = "__aws-lc-rs"))] use p256::ecdsa::{DerSignature, SigningKey}; +#[cfg(not(feature = "__aws-lc-rs"))] use sha2::Sha256; use std::io::Write; use std::sync::LazyLock; @@ -25,13 +29,64 @@ static BIG_N_MINUS_2: LazyLock = LazyLock::new(|| { ORDER.checked_sub(&U256::from(2u32)).unwrap() }); +/// Wraps a P-256 scalar in a minimal RFC 5915 SEC1 `ECPrivateKey` DER blob. +/// +/// AWS-LC parses this via [`aws_lc_rs::signature::EcdsaKeyPair::from_private_key_der`] and +/// derives the public point internally — the `publicKey [1] BIT STRING OPTIONAL` field is +/// allowed to be absent by RFC 5915, which lets the caller avoid pulling in a separate EC +/// implementation just to compute the public key. +/// +/// Output layout (51 bytes): +/// 30 31 SEQUENCE, length 49 +/// 02 01 01 INTEGER 1 (version = ecPrivkeyVer1) +/// 04 20 <32> OCTET STRING (privateKey scalar) +/// A0 0A [0] EXPLICIT (parameters) +/// 06 08 2A 86 48 CE 3D 03 01 07 OID 1.2.840.10045.3.1.7 (prime256v1) +#[cfg(feature = "__aws-lc-rs")] +pub(crate) fn scalar_to_sec1_p256_der(scalar: &[u8; 32]) -> [u8; 51] { + let mut out = [0u8; 51]; + out[0] = 0x30; + out[1] = 0x31; + out[2] = 0x02; + out[3] = 0x01; + out[4] = 0x01; + out[5] = 0x04; + out[6] = 0x20; + out[7..39].copy_from_slice(scalar); + out[39] = 0xA0; + out[40] = 0x0A; + out[41..51].copy_from_slice(&[0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07]); + out +} + /// Calculates a Sigv4a signature +#[cfg(not(feature = "__aws-lc-rs"))] pub fn calculate_signature(signing_key: impl AsRef<[u8]>, string_to_sign: &[u8]) -> String { let signing_key = SigningKey::from_slice(signing_key.as_ref()).unwrap(); let signature: DerSignature = signing_key.sign(string_to_sign); hex::encode(signature.as_bytes()) } +/// Calculates a Sigv4a signature +#[cfg(feature = "__aws-lc-rs")] +pub fn calculate_signature(signing_key: impl AsRef<[u8]>, string_to_sign: &[u8]) -> String { + let scalar: &[u8; 32] = signing_key + .as_ref() + .try_into() + .expect("sigv4a signing key is always a 32-byte P-256 scalar"); + let der = scalar_to_sec1_p256_der(scalar); + let key_pair = aws_lc_rs::signature::EcdsaKeyPair::from_private_key_der( + &aws_lc_rs::signature::ECDSA_P256_SHA256_ASN1_SIGNING, + &der, + ) + .expect("scalar-derived SEC1 DER is well-formed"); + let rng = aws_lc_rs::rand::SystemRandom::new(); + let signature = key_pair + .sign(&rng, string_to_sign) + .expect("ECDSA signing should not fail with a valid key"); + hex::encode(signature.as_ref()) +} + /// Generates a signing key for Sigv4a signing. pub fn generate_signing_key(access_key: &str, secret_access_key: &str) -> impl AsRef<[u8]> { // Capacity is the secret access key length plus the length of "AWS4A" @@ -41,7 +96,7 @@ pub fn generate_signing_key(access_key: &str, secret_access_key: &str) -> impl A // Capacity is the access key length plus the counter byte let mut kdf_context = Zeroizing::new(Vec::with_capacity(access_key.len() + 1)); let mut counter = Zeroizing::new(1u8); - let key = loop { + let d = loop { write!(kdf_context, "{access_key}").unwrap(); kdf_context.push(*counter); @@ -50,14 +105,11 @@ pub fn generate_signing_key(access_key: &str, secret_access_key: &str) -> impl A fis.append(&mut kdf_context); fis.put_i32(256); - let mut mac = - Hmac::::new_from_slice(&input_key).expect("HMAC can take key of any size"); - let mut buf = BytesMut::new(); buf.put_i32(1); buf.put_slice(&fis); - mac.update(&buf); - let k0 = U256::from_be_bytes(mac.finalize_fixed().into()); + + let k0 = hmac_sha256_for_kdf(&input_key, &buf); // It would be more secure for this to be a constant time comparison, but because this // is for client usage, that's not as big a deal. @@ -65,8 +117,7 @@ pub fn generate_signing_key(access_key: &str, secret_access_key: &str) -> impl A let pk = k0 .checked_add(&U256::ONE) .expect("k0 is always less than U256::MAX"); - let d = Zeroizing::new(pk.to_be_bytes()); - break SigningKey::from_slice(d.as_ref()).unwrap(); + break Zeroizing::new(pk.to_be_bytes()); } *counter = counter @@ -74,7 +125,25 @@ pub fn generate_signing_key(access_key: &str, secret_access_key: &str) -> impl A .expect("counter will never get to 255"); }; - key.to_bytes() + *d +} + +#[cfg(not(feature = "__aws-lc-rs"))] +fn hmac_sha256_for_kdf(key: &[u8], msg: &[u8]) -> U256 { + let mut mac = Hmac::::new_from_slice(key).expect("HMAC can take key of any size"); + mac.update(msg); + U256::from_be_bytes(mac.finalize_fixed().into()) +} + +#[cfg(feature = "__aws-lc-rs")] +fn hmac_sha256_for_kdf(key: &[u8], msg: &[u8]) -> U256 { + let key = aws_lc_rs::hmac::Key::new(aws_lc_rs::hmac::HMAC_SHA256, key); + let tag = aws_lc_rs::hmac::sign(&key, msg); + let bytes: [u8; 32] = tag + .as_ref() + .try_into() + .expect("HMAC-SHA256 tag is always 32 bytes"); + U256::from_be_bytes(bytes) } /// Parameters to use when signing.