Skip to main content

smolvm_pack/
signing.rs

1//! Code signing support for packed executables.
2//!
3//! On macOS, executables that use Hypervisor.framework must be signed
4//! with the appropriate entitlements.
5
6use std::path::Path;
7
8#[cfg(target_os = "macos")]
9use std::fs;
10#[cfg(target_os = "macos")]
11use std::process::Command;
12
13#[cfg(target_os = "macos")]
14use crate::PackError;
15use crate::Result;
16
17/// Default entitlements for packed binaries on macOS.
18///
19/// - `hypervisor`: access to Hypervisor.framework for the microVM
20/// - `cs.disable-library-validation`: allow dlopen of libkrun from
21///   the extracted cache directory (ad-hoc signed libraries)
22/// - `cs.allow-jit`: a forkable golden (`machine start --forkable`) maps guest
23///   RAM from a file-backed region that HVF executes guest code from; under the
24///   hardened runtime that needs a JIT entitlement or `krun_start_enter` returns
25///   -22. (`allow-unsigned-executable-memory` also works; `allow-jit` is the
26///   narrower capability. Harmless for non-forkable VMs, which use anon RAM.)
27#[cfg(target_os = "macos")]
28const HYPERVISOR_ENTITLEMENTS: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
29<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
30<plist version="1.0">
31<dict>
32    <key>com.apple.security.hypervisor</key>
33    <true/>
34    <key>com.apple.security.cs.disable-library-validation</key>
35    <true/>
36    <key>com.apple.security.cs.allow-jit</key>
37    <true/>
38</dict>
39</plist>
40"#;
41
42/// Sign a binary with hypervisor entitlements (macOS only).
43///
44/// This uses ad-hoc signing (no certificate required) which is sufficient
45/// for local development and distribution.
46#[cfg(target_os = "macos")]
47pub fn sign_with_hypervisor_entitlements(binary_path: &Path) -> Result<()> {
48    // Reset SIGCHLD to default before spawning codesign.
49    // The agent VM startup installs a SIGCHLD handler that reaps all children
50    // via waitpid(-1), which races with Command::output()'s internal waitpid
51    // and causes ECHILD (os error 10).
52    unsafe {
53        libc::signal(libc::SIGCHLD, libc::SIG_DFL);
54    }
55
56    // Create temporary entitlements file
57    let temp_dir = tempfile::tempdir()?;
58    let entitlements_path = temp_dir.path().join("entitlements.plist");
59    fs::write(&entitlements_path, HYPERVISOR_ENTITLEMENTS)?;
60
61    // Run codesign
62    let output = Command::new("codesign")
63        .args([
64            "--force",
65            "--sign",
66            "-", // Ad-hoc signing
67            "--entitlements",
68        ])
69        .arg(&entitlements_path)
70        .arg(binary_path)
71        .output()?;
72
73    if !output.status.success() {
74        let stderr = String::from_utf8_lossy(&output.stderr);
75        return Err(PackError::Signing(format!(
76            "codesign failed: {}",
77            stderr.trim()
78        )));
79    }
80
81    Ok(())
82}
83
84/// Sign a binary with hypervisor entitlements (no-op on non-macOS).
85#[cfg(not(target_os = "macos"))]
86pub fn sign_with_hypervisor_entitlements(_binary_path: &Path) -> Result<()> {
87    // No signing needed on Linux
88    Ok(())
89}
90
91/// Check if a binary is signed (macOS only).
92#[cfg(target_os = "macos")]
93pub fn is_signed(binary_path: &Path) -> Result<bool> {
94    let output = Command::new("codesign")
95        .args(["--verify", "--verbose"])
96        .arg(binary_path)
97        .output()?;
98
99    Ok(output.status.success())
100}
101
102/// Check if a binary is signed (always false on non-macOS).
103#[cfg(not(target_os = "macos"))]
104pub fn is_signed(_binary_path: &Path) -> Result<bool> {
105    Ok(false)
106}
107
108/// Get signature information for a binary (macOS only).
109#[cfg(target_os = "macos")]
110pub fn get_signature_info(binary_path: &Path) -> Result<Option<SignatureInfo>> {
111    let output = Command::new("codesign")
112        .args(["--display", "--verbose=2"])
113        .arg(binary_path)
114        .output()?;
115
116    if !output.status.success() {
117        return Ok(None);
118    }
119
120    let stderr = String::from_utf8_lossy(&output.stderr);
121    let is_adhoc = stderr.contains("Signature=adhoc");
122
123    Ok(Some(SignatureInfo {
124        is_adhoc,
125        raw_output: stderr.to_string(),
126    }))
127}
128
129/// Get signature information (always None on non-macOS).
130#[cfg(not(target_os = "macos"))]
131pub fn get_signature_info(_binary_path: &Path) -> Result<Option<SignatureInfo>> {
132    Ok(None)
133}
134
135/// Information about a code signature.
136#[derive(Debug, Clone)]
137pub struct SignatureInfo {
138    /// Whether this is ad-hoc signed.
139    pub is_adhoc: bool,
140    /// Raw codesign output.
141    pub raw_output: String,
142}
143
144// Every test here drives `codesign`, so the whole module is macOS-only —
145// otherwise its imports are unused on other platforms.
146#[cfg(all(test, target_os = "macos"))]
147mod tests {
148    use super::*;
149    use std::io::Write;
150
151    #[test]
152    fn test_sign_binary() {
153        let temp_dir = tempfile::tempdir().unwrap();
154        let binary_path = temp_dir.path().join("test_binary");
155
156        // Create a minimal Mach-O binary (just the header)
157        // This is a valid but non-functional binary that codesign can process
158        let mut file = fs::File::create(&binary_path).unwrap();
159
160        // Minimal arm64 Mach-O header
161        let macho_header: [u8; 32] = [
162            0xCF, 0xFA, 0xED, 0xFE, // Magic (64-bit)
163            0x0C, 0x00, 0x00, 0x01, // CPU type (ARM64)
164            0x00, 0x00, 0x00, 0x00, // CPU subtype
165            0x02, 0x00, 0x00, 0x00, // File type (MH_EXECUTE)
166            0x00, 0x00, 0x00, 0x00, // Number of load commands
167            0x00, 0x00, 0x00, 0x00, // Size of load commands
168            0x00, 0x00, 0x00, 0x00, // Flags
169            0x00, 0x00, 0x00, 0x00, // Reserved
170        ];
171        file.write_all(&macho_header).unwrap();
172        drop(file);
173
174        // Make executable
175        #[cfg(unix)]
176        {
177            use std::os::unix::fs::PermissionsExt;
178            let mut perms = fs::metadata(&binary_path).unwrap().permissions();
179            perms.set_mode(0o755);
180            fs::set_permissions(&binary_path, perms).unwrap();
181        }
182
183        // Sign it
184        let result = sign_with_hypervisor_entitlements(&binary_path);
185        // This may fail if the binary isn't valid enough for codesign,
186        // but we're testing that the function runs without panicking
187        if let Err(e) = &result {
188            eprintln!("Signing failed (expected for minimal test binary): {}", e);
189        }
190    }
191
192    #[test]
193    fn test_entitlements_format() {
194        // Verify the entitlements XML is valid
195        assert!(HYPERVISOR_ENTITLEMENTS.contains("com.apple.security.hypervisor"));
196        assert!(HYPERVISOR_ENTITLEMENTS.contains("cs.disable-library-validation"));
197        assert!(HYPERVISOR_ENTITLEMENTS.contains("<true/>"));
198    }
199}