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