mur_common/
binary_attestation.rs1use std::fmt;
6use std::path::{Path, PathBuf};
7use std::process::Command;
8
9pub const IS_EMBEDDED_RELEASE: bool = {
12 let bytes = env!("MUR_EMBEDDED_RELEASE").as_bytes();
15 bytes.len() == 1 && bytes[0] == b'1'
16};
17
18pub const APPLE_TEAM_ID: &str = env!("MUR_APPLE_TEAM_ID");
21
22pub(crate) fn production_requirement() -> String {
25 format!("=anchor apple generic and certificate leaf[subject.OU] = \"{APPLE_TEAM_ID}\"")
29}
30
31pub fn verify_runtime_signature(path: &Path) -> Result<(), AttestError> {
34 if !IS_EMBEDDED_RELEASE || !cfg!(target_os = "macos") {
35 return Ok(());
36 }
37 let real = path.canonicalize().map_err(|e| AttestError::Io {
40 path: path.to_path_buf(),
41 source: e,
42 })?;
43 verify_with_requirement(&real, &production_requirement())
44}
45
46#[doc(hidden)]
48pub fn verify_with_requirement(path: &Path, requirement: &str) -> Result<(), AttestError> {
49 let out = Command::new("codesign")
50 .args(["--verify", "--strict", "-R", requirement])
51 .arg(path)
52 .output()
53 .map_err(|e| AttestError::Io {
54 path: path.to_path_buf(),
55 source: e,
56 })?;
57 if out.status.success() {
58 Ok(())
59 } else {
60 Err(AttestError::VerificationFailed {
61 path: path.to_path_buf(),
62 stderr: String::from_utf8_lossy(&out.stderr).trim().to_string(),
63 })
64 }
65}
66
67#[derive(Debug)]
68pub enum AttestError {
69 VerificationFailed { path: PathBuf, stderr: String },
71 Io {
73 path: PathBuf,
74 source: std::io::Error,
75 },
76}
77
78impl fmt::Display for AttestError {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 match self {
81 Self::VerificationFailed { path, stderr } => write!(
82 f,
83 "runtime binary at {} failed signature verification: {stderr}",
84 path.display()
85 ),
86 Self::Io { path, source } => {
87 write!(
88 f,
89 "cannot verify runtime binary at {}: {source}",
90 path.display()
91 )
92 }
93 }
94 }
95}
96
97impl std::error::Error for AttestError {
98 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
99 match self {
100 Self::Io { source, .. } => Some(source),
101 Self::VerificationFailed { .. } => None,
102 }
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 #[cfg(unix)]
110 use std::path::PathBuf;
111
112 #[test]
116 #[allow(clippy::assertions_on_constants)] fn dev_build_never_verifies() {
118 assert!(!IS_EMBEDDED_RELEASE);
119 assert!(verify_runtime_signature(Path::new("/nonexistent/nope")).is_ok());
121 }
122
123 #[test]
124 fn production_requirement_binds_anchor_and_team() {
125 let req = production_requirement();
126 assert!(req.contains("anchor apple generic"), "req: {req}");
127 assert!(req.contains("subject.OU"), "req: {req}");
128 assert!(req.starts_with("=anchor apple generic and"), "req: {req}");
129 }
130
131 #[cfg(unix)]
135 fn test_dir(name: &str) -> PathBuf {
136 let d = std::env::temp_dir().join(format!("mur-attest-{}-{}", name, std::process::id()));
137 let _ = std::fs::remove_dir_all(&d);
138 std::fs::create_dir_all(&d).unwrap();
139 d
140 }
141
142 #[cfg(unix)]
143 fn test_ou() -> Option<String> {
144 std::env::var("MUR_TEST_SIGNING_OU").ok()
145 }
146
147 #[test]
148 #[cfg(unix)] fn unsigned_file_fails_test_requirement() {
150 let Some(ou) = test_ou() else {
151 eprintln!("skipping: MUR_TEST_SIGNING_OU not set");
152 return;
153 };
154 let dir = test_dir("unsigned");
155 let f = dir.join("runtime");
156 std::fs::write(&f, b"#!/bin/sh\nexit 0\n").unwrap();
157 std::fs::set_permissions(&f, std::fs::Permissions::from_mode(0o755)).unwrap();
158 let req = format!("=certificate leaf[subject.OU] = \"{ou}\"");
159 let err = verify_with_requirement(&f, &req).expect_err("unsigned must fail");
160 assert!(matches!(err, AttestError::VerificationFailed { .. }));
161 let _ = std::fs::remove_dir_all(&dir);
162 }
163
164 #[test]
165 #[cfg(unix)] fn adhoc_signed_fails_test_requirement() {
167 let Some(ou) = test_ou() else {
168 eprintln!("skipping");
169 return;
170 };
171 let dir = test_dir("adhoc");
172 let f = dir.join("runtime");
173 std::fs::write(&f, b"#!/bin/sh\nexit 0\n").unwrap();
174 std::fs::set_permissions(&f, std::fs::Permissions::from_mode(0o755)).unwrap();
175 let out = std::process::Command::new("codesign")
176 .args(["--force", "-s", "-"])
177 .arg(&f)
178 .output()
179 .unwrap();
180 assert!(
181 out.status.success(),
182 "ad-hoc sign failed: {}",
183 String::from_utf8_lossy(&out.stderr)
184 );
185 let req = format!("=certificate leaf[subject.OU] = \"{ou}\"");
186 let err = verify_with_requirement(&f, &req).expect_err("ad-hoc (no OU) must fail");
187 assert!(matches!(err, AttestError::VerificationFailed { .. }));
188 let _ = std::fs::remove_dir_all(&dir);
189 }
190
191 #[test]
192 #[cfg(unix)] fn wrong_ou_fails_test_requirement() {
194 let Some(ou) = test_ou() else {
195 eprintln!("skipping");
196 return;
197 };
198 let dir = test_dir("wrongou");
199 let f = dir.join("runtime");
200 std::fs::write(&f, b"#!/bin/sh\nexit 0\n").unwrap();
201 std::fs::set_permissions(&f, std::fs::Permissions::from_mode(0o755)).unwrap();
202 let out = std::process::Command::new("codesign")
203 .args(["--force", "-s", &format!("Mur Test ({ou})")])
204 .arg(&f)
205 .output()
206 .unwrap();
207 assert!(
208 out.status.success(),
209 "sign failed: {}",
210 String::from_utf8_lossy(&out.stderr)
211 );
212 let wrong = "=certificate leaf[subject.OU] = \"WRONGTEAM000\"".to_string();
213 let err = verify_with_requirement(&f, &wrong).expect_err("wrong OU must fail");
214 assert!(matches!(err, AttestError::VerificationFailed { .. }));
215 let right = format!("=certificate leaf[subject.OU] = \"{ou}\"");
217 verify_with_requirement(&f, &right).expect("matching OU must pass");
218 let _ = std::fs::remove_dir_all(&dir);
219 }
220
221 #[cfg(unix)]
222 use std::os::unix::fs::PermissionsExt;
223}