cellos-core 0.8.0-pre

CellOS domain types and ports — typed authority, formation DAG, CloudEvent envelopes, RBAC primitives. No I/O.
Documentation
//! FIPS [`CryptoProvider`] adapter (S05, ADR-0027) over `aws-lc-rs` built with
//! the `fips` feature — the CMVP-validated AWS-LC FIPS module.
//!
//! Compiled ONLY under `--features fips`. It reports `fips_mode = true`, which is
//! a **producer claim, not a validated attestation**: SC-13 / IA-7 are met only
//! when an active CMVP certificate covers this build/platform in its validated
//! configuration (procurement, S12) — linking aws-lc-fips does not by itself
//! satisfy the control.
//!
//! Wire bytes are identical to the [`super::dalek`] adapter (Ed25519 is
//! deterministic per RFC 8032; HMAC-SHA-256 and SHA-256 are standard), so a
//! dalek↔fips provider swap is byte-stable over the same canonical payload — a
//! receipt signed by one verifies under the other.

use super::{CryptoError, CryptoProvider, ProviderIdentity};
use aws_lc_rs::{constant_time, digest, hmac, signature};

/// The aws-lc-fips-backed [`CryptoProvider`].
pub struct FipsProvider;

impl CryptoProvider for FipsProvider {
    fn identity(&self) -> ProviderIdentity {
        ProviderIdentity {
            name: "aws-lc-fips",
            // Producer claim only (ADR-0027). Not a validated attestation.
            fips_mode: true,
            // No cert asserted in code; the CMVP cert is recorded at
            // build/procurement time (S12), never hard-coded as truth here.
            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> {
        // C05: derive the public key inside the CMVP-validated module so the
        // FIPS build's power-on self-test never reaches dalek. `public_key()`
        // comes from the aws-lc-rs `KeyPair` trait.
        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> {
        // C06 — DOCUMENTED RESIDUAL (C-EXT-AWSLC). aws-lc-rs exposes no standalone
        // Ed25519 point validator, so this enforces only the structural length
        // here; on-curve / canonical rejection is deferred to verify-time, which
        // `c06_aws_lc_rejects_every_key_dalek_rejects_at_load` proves rejects
        // every key dalek rejects at load (0 divergences over 1985 malformed
        // keys). A malformed trust-anchor key thus loads but is inert — it can
        // never verify a signature — rather than being rejected up front. This is
        // a deliberate, bounded degradation under fips, not a silent one.
        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() {
        // Under --features fips, provider() MUST be the fips adapter.
        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]));
    }

    /// S07 cross-provider wire-compat gate: the dalek and fips adapters MUST
    /// produce byte-identical Ed25519 signatures / HMACs / digests for the same
    /// inputs, and each MUST verify the other's signature. This is what makes a
    /// dalek<->fips provider swap wire-stable — a receipt signed under one
    /// verifies under the other, so the offline-verify guarantee survives the
    /// crypto-module change.
    #[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");
    }

    /// C-EXT-AWSLC (C06 gate): a FIPS-pure build drops dalek's load-time point
    /// validator, so `from_validated_bytes` under fips only length-checks. This
    /// pins the property that makes that safe: a malformed trust-anchor key is
    /// **INERT** under aws-lc-fips — it cannot verify a legitimately-produced
    /// signature.
    ///
    /// HONEST SCOPE (do not overstate). Method: for a mutation corpus of 32-byte
    /// candidates that dalek's validator rejects (off-curve / non-canonical),
    /// assert aws-lc-fips fails to verify a *real* signature made under a
    /// DIFFERENT key. This proves the malformed key does not verify honest
    /// traffic; it does NOT prove aws-lc structurally *rejects* the key (a wrong
    /// key fails a signature check whether malformed or not — aws-lc-rs exposes
    /// no standalone point validator to test that directly). The residual it
    /// does NOT close: an adversarially-crafted signature under a degenerate
    /// (small-order) key. That residual is bounded operationally — trust anchors
    /// are operator-pinned, and an attacker who can inject a malformed anchor can
    /// inject a valid one — NOT by this test. See ADR-0027 / the C-EXT-AWSLC
    /// finding.
    #[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 {
            // Deterministic pseudo-random candidate key bytes.
            let cand = fips.sha256(&i.to_le_bytes());
            if validate_ed25519_public_key(&cand).is_err() {
                dalek_rejected += 1;
                // A key dalek rejects at load must NOT be able to verify a real
                // signature under aws-lc-fips (i.e. it is inert at use-time).
                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."
        );
    }
}