use std::fmt;
use std::path::{Path, PathBuf};
use std::process::Command;
pub const IS_EMBEDDED_RELEASE: bool = {
let bytes = env!("MUR_EMBEDDED_RELEASE").as_bytes();
bytes.len() == 1 && bytes[0] == b'1'
};
pub const APPLE_TEAM_ID: &str = env!("MUR_APPLE_TEAM_ID");
pub(crate) fn production_requirement() -> String {
format!("=anchor apple generic and certificate leaf[subject.OU] = \"{APPLE_TEAM_ID}\"")
}
pub fn verify_runtime_signature(path: &Path) -> Result<(), AttestError> {
if !IS_EMBEDDED_RELEASE || !cfg!(target_os = "macos") {
return Ok(());
}
let real = path.canonicalize().map_err(|e| AttestError::Io {
path: path.to_path_buf(),
source: e,
})?;
verify_with_requirement(&real, &production_requirement())
}
#[doc(hidden)]
pub fn verify_with_requirement(path: &Path, requirement: &str) -> Result<(), AttestError> {
let out = Command::new("codesign")
.args(["--verify", "--strict", "-R", requirement])
.arg(path)
.output()
.map_err(|e| AttestError::Io {
path: path.to_path_buf(),
source: e,
})?;
if out.status.success() {
Ok(())
} else {
Err(AttestError::VerificationFailed {
path: path.to_path_buf(),
stderr: String::from_utf8_lossy(&out.stderr).trim().to_string(),
})
}
}
#[derive(Debug)]
pub enum AttestError {
VerificationFailed { path: PathBuf, stderr: String },
Io {
path: PathBuf,
source: std::io::Error,
},
}
impl fmt::Display for AttestError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::VerificationFailed { path, stderr } => write!(
f,
"runtime binary at {} failed signature verification: {stderr}",
path.display()
),
Self::Io { path, source } => {
write!(
f,
"cannot verify runtime binary at {}: {source}",
path.display()
)
}
}
}
}
impl std::error::Error for AttestError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io { source, .. } => Some(source),
Self::VerificationFailed { .. } => None,
}
}
}
const STDERR_UNSIGNED: &str = "not signed at all";
const STDERR_REQUIREMENT_UNMET: &str = "failed to satisfy specified code requirement";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AttestFailureKind {
NotMurSigned,
Tampered,
Unverifiable,
}
impl AttestError {
pub fn kind(&self) -> AttestFailureKind {
match self {
Self::Io { .. } => AttestFailureKind::Unverifiable,
Self::VerificationFailed { stderr, .. } => {
if stderr.contains(STDERR_UNSIGNED) || stderr.contains(STDERR_REQUIREMENT_UNMET) {
AttestFailureKind::NotMurSigned
} else {
AttestFailureKind::Tampered
}
}
}
}
pub fn fix_hint(&self) -> &'static str {
match self.kind() {
AttestFailureKind::NotMurSigned => {
"the runtime is not signed by MUR's Developer ID, which usually \
means a locally built (dev) mur-agent-runtime is installed next \
to a release mur. Fix: reinstall MUR to restore the release \
runtime, or use a dev build of mur together with the dev \
runtime. If you did not build it yourself, treat the binary as \
swapped and reinstall MUR."
}
AttestFailureKind::Tampered => {
"the runtime binary was modified after it was signed \
(launch-chain protection covers writes, attestation covers \
swaps). Fix: reinstall MUR."
}
AttestFailureKind::Unverifiable => {
"the runtime binary could not be checked. Fix: confirm the \
file exists and is readable, or reinstall MUR."
}
}
}
pub fn mount_failure_message(&self) -> String {
format!("{self} — {}", self.fix_hint())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn failed(stderr: &str) -> AttestError {
AttestError::VerificationFailed {
path: PathBuf::from("/x/mur-agent-runtime"),
stderr: stderr.to_string(),
}
}
#[test]
fn adhoc_dev_runtime_hint_names_dev_build_not_restart() {
let e = failed("test-requirement: code failed to satisfy specified code requirement(s)");
assert_eq!(e.kind(), AttestFailureKind::NotMurSigned);
let msg = e.mount_failure_message();
assert!(msg.contains("not signed by MUR's Developer ID"), "{msg}");
assert!(msg.contains("dev build of mur"), "{msg}");
assert!(msg.contains("reinstall MUR"), "{msg}");
assert!(!msg.contains("--restart-agents"), "{msg}");
}
#[test]
fn unsigned_runtime_is_not_mur_signed() {
let e = failed("/x/mur-agent-runtime: code object is not signed at all");
assert_eq!(e.kind(), AttestFailureKind::NotMurSigned);
}
#[test]
fn modified_after_signing_is_tampered() {
let e = failed("/x/mur-agent-runtime: main executable failed strict validation");
assert_eq!(e.kind(), AttestFailureKind::Tampered);
let msg = e.mount_failure_message();
assert!(msg.contains("modified after it was signed"), "{msg}");
assert!(!msg.contains("--restart-agents"), "{msg}");
}
#[test]
fn io_failure_is_unverifiable() {
let e = AttestError::Io {
path: PathBuf::from("/x/mur-agent-runtime"),
source: std::io::Error::from(std::io::ErrorKind::NotFound),
};
assert_eq!(e.kind(), AttestFailureKind::Unverifiable);
assert!(
e.mount_failure_message()
.starts_with("cannot verify runtime binary at /x/mur-agent-runtime")
);
}
#[test]
#[allow(clippy::assertions_on_constants)] fn dev_build_never_verifies() {
assert!(!IS_EMBEDDED_RELEASE);
assert!(verify_runtime_signature(Path::new("/nonexistent/nope")).is_ok());
}
#[test]
fn production_requirement_binds_anchor_and_team() {
let req = production_requirement();
assert!(req.contains("anchor apple generic"), "req: {req}");
assert!(req.contains("subject.OU"), "req: {req}");
assert!(req.starts_with("=anchor apple generic and"), "req: {req}");
}
#[cfg(unix)]
fn test_dir(name: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("mur-attest-{}-{}", name, std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
d
}
#[cfg(unix)]
fn test_ou() -> Option<String> {
std::env::var("MUR_TEST_SIGNING_OU").ok()
}
#[test]
#[cfg(unix)] fn unsigned_file_fails_test_requirement() {
let Some(ou) = test_ou() else {
eprintln!("skipping: MUR_TEST_SIGNING_OU not set");
return;
};
let dir = test_dir("unsigned");
let f = dir.join("runtime");
std::fs::write(&f, b"#!/bin/sh\nexit 0\n").unwrap();
std::fs::set_permissions(&f, std::fs::Permissions::from_mode(0o755)).unwrap();
let req = format!("=certificate leaf[subject.OU] = \"{ou}\"");
let err = verify_with_requirement(&f, &req).expect_err("unsigned must fail");
assert!(matches!(err, AttestError::VerificationFailed { .. }));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
#[cfg(unix)] fn adhoc_signed_fails_test_requirement() {
let Some(ou) = test_ou() else {
eprintln!("skipping");
return;
};
let dir = test_dir("adhoc");
let f = dir.join("runtime");
std::fs::write(&f, b"#!/bin/sh\nexit 0\n").unwrap();
std::fs::set_permissions(&f, std::fs::Permissions::from_mode(0o755)).unwrap();
let out = std::process::Command::new("codesign")
.args(["--force", "-s", "-"])
.arg(&f)
.output()
.unwrap();
assert!(
out.status.success(),
"ad-hoc sign failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let req = format!("=certificate leaf[subject.OU] = \"{ou}\"");
let err = verify_with_requirement(&f, &req).expect_err("ad-hoc (no OU) must fail");
assert!(matches!(err, AttestError::VerificationFailed { .. }));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
#[cfg(unix)] fn wrong_ou_fails_test_requirement() {
let Some(ou) = test_ou() else {
eprintln!("skipping");
return;
};
let dir = test_dir("wrongou");
let f = dir.join("runtime");
std::fs::write(&f, b"#!/bin/sh\nexit 0\n").unwrap();
std::fs::set_permissions(&f, std::fs::Permissions::from_mode(0o755)).unwrap();
let out = std::process::Command::new("codesign")
.args(["--force", "-s", &format!("Mur Test ({ou})")])
.arg(&f)
.output()
.unwrap();
assert!(
out.status.success(),
"sign failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let wrong = "=certificate leaf[subject.OU] = \"WRONGTEAM000\"".to_string();
let err = verify_with_requirement(&f, &wrong).expect_err("wrong OU must fail");
assert!(matches!(err, AttestError::VerificationFailed { .. }));
let right = format!("=certificate leaf[subject.OU] = \"{ou}\"");
verify_with_requirement(&f, &right).expect("matching OU must pass");
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
}