aa-ebpf 0.0.1-beta.4

eBPF-based kernel-level monitoring hooks for Agent Assembly
//! Load-time eBPF bytecode integrity verification (AAASM-3602).
//!
//! Each embedded probe object carries a sha256 digest baked in at build time
//! (`build.rs` emits `cargo:rustc-env=AA_*_BPF_SHA256=…`, sourced from the
//! signed `EBPF_SHA256SUMS` produced by CI in AAASM-3601). Before any bytes are
//! handed to `aya::Ebpf::load`, [`verify_bytecode`] recomputes the digest over
//! the embedded bytes and refuses to proceed on a mismatch.
//!
//! This is the last line of defense against the swapped-`.o` supply-chain
//! attack: even if a tampered object is somehow embedded, the binary will not
//! load a probe whose digest does not match what CI signed. The check is
//! **fail-closed** — a mismatch (or an empty / unverifiable stub) returns
//! [`EbpfError::IntegrityMismatch`], never a silent degrade-to-allow.

use sha2::{Digest, Sha256};

use crate::error::EbpfError;

/// Expected sha256 (hex) of the file-I/O probe object, baked in by `build.rs`.
pub const AA_FILE_IO_BPF_SHA256: &str = env!("AA_FILE_IO_BPF_SHA256");
/// Expected sha256 (hex) of the exec-tracepoint probe object.
pub const AA_EXEC_BPF_SHA256: &str = env!("AA_EXEC_BPF_SHA256");
/// Expected sha256 (hex) of the TLS uprobe probe object.
pub const AA_TLS_BPF_SHA256: &str = env!("AA_TLS_BPF_SHA256");
/// Expected sha256 (hex) of the syscall-guard enforcement probe object.
pub const AA_SYSCALL_GUARD_BPF_SHA256: &str = env!("AA_SYSCALL_GUARD_BPF_SHA256");

/// Verify `bytes` hashes to `expected_hex`, returning
/// [`EbpfError::IntegrityMismatch`] on any divergence.
///
/// `object` names the probe for diagnostics. An empty `expected_hex` (the
/// placeholder emitted on non-Linux or when no digest was produced) is treated
/// as **unverifiable** and rejected — we never load bytecode we cannot pin.
pub fn verify_bytecode(object: &str, bytes: &[u8], expected_hex: &str) -> Result<(), EbpfError> {
    let actual = hex::encode(Sha256::digest(bytes));

    if expected_hex.is_empty() {
        return Err(EbpfError::IntegrityMismatch {
            object: object.to_string(),
            expected: "<unset: no signed digest baked in>".to_string(),
            actual,
        });
    }

    if !actual.eq_ignore_ascii_case(expected_hex) {
        return Err(EbpfError::IntegrityMismatch {
            object: object.to_string(),
            expected: expected_hex.to_string(),
            actual,
        });
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn matching_digest_passes() {
        let bytes = b"hello bpf";
        let expected = hex::encode(Sha256::digest(bytes));
        assert!(verify_bytecode("aa-test", bytes, &expected).is_ok());
    }

    #[test]
    fn mismatched_digest_is_rejected() {
        let bytes = b"tampered bytecode";
        // digest of *different* bytes
        let wrong = hex::encode(Sha256::digest(b"original bytecode"));
        let err = verify_bytecode("aa-test", bytes, &wrong).unwrap_err();
        match err {
            EbpfError::IntegrityMismatch {
                object,
                expected,
                actual,
            } => {
                assert_eq!(object, "aa-test");
                assert_eq!(expected, wrong);
                assert_ne!(actual, wrong);
            }
            other => panic!("unexpected error: {other:?}"),
        }
    }

    #[test]
    fn empty_expected_is_unverifiable_and_rejected() {
        let err = verify_bytecode("aa-test", b"anything", "").unwrap_err();
        assert!(matches!(err, EbpfError::IntegrityMismatch { .. }));
    }

    #[test]
    fn digest_comparison_is_case_insensitive() {
        let bytes = b"case test";
        let expected = hex::encode(Sha256::digest(bytes)).to_uppercase();
        assert!(verify_bytecode("aa-test", bytes, &expected).is_ok());
    }
}