bcx-crypto 0.3.0

Crypto-agile BCX envelope metadata and verifier traits.
Documentation
#![no_std]
#![doc = "Crypto-agile envelope metadata for BCX."]

mod envelope;
mod error;

pub use envelope::{
    AlgorithmPolicy, HybridVerified, HybridVerifier, SignatureAlgorithm, SignatureEnvelope,
    SignedEnvelope, Verifier,
};
pub use error::VerificationError;

#[cfg(test)]
mod tests {
    use super::*;
    use bcx_core::Digest;
    use bcx_wire::WireLimits;
    use core::cell::Cell;

    #[test]
    fn empty_signature_is_rejected() {
        assert_eq!(
            SignatureEnvelope::new(
                Digest::new([1; Digest::LEN]),
                SignatureAlgorithm::Ed25519,
                &[],
                WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
            ),
            Err(VerificationError::EmptySignature)
        );
    }

    #[test]
    fn wrong_signature_length_is_rejected() {
        assert_eq!(
            SignatureEnvelope::new(
                Digest::new([1; Digest::LEN]),
                SignatureAlgorithm::Ed25519,
                &[7; 63],
                WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
            ),
            Err(VerificationError::InvalidSignature)
        );
    }

    #[test]
    fn hybrid_signature_split_uses_documented_layout() {
        let signature = [5; SignatureAlgorithm::HYBRID_ED25519_ML_DSA_65_SIGNATURE_LEN];
        let split = SignatureAlgorithm::HybridEd25519MlDsa65.split_hybrid(&signature);

        assert_eq!(
            split.map(|(ed25519, ml_dsa)| (ed25519.len(), ml_dsa.len())),
            Some((
                SignatureAlgorithm::ED25519_SIGNATURE_LEN,
                SignatureAlgorithm::ML_DSA_65_SIGNATURE_LEN,
            ))
        );
        assert_eq!(SignatureAlgorithm::Ed25519.split_hybrid(&signature), None);
    }

    #[test]
    fn empty_algorithm_policy_requires_explicit_deny_all() {
        assert_eq!(
            AlgorithmPolicy::new(&[]),
            Err(VerificationError::EmptyAlgorithmPolicy)
        );
        assert!(!AlgorithmPolicy::deny_all().admits(SignatureAlgorithm::Ed25519));
    }

    #[test]
    fn policy_rejects_unadmitted_algorithm_before_verifier() -> Result<(), VerificationError> {
        struct RejectingVerifier;

        impl HybridVerifier for RejectingVerifier {
            fn verify_ed25519(
                &self,
                _ed25519_signature: &[u8],
                _canonical_payload: &[u8],
            ) -> Result<(), VerificationError> {
                Err(VerificationError::InvalidSignature)
            }

            fn verify_ml_dsa_65(
                &self,
                _ml_dsa_65_signature: &[u8],
                _canonical_payload: &[u8],
            ) -> Result<(), VerificationError> {
                Err(VerificationError::InvalidSignature)
            }
        }

        impl Verifier for RejectingVerifier {
            fn verify(
                &self,
                _envelope: &SignatureEnvelope<'_>,
                _canonical_payload: &[u8],
            ) -> Result<(), VerificationError> {
                Err(VerificationError::InvalidSignature)
            }
        }

        let signature = [1; 64];
        let envelope = SignedEnvelope::new(
            (),
            SignatureEnvelope::new(
                Digest::new([1; Digest::LEN]),
                SignatureAlgorithm::Ed25519,
                &signature,
                WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
            )?,
        );
        let policy = AlgorithmPolicy::new(&[SignatureAlgorithm::MlDsa65])?;

        assert_eq!(
            envelope.verify_detached_bytes(
                &RejectingVerifier,
                &policy,
                b"payload",
                WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
            ),
            Err(VerificationError::AlgorithmNotAdmitted)
        );
        Ok(())
    }

    #[test]
    fn detached_payload_is_bounded_by_wire_limits() -> Result<(), VerificationError> {
        struct AcceptingVerifier;

        impl HybridVerifier for AcceptingVerifier {
            fn verify_ed25519(
                &self,
                _ed25519_signature: &[u8],
                _canonical_payload: &[u8],
            ) -> Result<(), VerificationError> {
                Ok(())
            }

            fn verify_ml_dsa_65(
                &self,
                _ml_dsa_65_signature: &[u8],
                _canonical_payload: &[u8],
            ) -> Result<(), VerificationError> {
                Ok(())
            }
        }

        impl Verifier for AcceptingVerifier {
            fn verify(
                &self,
                _envelope: &SignatureEnvelope<'_>,
                _canonical_payload: &[u8],
            ) -> Result<(), VerificationError> {
                Ok(())
            }
        }

        let signature = [1; 64];
        let envelope = SignedEnvelope::new(
            (),
            SignatureEnvelope::new(
                Digest::new([1; Digest::LEN]),
                SignatureAlgorithm::Ed25519,
                &signature,
                WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
            )?,
        );
        let payload = [0; 65];
        let limits =
            WireLimits::new(64, 1, 1, 1).map_err(|_| VerificationError::InvalidSignature)?;
        let policy = AlgorithmPolicy::new(&[SignatureAlgorithm::Ed25519])?;

        assert_eq!(
            envelope.verify_detached_bytes(&AcceptingVerifier, &policy, &payload, limits),
            Err(VerificationError::PayloadTooLarge)
        );
        Ok(())
    }

    #[test]
    fn signature_envelope_debug_redacts_signature_bytes() -> Result<(), VerificationError> {
        extern crate std;
        use std::{format, string::String};

        let signature = [7; SignatureAlgorithm::ED25519_SIGNATURE_LEN];
        let envelope = SignatureEnvelope::new(
            Digest::new([1; Digest::LEN]),
            SignatureAlgorithm::Ed25519,
            &signature,
            WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
        )?;

        assert_eq!(
            format!("{envelope:?}"),
            String::from(
                "SignatureEnvelope { key_id: Digest(..), algorithm: Ed25519, signature: [64 bytes] }"
            )
        );
        Ok(())
    }

    #[test]
    fn hybrid_verification_requires_both_components() -> Result<(), VerificationError> {
        struct PartialHybridVerifier;

        impl HybridVerifier for PartialHybridVerifier {
            fn verify_ed25519(
                &self,
                ed25519_signature: &[u8],
                _canonical_payload: &[u8],
            ) -> Result<(), VerificationError> {
                if ed25519_signature.len() == SignatureAlgorithm::ED25519_SIGNATURE_LEN {
                    Ok(())
                } else {
                    Err(VerificationError::InvalidSignature)
                }
            }

            fn verify_ml_dsa_65(
                &self,
                _ml_dsa_65_signature: &[u8],
                _canonical_payload: &[u8],
            ) -> Result<(), VerificationError> {
                Err(VerificationError::InvalidSignature)
            }
        }

        impl Verifier for PartialHybridVerifier {
            fn verify(
                &self,
                _envelope: &SignatureEnvelope<'_>,
                _canonical_payload: &[u8],
            ) -> Result<(), VerificationError> {
                Ok(())
            }
        }

        let signature = [9; SignatureAlgorithm::HYBRID_ED25519_ML_DSA_65_SIGNATURE_LEN];
        let envelope = SignedEnvelope::new(
            (),
            SignatureEnvelope::new(
                Digest::new([1; Digest::LEN]),
                SignatureAlgorithm::HybridEd25519MlDsa65,
                &signature,
                WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
            )?,
        );
        let policy = AlgorithmPolicy::new(&[SignatureAlgorithm::HybridEd25519MlDsa65])?;

        assert_eq!(
            envelope.verify_detached_bytes(
                &PartialHybridVerifier,
                &policy,
                b"payload",
                WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
            ),
            Err(VerificationError::InvalidSignature)
        );
        Ok(())
    }

    #[test]
    fn hybrid_verification_invokes_both_components_on_failure() -> Result<(), VerificationError> {
        struct CountingHybridVerifier {
            ed25519_calls: Cell<usize>,
            ml_dsa_65_calls: Cell<usize>,
        }

        impl HybridVerifier for CountingHybridVerifier {
            fn verify_ed25519(
                &self,
                _ed25519_signature: &[u8],
                _canonical_payload: &[u8],
            ) -> Result<(), VerificationError> {
                self.ed25519_calls.set(self.ed25519_calls.get() + 1);
                Err(VerificationError::InvalidSignature)
            }

            fn verify_ml_dsa_65(
                &self,
                _ml_dsa_65_signature: &[u8],
                _canonical_payload: &[u8],
            ) -> Result<(), VerificationError> {
                self.ml_dsa_65_calls.set(self.ml_dsa_65_calls.get() + 1);
                Err(VerificationError::InvalidSignature)
            }
        }

        impl Verifier for CountingHybridVerifier {
            fn verify(
                &self,
                _envelope: &SignatureEnvelope<'_>,
                _canonical_payload: &[u8],
            ) -> Result<(), VerificationError> {
                Err(VerificationError::InvalidSignature)
            }
        }

        let signature = [9; SignatureAlgorithm::HYBRID_ED25519_ML_DSA_65_SIGNATURE_LEN];
        let envelope = SignedEnvelope::new(
            (),
            SignatureEnvelope::new(
                Digest::new([1; Digest::LEN]),
                SignatureAlgorithm::HybridEd25519MlDsa65,
                &signature,
                WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
            )?,
        );
        let policy = AlgorithmPolicy::new(&[SignatureAlgorithm::HybridEd25519MlDsa65])?;
        let verifier = CountingHybridVerifier {
            ed25519_calls: Cell::new(0),
            ml_dsa_65_calls: Cell::new(0),
        };

        assert_eq!(
            envelope.verify_detached_bytes(
                &verifier,
                &policy,
                b"payload",
                WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
            ),
            Err(VerificationError::InvalidSignature)
        );
        assert_eq!(verifier.ed25519_calls.get(), 1);
        assert_eq!(verifier.ml_dsa_65_calls.get(), 1);
        Ok(())
    }
}