use sha2::{Digest, Sha256};
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct BinaryManifest {
pub binary_hash: String,
#[serde(default)]
pub manifest_signature: String,
#[serde(default)]
pub signer_kid: String,
#[serde(default)]
pub version: String,
}
pub const GIT_COMMIT: &str = env!("INVARIANT_GIT_COMMIT");
pub const BUILD_PROFILE: &str = env!("INVARIANT_BUILD_PROFILE");
pub fn compiled_hash() -> Option<&'static str> {
option_env!("INVARIANT_BUILD_HASH")
}
pub fn hash_current_binary() -> Result<String, String> {
let exe_path = std::env::current_exe().map_err(|e| format!("cannot locate own binary: {e}"))?;
hash_file(&exe_path)
}
pub fn hash_file(path: &Path) -> Result<String, String> {
let data = std::fs::read(path).map_err(|e| format!("cannot read {}: {e}", path.display()))?;
let hash = Sha256::digest(&data);
Ok(format!("sha256:{:x}", hash))
}
pub fn find_manifest() -> Option<PathBuf> {
let exe = std::env::current_exe().ok()?;
let manifest = exe.with_extension("manifest.json");
if manifest.exists() {
Some(manifest)
} else {
None
}
}
pub fn load_manifest(path: &Path) -> Result<BinaryManifest, String> {
let data = std::fs::read_to_string(path).map_err(|e| format!("read manifest: {e}"))?;
serde_json::from_str(&data).map_err(|e| format!("parse manifest: {e}"))
}
pub fn run() -> i32 {
let binary_hash = match hash_current_binary() {
Ok(h) => h,
Err(e) => {
eprintln!("FATAL: {e}");
return 2;
}
};
println!("Binary hash: {binary_hash}");
let mut verified = false;
if let Some(compiled) = compiled_hash() {
if binary_hash == compiled {
println!("OK: binary hash matches compiled-in manifest ({compiled})");
verified = true;
} else {
eprintln!(
"FATAL: binary integrity check failed\n expected: {compiled}\n actual: {binary_hash}"
);
return 1;
}
}
if let Some(manifest_path) = find_manifest() {
match load_manifest(&manifest_path) {
Ok(manifest) => {
if binary_hash == manifest.binary_hash {
println!(
"OK: binary hash matches manifest file ({})",
manifest_path.display()
);
if !manifest.signer_kid.is_empty() {
println!(" Signed by: {}", manifest.signer_kid);
}
verified = true;
} else {
eprintln!(
"FATAL: binary hash does not match manifest\n expected: {}\n actual: {binary_hash}",
manifest.binary_hash
);
return 1;
}
}
Err(e) => {
eprintln!("WARNING: could not load manifest: {e}");
}
}
}
if !verified {
println!("No compiled-in hash or manifest file found.");
println!("To enable verification, rebuild with:");
println!(" INVARIANT_BUILD_HASH=\"{binary_hash}\" cargo build --release");
println!("Or place a manifest at: <binary>.manifest.json");
}
println!("Build profile: {BUILD_PROFILE}");
println!("Git commit: {GIT_COMMIT}");
match validate_all_builtin_profiles() {
Ok(n) => println!("Profiles: {n} built-in profile(s) loaded and validated"),
Err(e) => {
eprintln!("FATAL: built-in profile validation failed: {e}");
return 1;
}
}
0
}
pub fn validate_all_builtin_profiles() -> Result<usize, String> {
use invariant_robotics::models::error::Validate;
let names = invariant_robotics::profiles::list_builtins();
let mut count = 0usize;
for name in names.iter() {
let profile = invariant_robotics::profiles::load_builtin(name)
.map_err(|e| format!("load '{name}': {e}"))?;
profile
.validate()
.map_err(|e| format!("validate '{name}': {e}"))?;
count += 1;
}
Ok(count)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hash_file_produces_sha256_prefix() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.bin");
std::fs::write(&path, b"hello world").unwrap();
let hash = hash_file(&path).unwrap();
assert!(
hash.starts_with("sha256:"),
"hash must start with sha256: prefix, got: {hash}"
);
assert_eq!(
hash,
"sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
);
}
#[test]
fn hash_file_different_content_different_hash() {
let dir = tempfile::tempdir().unwrap();
let p1 = dir.path().join("a.bin");
let p2 = dir.path().join("b.bin");
std::fs::write(&p1, b"aaa").unwrap();
std::fs::write(&p2, b"bbb").unwrap();
assert_ne!(hash_file(&p1).unwrap(), hash_file(&p2).unwrap());
}
#[test]
fn hash_file_nonexistent_returns_error() {
let result = hash_file(Path::new("/nonexistent/binary"));
assert!(result.is_err());
}
#[test]
fn hash_current_binary_succeeds() {
let hash = hash_current_binary().unwrap();
assert!(hash.starts_with("sha256:"));
assert!(hash.len() > 10);
}
#[test]
fn compiled_hash_is_none_in_dev_builds() {
let _ = compiled_hash();
}
#[test]
fn load_manifest_valid() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.manifest.json");
let manifest = BinaryManifest {
binary_hash: "sha256:abc123".into(),
manifest_signature: "sig".into(),
signer_kid: "build-001".into(),
version: "0.1.0".into(),
};
let json = serde_json::to_string_pretty(&manifest).unwrap();
std::fs::write(&path, json).unwrap();
let loaded = load_manifest(&path).unwrap();
assert_eq!(loaded.binary_hash, "sha256:abc123");
assert_eq!(loaded.signer_kid, "build-001");
}
#[test]
fn load_manifest_invalid_json() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("bad.manifest.json");
std::fs::write(&path, "not json").unwrap();
assert!(load_manifest(&path).is_err());
}
#[test]
fn load_manifest_nonexistent() {
assert!(load_manifest(Path::new("/nonexistent/manifest.json")).is_err());
}
#[test]
fn run_returns_0() {
assert_eq!(run(), 0);
}
#[test]
fn build_profile_constant_is_known_value() {
assert!(
BUILD_PROFILE == "debug" || BUILD_PROFILE == "release" || BUILD_PROFILE == "test",
"BUILD_PROFILE must be one of debug/release/test, got: {BUILD_PROFILE}"
);
}
#[test]
fn git_commit_constant_is_non_empty() {
assert!(!GIT_COMMIT.is_empty());
}
#[test]
fn validate_all_builtin_profiles_loads_every_one() {
let count = validate_all_builtin_profiles().expect("all built-in profiles must validate");
assert!(
count >= 30,
"expected at least 30 built-in profiles, loaded {count}"
);
}
#[test]
fn hash_current_binary_matches_in_process_sha256() {
let exe = std::env::current_exe().unwrap();
let bytes = std::fs::read(&exe).unwrap();
let expected = format!("sha256:{:x}", Sha256::digest(&bytes));
let got = hash_current_binary().unwrap();
assert_eq!(got, expected);
}
#[test]
fn manifest_serde_roundtrip() {
let m = BinaryManifest {
binary_hash: "sha256:deadbeef".into(),
manifest_signature: "AAAA".into(),
signer_kid: "kid".into(),
version: "1.0.0".into(),
};
let json = serde_json::to_string(&m).unwrap();
let parsed: BinaryManifest = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.binary_hash, m.binary_hash);
assert_eq!(parsed.version, m.version);
}
}