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 f148da8103f..da6a665d559 100644 --- a/aws/rust-runtime/Cargo.lock +++ b/aws/rust-runtime/Cargo.lock @@ -117,6 +117,43 @@ 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.10.0" @@ -164,6 +201,7 @@ name = "aws-sigv4" version = "1.6.0" dependencies = [ "aws-credential-types", + "aws-lc-rs", "aws-smithy-eventstream", "aws-smithy-http", "aws-smithy-runtime-api", @@ -420,6 +458,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" @@ -522,7 +580,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" 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]] @@ -567,6 +636,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.6" @@ -592,6 +672,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" @@ -860,6 +949,12 @@ dependencies = [ "syn 3.0.5", ] +[[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" @@ -959,6 +1054,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.34" @@ -1043,6 +1144,12 @@ dependencies = [ "r-efi 6.0.0", ] +[[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" @@ -1408,6 +1515,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.105" @@ -1431,6 +1548,16 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[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" @@ -1939,6 +2066,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" @@ -2197,6 +2330,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 2829916d70a..c0b6fc48374 100644 --- a/aws/rust-runtime/aws-sigv4/Cargo.toml +++ b/aws/rust-runtime/aws-sigv4/Cargo.toml @@ -20,8 +20,19 @@ sign-http = ["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"] } @@ -67,7 +78,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 bfa02f122e9..a29c3397201 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.