Skip to main content

dscode_extension_host/
binary_verifier.rs

1//! Node.js binary integrity verification.
2//!
3//! On startup, the extension host verifies the SHA256 hash of the resolved
4//! Node.js binary against a list of known-good hashes. If the hash doesn't
5//! match, the extension host refuses to start.
6
7use sha2::{Sha256, Digest};
8use std::fs;
9use std::path::Path;
10use tracing::warn;
11
12/// Known-good SHA256 hashes for Node.js binaries.
13/// These are bundled with the application and checked at startup.
14const ALLOWED_HASHES: &[&str] = &[
15    // Node.js 20.x LTS (common versions)
16    // These should be updated when bundling specific Node.js versions
17    // For development, the DSCODE_SKIP_NODE_VERIFY env var can bypass this check
18];
19
20/// Verify the SHA256 hash of a binary file against allowed hashes.
21///
22/// Returns `Ok(())` if the hash matches, or `Err` with a description of the mismatch.
23/// If the `DSCODE_SKIP_NODE_VERIFY` environment variable is set, verification is skipped.
24pub fn verify_binary(binary_path: &Path) -> Result<(), String> {
25    // Allow bypass for development
26    if std::env::var("DSCODE_SKIP_NODE_VERIFY").is_ok() {
27        warn!("Node.js binary verification skipped (DSCODE_SKIP_NODE_VERIFY set)");
28        return Ok(());
29    }
30
31    // If no allowed hashes are configured, skip verification (dev mode)
32    if ALLOWED_HASHES.is_empty() {
33        return Ok(());
34    }
35
36    let hash = compute_file_hash(binary_path)?;
37
38    if ALLOWED_HASHES.contains(&hash.as_str()) {
39        Ok(())
40    } else {
41        Err(format!(
42            "Node.js binary hash mismatch: {} (expected one of: {:?})",
43            hash, ALLOWED_HASHES
44        ))
45    }
46}
47
48/// Compute the SHA256 hash of a file.
49fn compute_file_hash(path: &Path) -> Result<String, String> {
50    let data = fs::read(path)
51        .map_err(|e| format!("Failed to read binary {:?}: {}", path, e))?;
52
53    let mut hasher = Sha256::new();
54    hasher.update(&data);
55    let result = hasher.finalize();
56
57    Ok(format!("{:x}", result))
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use std::io::Write;
64
65    #[test]
66    fn test_compute_file_hash() {
67        let dir = tempfile::tempdir().unwrap();
68        let file_path = dir.path().join("test_binary");
69        let mut file = fs::File::create(&file_path).unwrap();
70        file.write_all(b"test content").unwrap();
71        drop(file);
72
73        let hash = compute_file_hash(&file_path).unwrap();
74        // SHA256 of "test content" is known
75        assert_eq!(hash.len(), 64); // SHA256 hex output is 64 chars
76        assert!(!hash.is_empty());
77    }
78
79    #[test]
80    fn test_verify_binary_skips_with_env() {
81        // This test verifies the env var bypass works
82        std::env::set_var("DSCODE_SKIP_NODE_VERIFY", "1");
83        let result = verify_binary(Path::new("/nonexistent"));
84        std::env::remove_var("DSCODE_SKIP_NODE_VERIFY");
85        assert!(result.is_ok());
86    }
87
88    #[test]
89    fn test_verify_binary_skips_empty_hashes() {
90        // With empty ALLOWED_HASHES, verification passes (dev mode)
91        let result = verify_binary(Path::new("/nonexistent"));
92        assert!(result.is_ok());
93    }
94}