1#![forbid(unsafe_code)]
8
9pub mod sign;
10pub mod trust;
11pub use sign::{minisign_verify, parse_minisign_pubkey, MinisignKey};
12
13use serde::{Deserialize, Serialize};
14use sha2::{Digest, Sha256};
15use std::fs::File;
16use std::io::Read;
17use std::path::Path;
18use vanta_core::{Area, VtaError, VtaResult};
19
20pub fn sha256_file(path: &Path) -> VtaResult<String> {
22 let mut hasher = Sha256::new();
23 hash_into(path, &mut |chunk| hasher.update(chunk))?;
24 Ok(hasher
25 .finalize()
26 .iter()
27 .map(|b| format!("{b:02x}"))
28 .collect())
29}
30
31pub fn blake3_file(path: &Path) -> VtaResult<String> {
33 let mut hasher = blake3::Hasher::new();
34 hash_into(path, &mut |chunk| {
35 hasher.update(chunk);
36 })?;
37 Ok(hasher.finalize().to_hex().to_string())
38}
39
40fn hash_into(path: &Path, sink: &mut dyn FnMut(&[u8])) -> VtaResult<()> {
41 let mut file = File::open(path)
42 .map_err(|e| VtaError::new(Area::Vrf, 1, format!("opening {}: {e}", path.display())))?;
43 let mut buf = [0u8; 65536];
44 loop {
45 let n = file
46 .read(&mut buf)
47 .map_err(|e| VtaError::new(Area::Vrf, 1, format!("reading {}: {e}", path.display())))?;
48 if n == 0 {
49 break;
50 }
51 sink(&buf[..n]);
52 }
53 Ok(())
54}
55
56pub fn verify_file(path: &Path, algo: &str, expected: &str) -> VtaResult<()> {
59 let got = match algo.to_ascii_lowercase().as_str() {
60 "sha256" => sha256_file(path)?,
61 "blake3" => blake3_file(path)?,
62 other => {
63 return Err(VtaError::new(
64 Area::Vrf,
65 2,
66 format!("unsupported checksum algorithm `{other}`"),
67 ))
68 }
69 };
70 if got.eq_ignore_ascii_case(expected) {
71 Ok(())
72 } else {
73 Err(VtaError::new(
74 Area::Vrf,
75 1,
76 format!("checksum mismatch ({algo}): expected {expected}, got {got}"),
77 ))
78 }
79}
80
81#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
83#[serde(default, deny_unknown_fields)]
84pub struct Policy {
85 pub require_signature: bool,
86 pub forbid_no_verify: bool,
87 pub allow_source_builds: Option<bool>,
88 pub min_slsa_level: Option<u8>,
89 pub allow_tools: Vec<String>,
91 pub deny_tools: Vec<String>,
92 pub allow_licenses: Vec<String>,
93}
94
95impl Policy {
96 pub fn from_toml(src: &str) -> VtaResult<Policy> {
97 toml::from_str(src).map_err(|e| VtaError::new(Area::Vrf, 3, format!("parse policy: {e}")))
98 }
99
100 pub fn allows_tool(&self, tool: &str) -> bool {
102 if self.deny_tools.iter().any(|p| matches_pattern(p, tool)) {
103 return false;
104 }
105 self.allow_tools.is_empty() || self.allow_tools.iter().any(|p| matches_pattern(p, tool))
106 }
107
108 pub fn check(&self, tool: &str, license: Option<&str>) -> VtaResult<()> {
110 if !self.allows_tool(tool) {
111 return Err(VtaError::new(
112 Area::Res,
113 6,
114 format!("tool `{tool}` is denied by org policy"),
115 ));
116 }
117 if let (false, Some(lic)) = (self.allow_licenses.is_empty(), license) {
118 if !self
119 .allow_licenses
120 .iter()
121 .any(|l| l.eq_ignore_ascii_case(lic))
122 {
123 return Err(VtaError::new(
124 Area::Res,
125 6,
126 format!("license `{lic}` for `{tool}` is not in the policy allow-list"),
127 ));
128 }
129 }
130 Ok(())
131 }
132}
133
134fn matches_pattern(pattern: &str, name: &str) -> bool {
135 match pattern.strip_suffix('*') {
136 Some(prefix) => name.starts_with(prefix),
137 None => pattern == name,
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn verify_sha256_roundtrip() {
147 let path = std::env::temp_dir().join(format!("vanta-sec-{}.bin", std::process::id()));
148 std::fs::write(&path, b"hello world").unwrap();
149 let digest = sha256_file(&path).unwrap();
150 assert!(verify_file(&path, "sha256", &digest).is_ok());
151 assert!(verify_file(&path, "sha256", "deadbeef").is_err());
152 assert!(verify_file(&path, "md5", &digest).is_err()); let _ = std::fs::remove_file(&path);
154 }
155
156 #[test]
157 fn policy_tool_rules() {
158 let p = Policy {
159 allow_tools: vec!["node".into(), "acme/*".into()],
160 deny_tools: vec!["leftpad".into()],
161 ..Default::default()
162 };
163 assert!(p.allows_tool("node"));
164 assert!(p.allows_tool("acme/deploy"));
165 assert!(!p.allows_tool("python")); assert!(!p.allows_tool("leftpad")); assert!(p.check("node", None).is_ok());
168 assert!(p.check("python", None).is_err());
169 }
170
171 #[test]
172 fn policy_license_allowlist() {
173 let p = Policy {
174 allow_licenses: vec!["MIT".into(), "Apache-2.0".into()],
175 ..Default::default()
176 };
177 assert!(p.check("node", Some("MIT")).is_ok());
178 assert!(p.check("node", Some("GPL-3.0")).is_err());
179 }
180}