use std::path::Path;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
pub const WASMTIME_VERSION: &str = "27";
pub fn target_triple() -> &'static str {
option_env!("TARGET_TRIPLE_OR_RUST_BUILT_IN").unwrap_or("host")
}
pub fn engine_config_hash(fingerprint: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(fingerprint.as_bytes());
hex::encode(hasher.finalize())
}
pub fn sha256_hex(bytes: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(bytes);
hex::encode(hasher.finalize())
}
pub fn sidecar_path(cwasm: &Path) -> std::path::PathBuf {
let mut s = cwasm.as_os_str().to_owned();
s.push(".meta");
s.into()
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CacheKey {
pub wasm_sha256: String,
pub wasmtime_version: String,
pub target_triple: String,
pub engine_config_hash: String,
}
impl CacheKey {
pub fn for_runtime(wasm_sha256: impl Into<String>, engine_fingerprint: &str) -> Self {
CacheKey {
wasm_sha256: wasm_sha256.into(),
wasmtime_version: WASMTIME_VERSION.to_string(),
target_triple: target_triple().to_string(),
engine_config_hash: engine_config_hash(engine_fingerprint),
}
}
pub fn matches(&self, other: &CacheKey) -> bool {
self.wasm_sha256 == other.wasm_sha256
&& self.wasmtime_version == other.wasmtime_version
&& self.target_triple == other.target_triple
&& self.engine_config_hash == other.engine_config_hash
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SidecarMeta {
pub schema: u32,
pub key: CacheKey,
}
impl SidecarMeta {
pub const SCHEMA_VERSION: u32 = 1;
#[allow(dead_code)] pub fn new(key: CacheKey) -> Self {
SidecarMeta {
schema: Self::SCHEMA_VERSION,
key,
}
}
pub fn read_from(path: &Path) -> Result<Self, String> {
let bytes = std::fs::read_to_string(path)
.map_err(|e| format!("read cwasm sidecar {}: {}", path.display(), e))?;
toml::from_str(&bytes).map_err(|e| format!("parse cwasm sidecar {}: {}", path.display(), e))
}
#[cfg(test)]
pub fn write_to(&self, path: &Path) -> Result<(), String> {
let s = toml::to_string(self)
.map_err(|e| format!("serialize cwasm sidecar {}: {}", path.display(), e))?;
std::fs::write(path, s)
.map_err(|e| format!("write cwasm sidecar {}: {}", path.display(), e))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CacheRejection {
Missing,
UidMismatch,
PermissionsTooOpen,
SidecarUnreadable(String),
KeyMismatch,
WasmShaMismatch,
}
impl CacheRejection {
pub fn as_str(&self) -> &'static str {
match self {
CacheRejection::Missing => "cwasm missing",
CacheRejection::UidMismatch => "cwasm not owned by current user",
CacheRejection::PermissionsTooOpen => "cwasm or cache dir permissions too permissive",
CacheRejection::SidecarUnreadable(_) => "cwasm sidecar unreadable",
CacheRejection::KeyMismatch => "cwasm cache key mismatch",
CacheRejection::WasmShaMismatch => "wasm SHA-256 mismatch",
}
}
}
pub fn validate_cwasm(
cwasm_path: &Path,
sidecar_path: &Path,
wasm_path: &Path,
runtime_key: &CacheKey,
) -> Result<(), CacheRejection> {
check_filesystem_trust(cwasm_path)?;
let meta = SidecarMeta::read_from(sidecar_path).map_err(CacheRejection::SidecarUnreadable)?;
if meta.schema != SidecarMeta::SCHEMA_VERSION {
return Err(CacheRejection::SidecarUnreadable(format!(
"schema {} != {}",
meta.schema,
SidecarMeta::SCHEMA_VERSION
)));
}
if !meta.key.matches(runtime_key) {
return Err(CacheRejection::KeyMismatch);
}
let wasm_bytes = std::fs::read(wasm_path).map_err(|_| CacheRejection::WasmShaMismatch)?;
let actual = sha256_hex(&wasm_bytes);
if actual != runtime_key.wasm_sha256 {
return Err(CacheRejection::WasmShaMismatch);
}
Ok(())
}
#[cfg(unix)]
fn check_filesystem_trust(cwasm_path: &Path) -> Result<(), CacheRejection> {
use std::os::unix::fs::MetadataExt;
let cwasm_meta = match std::fs::metadata(cwasm_path) {
Ok(m) => m,
Err(_) => return Err(CacheRejection::Missing),
};
let parent = cwasm_path.parent().ok_or(CacheRejection::Missing)?;
let parent_meta = match std::fs::metadata(parent) {
Ok(m) => m,
Err(_) => return Err(CacheRejection::Missing),
};
let current_uid = unsafe { libc::getuid() };
if cwasm_meta.uid() != current_uid || parent_meta.uid() != current_uid {
return Err(CacheRejection::UidMismatch);
}
let cwasm_mode = cwasm_meta.mode() & 0o777;
let parent_mode = parent_meta.mode() & 0o777;
if cwasm_mode != 0o600 || parent_mode != 0o700 {
return Err(CacheRejection::PermissionsTooOpen);
}
Ok(())
}
#[cfg(not(unix))]
fn check_filesystem_trust(cwasm_path: &Path) -> Result<(), CacheRejection> {
if !cwasm_path.exists() {
return Err(CacheRejection::Missing);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::unix::fs::PermissionsExt;
use tempfile::TempDir;
fn write_cwasm(dir: &Path, name: &str, content: &[u8]) -> std::path::PathBuf {
let path = dir.join(name);
std::fs::write(&path, content).unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).unwrap();
path
}
fn make_cache_dir(parent: &TempDir) -> std::path::PathBuf {
let dir = parent.path().join("cache");
std::fs::create_dir(&dir).unwrap();
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700)).unwrap();
dir
}
fn write_wasm(parent: &TempDir, name: &str, content: &[u8]) -> std::path::PathBuf {
let path = parent.path().join(name);
std::fs::write(&path, content).unwrap();
path
}
fn make_runtime_key(wasm_bytes: &[u8]) -> CacheKey {
CacheKey::for_runtime(sha256_hex(wasm_bytes), "test-fingerprint")
}
#[test]
fn validate_happy_path() {
let tmp = TempDir::new().unwrap();
let cache_dir = make_cache_dir(&tmp);
let wasm_bytes = b"fake wasm bytes";
let wasm_path = write_wasm(&tmp, "plugin.wasm", wasm_bytes);
let runtime_key = make_runtime_key(wasm_bytes);
let cwasm_path = write_cwasm(&cache_dir, "plugin.cwasm", b"fake cwasm");
let sidecar_path = cache_dir.join("plugin.cwasm.meta");
SidecarMeta::new(runtime_key.clone())
.write_to(&sidecar_path)
.unwrap();
std::fs::set_permissions(&sidecar_path, std::fs::Permissions::from_mode(0o600)).unwrap();
validate_cwasm(&cwasm_path, &sidecar_path, &wasm_path, &runtime_key)
.expect("validation should pass");
}
#[test]
fn rejects_missing_cwasm() {
let tmp = TempDir::new().unwrap();
let cache_dir = make_cache_dir(&tmp);
let wasm_bytes = b"fake wasm";
let wasm_path = write_wasm(&tmp, "plugin.wasm", wasm_bytes);
let runtime_key = make_runtime_key(wasm_bytes);
let cwasm_path = cache_dir.join("does-not-exist.cwasm");
let sidecar_path = cache_dir.join("does-not-exist.cwasm.meta");
let res = validate_cwasm(&cwasm_path, &sidecar_path, &wasm_path, &runtime_key);
assert_eq!(res.unwrap_err(), CacheRejection::Missing);
}
#[test]
fn rejects_world_readable_cwasm() {
let tmp = TempDir::new().unwrap();
let cache_dir = make_cache_dir(&tmp);
let wasm_bytes = b"fake wasm";
let wasm_path = write_wasm(&tmp, "plugin.wasm", wasm_bytes);
let runtime_key = make_runtime_key(wasm_bytes);
let cwasm_path = cache_dir.join("plugin.cwasm");
std::fs::write(&cwasm_path, b"cwasm bytes").unwrap();
std::fs::set_permissions(&cwasm_path, std::fs::Permissions::from_mode(0o644)).unwrap();
let sidecar_path = cache_dir.join("plugin.cwasm.meta");
SidecarMeta::new(runtime_key.clone())
.write_to(&sidecar_path)
.unwrap();
let res = validate_cwasm(&cwasm_path, &sidecar_path, &wasm_path, &runtime_key);
assert_eq!(res.unwrap_err(), CacheRejection::PermissionsTooOpen);
}
#[test]
fn rejects_key_tuple_mismatch() {
let tmp = TempDir::new().unwrap();
let cache_dir = make_cache_dir(&tmp);
let wasm_bytes = b"fake wasm";
let wasm_path = write_wasm(&tmp, "plugin.wasm", wasm_bytes);
let runtime_key = make_runtime_key(wasm_bytes);
let mut stale_key = runtime_key.clone();
stale_key.wasmtime_version = "26".to_string();
let cwasm_path = write_cwasm(&cache_dir, "plugin.cwasm", b"fake cwasm");
let sidecar_path = cache_dir.join("plugin.cwasm.meta");
SidecarMeta::new(stale_key).write_to(&sidecar_path).unwrap();
let res = validate_cwasm(&cwasm_path, &sidecar_path, &wasm_path, &runtime_key);
assert_eq!(res.unwrap_err(), CacheRejection::KeyMismatch);
}
#[test]
fn rejects_wasm_sha_mismatch() {
let tmp = TempDir::new().unwrap();
let cache_dir = make_cache_dir(&tmp);
let wasm_bytes = b"original wasm";
let wasm_path = write_wasm(&tmp, "plugin.wasm", wasm_bytes);
let runtime_key = make_runtime_key(wasm_bytes);
let cwasm_path = write_cwasm(&cache_dir, "plugin.cwasm", b"fake cwasm");
let sidecar_path = cache_dir.join("plugin.cwasm.meta");
SidecarMeta::new(runtime_key.clone())
.write_to(&sidecar_path)
.unwrap();
std::fs::write(&wasm_path, b"tampered wasm content").unwrap();
let res = validate_cwasm(&cwasm_path, &sidecar_path, &wasm_path, &runtime_key);
assert_eq!(res.unwrap_err(), CacheRejection::WasmShaMismatch);
}
#[test]
fn engine_config_hash_is_deterministic() {
let a = engine_config_hash("async=false;fuel=false");
let b = engine_config_hash("async=false;fuel=false");
let c = engine_config_hash("async=true;fuel=false");
assert_eq!(a, b, "same fingerprint must hash to the same digest");
assert_ne!(
a, c,
"different fingerprints must produce different digests"
);
}
}