1use anyhow::{Context, Result};
8use std::path::Path;
9
10const MAGIC: &[u8; 4] = b"NSIG";
11const SIG_LEN: usize = 4 + 32; #[derive(Debug, PartialEq)]
14pub enum NanoSignResult {
15 Verified(blake3::Hash),
16 Failed { expected: [u8; 32], actual: blake3::Hash },
17 Unsigned,
18}
19
20pub fn sign(path: &Path) -> Result<blake3::Hash> {
22 let data = std::fs::read(path).with_context(|| format!("read {}", path.display()))?;
23 let hash = blake3::hash(&data);
24 let mut f = std::fs::OpenOptions::new()
25 .append(true)
26 .open(path)
27 .with_context(|| format!("open for append {}", path.display()))?;
28 std::io::Write::write_all(&mut f, MAGIC)?;
29 std::io::Write::write_all(&mut f, hash.as_bytes())?;
30 Ok(hash)
31}
32
33pub fn verify(path: &Path) -> Result<NanoSignResult> {
35 let data = std::fs::read(path).with_context(|| format!("read {}", path.display()))?;
36 Ok(verify_bytes(&data))
37}
38
39pub fn verify_bytes(data: &[u8]) -> NanoSignResult {
41 if data.len() < SIG_LEN {
42 return NanoSignResult::Unsigned;
43 }
44 let (payload, sig) = data.split_at(data.len() - SIG_LEN);
45 if &sig[..4] != MAGIC {
46 return NanoSignResult::Unsigned;
47 }
48 let mut expected = [0u8; 32];
49 expected.copy_from_slice(&sig[4..]);
50 let actual = blake3::hash(payload);
51 if *actual.as_bytes() == expected {
52 NanoSignResult::Verified(actual)
53 } else {
54 NanoSignResult::Failed { expected, actual }
55 }
56}
57
58pub fn sign_bytes(data: &[u8]) -> Vec<u8> {
60 let hash = blake3::hash(data);
61 let mut out = Vec::with_capacity(data.len() + SIG_LEN);
62 out.extend_from_slice(data);
63 out.extend_from_slice(MAGIC);
64 out.extend_from_slice(hash.as_bytes());
65 out
66}
67
68pub fn strip_bytes(data: &[u8]) -> &[u8] {
71 if data.len() >= SIG_LEN && &data[data.len() - SIG_LEN..data.len() - 32] == MAGIC {
72 &data[..data.len() - SIG_LEN]
73 } else {
74 data
75 }
76}
77
78pub fn save_signed(path: &Path, data: &[u8]) -> Result<blake3::Hash> {
80 let hash = blake3::hash(data);
81 let mut out = Vec::with_capacity(data.len() + SIG_LEN);
82 out.extend_from_slice(data);
83 out.extend_from_slice(MAGIC);
84 out.extend_from_slice(hash.as_bytes());
85 std::fs::write(path, &out).with_context(|| format!("write {}", path.display()))?;
86 Ok(hash)
87}
88
89pub fn load_verified(path: &Path) -> Result<Vec<u8>> {
92 let data = std::fs::read(path).with_context(|| format!("read {}", path.display()))?;
93 match verify_bytes(&data) {
94 NanoSignResult::Verified(_) => {
95 Ok(data[..data.len() - SIG_LEN].to_vec())
96 }
97 NanoSignResult::Failed { expected, actual } => {
98 anyhow::bail!(
99 "NanoSign FAILED for {}: expected {}, got {} — file tampered or corrupted",
100 path.display(),
101 hex(&expected),
102 actual.to_hex()
103 );
104 }
105 NanoSignResult::Unsigned => {
106 eprintln!(" nanosign: {} is unsigned (no NSIG marker)", path.display());
107 Ok(data)
108 }
109 }
110}
111
112fn hex(bytes: &[u8]) -> String {
113 bytes.iter().map(|b| format!("{b:02x}")).collect()
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn test_sign_and_verify_bytes() {
122 let payload = b"model weights go here";
123 let signed = sign_bytes(payload);
124 assert_eq!(signed.len(), payload.len() + 36);
125 assert_eq!(&signed[signed.len() - 36..signed.len() - 32], b"NSIG");
126 match verify_bytes(&signed) {
127 NanoSignResult::Verified(hash) => {
128 assert_eq!(hash, blake3::hash(payload));
129 }
130 other => panic!("expected Verified, got {other:?}"),
131 }
132 }
133
134 #[test]
135 fn test_unsigned_detection() {
136 let data = b"just some random data without a signature";
137 assert_eq!(verify_bytes(data), NanoSignResult::Unsigned);
138 }
139
140 #[test]
141 fn test_tamper_detection() {
142 let mut signed = sign_bytes(b"original data");
143 signed[0] = b'X';
145 match verify_bytes(&signed) {
146 NanoSignResult::Failed { .. } => {} other => panic!("expected Failed, got {other:?}"),
148 }
149 }
150
151 #[test]
152 fn test_strip_bytes() {
153 let payload = b"model data";
154 let signed = sign_bytes(payload);
155 let stripped = strip_bytes(&signed);
156 assert_eq!(stripped, payload);
157 }
158
159 #[test]
160 fn test_strip_unsigned() {
161 let data = b"no signature here";
162 assert_eq!(strip_bytes(data), data.as_slice());
163 }
164
165 #[test]
166 fn test_too_short() {
167 let data = b"short";
168 assert_eq!(verify_bytes(data), NanoSignResult::Unsigned);
169 }
170
171 #[test]
172 fn test_empty_payload() {
173 let signed = sign_bytes(b"");
174 match verify_bytes(&signed) {
175 NanoSignResult::Verified(hash) => {
176 assert_eq!(hash, blake3::hash(b""));
177 }
178 other => panic!("expected Verified, got {other:?}"),
179 }
180 }
181
182 #[test]
183 fn test_file_roundtrip() {
184 let dir = std::env::temp_dir().join("nanosign_test");
185 std::fs::create_dir_all(&dir).unwrap();
186 let path = dir.join("test_model.weights");
187
188 let payload = vec![42u8; 1024];
189 let hash = save_signed(&path, &payload).unwrap();
190 assert_eq!(hash, blake3::hash(&payload));
191
192 let loaded = load_verified(&path).unwrap();
193 assert_eq!(loaded, payload);
194
195 let mut raw = std::fs::read(&path).unwrap();
197 raw[0] = 0xFF;
198 std::fs::write(&path, &raw).unwrap();
199 assert!(load_verified(&path).is_err());
200
201 std::fs::remove_dir_all(&dir).ok();
202 }
203
204 #[test]
205 fn test_sign_bytes_deterministic() {
206 let data = b"same data twice";
207 let s1 = sign_bytes(data);
208 let s2 = sign_bytes(data);
209 assert_eq!(s1, s2);
210 }
211
212 #[test]
213 fn test_verify_exactly_36_bytes() {
214 let mut data = vec![0u8; 36];
216 data[0..4].copy_from_slice(b"NSIG");
217 match verify_bytes(&data) {
219 NanoSignResult::Failed { .. } => {} NanoSignResult::Verified(_) => panic!("should not verify with wrong hash"),
221 NanoSignResult::Unsigned => panic!("should detect NSIG magic"),
222 }
223 }
224
225 #[test]
226 fn test_strip_bytes_no_false_positive() {
227 let mut data = vec![0u8; 100];
229 data[10..14].copy_from_slice(b"NSIG"); let stripped = strip_bytes(&data);
231 assert_eq!(stripped.len(), 100); }
233
234 #[test]
235 fn test_sign_and_strip_roundtrip() {
236 let payload = b"round trip test data";
237 let signed = sign_bytes(payload);
238 let stripped = strip_bytes(&signed);
239 assert_eq!(stripped, payload);
240 }
241
242 #[test]
243 fn test_load_verified_unsigned_warning() {
244 let dir = std::env::temp_dir().join("nanosign_test_unsigned");
246 std::fs::create_dir_all(&dir).unwrap();
247 let path = dir.join("unsigned.bin");
248 std::fs::write(&path, b"no signature").unwrap();
249 let data = load_verified(&path).unwrap();
250 assert_eq!(data, b"no signature");
251 std::fs::remove_dir_all(&dir).ok();
252 }
253}