use super::{CryptoError, CryptoProvider, ProviderIdentity};
use aws_lc_rs::{constant_time, digest, hmac, signature};
pub struct FipsProvider;
impl CryptoProvider for FipsProvider {
fn identity(&self) -> ProviderIdentity {
ProviderIdentity {
name: "aws-lc-fips",
fips_mode: true,
module_cert: None,
}
}
fn sign_ed25519(&self, seed: &[u8], message: &[u8]) -> Result<[u8; 64], CryptoError> {
if seed.len() != 32 {
return Err(CryptoError::BadLength {
what: "ed25519 seed",
expected: 32,
got: seed.len(),
});
}
let kp = signature::Ed25519KeyPair::from_seed_unchecked(seed)
.map_err(|_| CryptoError::VerifyFailed("aws-lc-fips: invalid ed25519 seed".into()))?;
let sig = kp.sign(message);
let bytes = sig.as_ref();
if bytes.len() != 64 {
return Err(CryptoError::BadLength {
what: "ed25519 signature",
expected: 64,
got: bytes.len(),
});
}
let mut out = [0u8; 64];
out.copy_from_slice(bytes);
Ok(out)
}
fn public_key_from_seed(&self, seed: &[u8]) -> Result<[u8; 32], CryptoError> {
use aws_lc_rs::signature::KeyPair as _;
if seed.len() != 32 {
return Err(CryptoError::BadLength {
what: "ed25519 seed",
expected: 32,
got: seed.len(),
});
}
let kp = signature::Ed25519KeyPair::from_seed_unchecked(seed)
.map_err(|_| CryptoError::VerifyFailed("aws-lc-fips: invalid ed25519 seed".into()))?;
let public = kp.public_key().as_ref();
if public.len() != 32 {
return Err(CryptoError::BadLength {
what: "ed25519 public key",
expected: 32,
got: public.len(),
});
}
let mut out = [0u8; 32];
out.copy_from_slice(public);
Ok(out)
}
fn validate_ed25519_public_key(&self, public_key: &[u8]) -> Result<(), CryptoError> {
if public_key.len() != 32 {
return Err(CryptoError::BadLength {
what: "ed25519 public key",
expected: 32,
got: public_key.len(),
});
}
Ok(())
}
fn verify_ed25519(
&self,
public_key: &[u8],
message: &[u8],
sig: &[u8],
) -> Result<(), CryptoError> {
let pk = signature::UnparsedPublicKey::new(&signature::ED25519, public_key);
pk.verify(message, sig)
.map_err(|_| CryptoError::VerifyFailed("aws-lc-fips: ed25519 verify failed".into()))
}
fn hmac_sha256(&self, key: &[u8], message: &[u8]) -> [u8; 32] {
let k = hmac::Key::new(hmac::HMAC_SHA256, key);
let tag = hmac::sign(&k, message);
let mut out = [0u8; 32];
out.copy_from_slice(tag.as_ref());
out
}
fn constant_time_eq(&self, a: &[u8], b: &[u8]) -> bool {
constant_time::verify_slices_are_equal(a, b).is_ok()
}
fn sha256(&self, message: &[u8]) -> [u8; 32] {
let d = digest::digest(&digest::SHA256, message);
let mut out = [0u8; 32];
out.copy_from_slice(d.as_ref());
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::provider;
use aws_lc_rs::signature::KeyPair as _;
#[test]
fn fips_provider_is_active_and_round_trips() {
let id = provider().identity();
assert!(id.fips_mode, "fips build must report fips_mode=true");
assert_eq!(id.name, "aws-lc-fips");
let p = FipsProvider;
let seed = [7u8; 32];
let kp = signature::Ed25519KeyPair::from_seed_unchecked(&seed).unwrap();
let public = kp.public_key().as_ref().to_vec();
let msg = b"canonical-payload-bytes";
let sig = p.sign_ed25519(&seed, msg).unwrap();
p.verify_ed25519(&public, msg, &sig).expect("verify ok");
assert!(p.verify_ed25519(&public, b"other", &sig).is_err());
assert_eq!(p.sha256(b"").len(), 32);
let mac = p.hmac_sha256(b"k", b"m");
assert_eq!(mac.len(), 32);
assert!(p.constant_time_eq(&mac, &mac));
assert!(!p.constant_time_eq(&mac, &[0u8; 32]));
}
#[cfg(feature = "dalek")]
#[test]
fn dalek_and_fips_are_byte_identical_and_cross_verify() {
use crate::crypto::dalek::{public_key_from_seed, DalekProvider};
let dalek = DalekProvider;
let fips = FipsProvider;
let seed = [3u8; 32];
let msg = b"cross-provider-wire-compat";
let sig_d = dalek.sign_ed25519(&seed, msg).unwrap();
let sig_f = fips.sign_ed25519(&seed, msg).unwrap();
assert_eq!(
sig_d, sig_f,
"Ed25519 is deterministic — dalek and fips must produce identical signatures"
);
let public = public_key_from_seed(&seed).unwrap();
dalek
.verify_ed25519(&public, msg, &sig_f)
.expect("dalek must verify a fips-produced signature");
fips.verify_ed25519(&public, msg, &sig_d)
.expect("fips must verify a dalek-produced signature");
assert_eq!(
dalek.hmac_sha256(b"k", msg),
fips.hmac_sha256(b"k", msg),
"HMAC-SHA256 parity"
);
assert_eq!(dalek.sha256(msg), fips.sha256(msg), "SHA-256 parity");
}
#[cfg(feature = "dalek")]
#[test]
fn c06_malformed_keys_are_inert_against_honest_signatures_under_fips() {
use crate::crypto::dalek::{public_key_from_seed, validate_ed25519_public_key};
let fips = FipsProvider;
let seed = [0x42u8; 32];
let good_pk = public_key_from_seed(&seed).unwrap();
let msg = b"c06-empirical-probe";
let good_sig = fips.sign_ed25519(&seed, msg).unwrap();
fips.verify_ed25519(&good_pk, msg, &good_sig)
.expect("sanity: the good key verifies its own signature");
let mut dalek_rejected = 0u32;
let mut fips_also_rejected = 0u32;
for i in 0u32..4000 {
let cand = fips.sha256(&i.to_le_bytes());
if validate_ed25519_public_key(&cand).is_err() {
dalek_rejected += 1;
if fips.verify_ed25519(&cand, msg, &good_sig).is_err() {
fips_also_rejected += 1;
} else {
panic!(
"DIVERGENCE: aws-lc-fips accepted a key dalek rejects at load: {cand:02x?}"
);
}
}
}
assert!(
dalek_rejected > 100,
"expected many off-curve/non-canonical candidates in 4000, got {dalek_rejected}"
);
assert_eq!(
fips_also_rejected, dalek_rejected,
"every key dalek rejects at load must be inert (cannot verify an honest signature) under aws-lc-fips"
);
eprintln!(
"C-EXT-AWSLC: of 4000 candidates dalek's validator rejected {dalek_rejected}; \
aws-lc-fips found all {fips_also_rejected} INERT against an honest signature. \
=> FIPS-pure from_validated_bytes may defer load-time strictness; the malformed \
anchor cannot verify honest traffic. NOT proven: structural rejection / resistance \
to an adversarial signature under a degenerate key — bounded by operator-pinned \
trust anchors, see ADR-0027."
);
}
}