use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use sha2::{Digest, Sha256};
use std::path::PathBuf;
const INTEGRITY_PUBLIC_KEY: [u8; 32] = {
const fn hex_nibble(b: u8) -> u8 {
match b {
b'0'..=b'9' => b - b'0',
b'a'..=b'f' => b - b'a' + 10,
b'A'..=b'F' => b - b'A' + 10,
_ => panic!("CRATON_HSM_INTEGRITY_PUBLIC_KEY contains invalid hex character"),
}
}
match option_env!("CRATON_HSM_INTEGRITY_PUBLIC_KEY") {
Some(hex) => {
let bytes = hex.as_bytes();
assert!(
bytes.len() == 64,
"CRATON_HSM_INTEGRITY_PUBLIC_KEY must be exactly 64 hex chars"
);
let mut key = [0u8; 32];
let mut i = 0;
while i < 32 {
key[i] = (hex_nibble(bytes[i * 2]) << 4) | hex_nibble(bytes[i * 2 + 1]);
i += 1;
}
key
}
None => [0u8; 32], }
};
fn is_placeholder_key() -> bool {
INTEGRITY_PUBLIC_KEY.iter().all(|&b| b == 0)
}
const DEV_BYPASS_ENV: &str = "CRATON_HSM_INTEGRITY_BYPASS";
const DEV_BYPASS_VALUE: &str = "unsafe-dev-only";
fn dev_bypass_requested() -> bool {
std::env::var(DEV_BYPASS_ENV)
.map(|v| v == DEV_BYPASS_VALUE)
.unwrap_or(false)
}
pub fn check_integrity() -> Result<(), String> {
if is_placeholder_key() {
if dev_bypass_requested() {
tracing::warn!(
"Software integrity test: BYPASSED via {}={} — this build is UNSIGNED \
and MUST NOT be used in production. Set CRATON_HSM_INTEGRITY_PUBLIC_KEY \
at build time and ship a .sig sidecar for any non-development use.",
DEV_BYPASS_ENV,
DEV_BYPASS_VALUE
);
return Ok(());
}
return Err(format!(
"Software integrity test: INTEGRITY_PUBLIC_KEY is still the all-zeros placeholder. \
Set CRATON_HSM_INTEGRITY_PUBLIC_KEY at build time, or (for local dev/test only) \
set {}={} at runtime.",
DEV_BYPASS_ENV, DEV_BYPASS_VALUE
));
}
let module_path = match get_module_path() {
Some(p) => p,
None => {
return Err(
"Software integrity test: could not determine the loaded module path; \
refusing to bypass signature verification"
.to_string(),
)
}
};
let sig_path = module_path.with_extension("sig");
let hmac_path = module_path.with_extension("hmac");
if hmac_path.exists() && !sig_path.exists() {
tracing::warn!(
"Software integrity test: found legacy .hmac file at {} but no .sig file. \
Migrate to Ed25519 signatures using tools/sign-integrity.sh. \
The HMAC-based check has been removed.",
hmac_path.display()
);
}
if !sig_path.exists() {
return Err(format!(
"Software integrity test: no .sig file found at {} — required for all builds \
when CRATON_HSM_INTEGRITY_PUBLIC_KEY is set. Sign the binary with \
tools/sign-integrity.sh.",
sig_path.display()
));
}
let sig_hex = match std::fs::read_to_string(&sig_path) {
Ok(s) => s.trim().to_lowercase(),
Err(e) => {
return Err(format!(
"Software integrity test: failed to read {}: {}",
sig_path.display(),
e
));
}
};
if sig_hex.len() != 128 {
return Err(format!(
"Software integrity test: .sig file has invalid length {} (expected 128 hex chars / 64 bytes)",
sig_hex.len()
));
}
let module_bytes = match std::fs::read(&module_path) {
Ok(b) => b,
Err(e) => {
return Err(format!(
"Software integrity test: failed to read module binary {}: {}",
module_path.display(),
e
));
}
};
let sig_bytes = hex::decode(&sig_hex).map_err(|e| {
format!(
"Software integrity test: .sig file contains invalid hex: {}",
e
)
})?;
let hash = Sha256::digest(&module_bytes);
let verifying_key = VerifyingKey::from_bytes(&INTEGRITY_PUBLIC_KEY).map_err(|e| {
format!(
"Software integrity test: invalid embedded public key: {}",
e
)
})?;
let sig_array: [u8; 64] = sig_bytes.try_into().map_err(|_| {
"Software integrity test: signature has wrong length (expected 64 bytes)".to_string()
})?;
let signature = Signature::from_bytes(&sig_array);
if verifying_key.verify(&hash, &signature).is_err() {
tracing::error!(
"Software integrity test FAILED: Ed25519 signature verification failed for {}",
module_path.display()
);
tracing::error!(" Re-sign the binary with tools/sign-integrity.sh if this is expected.");
return Err(
"Software integrity test: signature verification failed — module may be tampered"
.to_string(),
);
}
tracing::info!(
"Software integrity test passed for {}",
module_path.display()
);
Ok(())
}
pub fn compute_hash(path: &std::path::Path) -> Result<String, String> {
let module_bytes =
std::fs::read(path).map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
let hash = Sha256::digest(&module_bytes);
Ok(hex::encode(hash))
}
fn get_module_path() -> Option<PathBuf> {
#[cfg(unix)]
{
get_module_path_unix()
}
#[cfg(windows)]
{
get_module_path_windows()
}
#[cfg(not(any(unix, windows)))]
{
None
}
}
#[cfg(unix)]
fn get_module_path_unix() -> Option<PathBuf> {
use std::ffi::CStr;
let addr = check_integrity as *const () as *mut libc::c_void;
let mut info: libc::Dl_info = unsafe { std::mem::zeroed() };
let result = unsafe { libc::dladdr(addr, &mut info) };
if result != 0 && !info.dli_fname.is_null() {
let path = unsafe { CStr::from_ptr(info.dli_fname) };
path.to_str().ok().map(PathBuf::from)
} else {
None
}
}
#[cfg(windows)]
fn get_module_path_windows() -> Option<PathBuf> {
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt;
const FLAGS: u32 = 0x04 | 0x02;
let addr = check_integrity as *const () as *const u8;
let mut hmodule: windows_sys::Win32::Foundation::HMODULE = std::ptr::null_mut();
let ok = unsafe {
windows_sys::Win32::System::LibraryLoader::GetModuleHandleExW(
FLAGS,
addr as *const u16,
&mut hmodule,
)
};
if ok == 0 || hmodule.is_null() {
return None;
}
let mut buf = vec![0u16; 4096];
let len = unsafe {
windows_sys::Win32::System::LibraryLoader::GetModuleFileNameW(
hmodule,
buf.as_mut_ptr(),
buf.len() as u32,
)
};
if len == 0 {
return None;
}
let path = OsString::from_wide(&buf[..len as usize]);
Some(PathBuf::from(path))
}
#[cfg(test)]
mod tests {
use super::*;
static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[test]
#[cfg_attr(miri, ignore)]
fn test_compute_hash_deterministic() {
let dir = std::env::temp_dir().join("craton_hsm_integrity_test");
let _ = std::fs::create_dir_all(&dir);
let test_file = dir.join("test_binary.bin");
std::fs::write(&test_file, b"test module content").unwrap();
let hash1 = compute_hash(&test_file).unwrap();
let hash2 = compute_hash(&test_file).unwrap();
assert_eq!(hash1, hash2, "SHA-256 hash should be deterministic");
assert_eq!(hash1.len(), 64, "SHA-256 hex should be 64 chars");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_integrity_check_placeholder_key_rejected_by_default() {
if !is_placeholder_key() {
return;
}
let _guard = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
let prev = std::env::var(DEV_BYPASS_ENV).ok();
unsafe { std::env::remove_var(DEV_BYPASS_ENV) };
let result = check_integrity();
unsafe {
match prev {
Some(v) => std::env::set_var(DEV_BYPASS_ENV, v),
None => std::env::remove_var(DEV_BYPASS_ENV),
}
}
assert!(
result.is_err(),
"placeholder pubkey + no bypass must be a hard error"
);
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_integrity_check_dev_bypass_allows_placeholder() {
if !is_placeholder_key() {
return;
}
let _guard = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
let prev = std::env::var(DEV_BYPASS_ENV).ok();
unsafe { std::env::set_var(DEV_BYPASS_ENV, DEV_BYPASS_VALUE) };
let result = check_integrity();
unsafe {
match prev {
Some(v) => std::env::set_var(DEV_BYPASS_ENV, v),
None => std::env::remove_var(DEV_BYPASS_ENV),
}
}
assert!(
result.is_ok(),
"dev bypass with placeholder pubkey must pass: {:?}",
result.err()
);
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_different_content_different_hash() {
let dir = std::env::temp_dir().join("craton_hsm_integrity_test2");
let _ = std::fs::create_dir_all(&dir);
let file1 = dir.join("file1.bin");
let file2 = dir.join("file2.bin");
std::fs::write(&file1, b"content A").unwrap();
std::fs::write(&file2, b"content B").unwrap();
let hash1 = compute_hash(&file1).unwrap();
let hash2 = compute_hash(&file2).unwrap();
assert_ne!(
hash1, hash2,
"Different content should produce different hash"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn test_signature_verification_roundtrip() {
use ed25519_dalek::{Signer, SigningKey};
let signing_key = SigningKey::from_bytes(&[42u8; 32]);
let verifying_key = signing_key.verifying_key();
let content = b"test module binary content";
let hash = Sha256::digest(content);
let signature = signing_key.sign(&hash);
assert!(
verifying_key.verify(&hash, &signature).is_ok(),
"Ed25519 signature should verify"
);
let tampered = b"tampered module binary content";
let tampered_hash = Sha256::digest(tampered);
assert!(
verifying_key.verify(&tampered_hash, &signature).is_err(),
"Tampered content should fail verification"
);
}
}