void_core/collab/manifest/
verify.rs1use crate::collab::Identity;
9use crate::metadata::Commit;
10
11use super::keys::SigningPubKey;
12#[cfg(test)]
13use super::keys::CommitSignature;
14use super::policy::{check_write_access, AuthResult};
15use super::types::Manifest;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum VerifyResult {
24 Ok,
26 InvalidSignature,
28 Unsigned,
30 Unauthorized {
32 signer: SigningPubKey,
34 ref_path: String,
36 },
37}
38
39impl VerifyResult {
40 pub fn is_ok(&self) -> bool {
42 matches!(self, VerifyResult::Ok)
43 }
44
45 pub fn is_unsigned(&self) -> bool {
47 matches!(self, VerifyResult::Unsigned)
48 }
49}
50
51pub fn verify_commit(
68 commit: &Commit,
69 ref_path: &str,
70 manifest: Option<&Manifest>,
71) -> VerifyResult {
72 let signer = match verify_commit_signature(commit) {
74 SignatureResult::Valid(signer) => signer,
75 SignatureResult::Invalid => return VerifyResult::InvalidSignature,
76 SignatureResult::Unsigned => return VerifyResult::Unsigned,
77 };
78
79 let Some(manifest) = manifest else {
81 return VerifyResult::Ok;
82 };
83
84 match check_write_access(manifest, &signer, ref_path) {
86 AuthResult::Allowed | AuthResult::Delegated { .. } => VerifyResult::Ok,
87 AuthResult::Denied => VerifyResult::Unauthorized {
88 signer,
89 ref_path: ref_path.to_string(),
90 },
91 }
92}
93
94pub fn verify_commits<'a>(
103 commits: impl Iterator<Item = &'a Commit>,
104 ref_path: &str,
105 manifest: Option<&Manifest>,
106) -> VerifyResult {
107 for commit in commits {
108 let result = verify_commit(commit, ref_path, manifest);
109 if !result.is_ok() {
110 return result;
111 }
112 }
113 VerifyResult::Ok
114}
115
116pub fn verify_signature_only(commit: &Commit) -> VerifyResult {
121 match verify_commit_signature(commit) {
122 SignatureResult::Valid(_) => VerifyResult::Ok,
123 SignatureResult::Invalid => VerifyResult::InvalidSignature,
124 SignatureResult::Unsigned => VerifyResult::Unsigned,
125 }
126}
127
128enum SignatureResult {
134 Valid(SigningPubKey),
136 Invalid,
138 Unsigned,
140}
141
142fn verify_commit_signature(commit: &Commit) -> SignatureResult {
144 let (author, signature) = match (&commit.author, &commit.signature) {
145 (Some(a), Some(s)) => (a, s),
146 (None, None) => return SignatureResult::Unsigned,
147 _ => return SignatureResult::Invalid,
149 };
150
151 let signable = commit.signable_bytes();
153
154 if Identity::verify(author, &signable, signature.as_bytes()) {
156 SignatureResult::Valid(*author)
157 } else {
158 SignatureResult::Invalid
159 }
160}
161
162pub fn extract_signer(commit: &Commit) -> Option<SigningPubKey> {
166 commit.author
167}
168
169#[cfg(test)]
174mod tests {
175 use super::*;
176 use ed25519_dalek::SigningKey;
177
178 fn test_signing_key(val: u8) -> SigningPubKey {
179 SigningPubKey::from_bytes([val; 32])
180 }
181
182 fn create_unsigned_commit() -> Commit {
183 Commit {
184 parents: vec![],
185 metadata_bundle: void_crypto::MetadataCid::from_bytes(vec![1, 2, 3]),
186 timestamp: 12345,
187 message: "test commit".to_string(),
188 author: None,
189 signature: None,
190 manifest_cid: None,
191 stats: None,
192 repo_manifest_cid: None,
193 }
194 }
195
196 fn create_signed_commit(signing_key: &SigningKey) -> Commit {
197 let mut commit = create_unsigned_commit();
198 commit.sign(signing_key);
199 commit
200 }
201
202 fn create_invalid_signed_commit() -> Commit {
203 let mut commit = create_unsigned_commit();
204 commit.author = Some(SigningPubKey::from_bytes([0xaa; 32]));
205 commit.signature = Some(CommitSignature::from_bytes([0xbb; 64])); commit
207 }
208
209 #[test]
210 fn verify_unsigned_commit() {
211 let commit = create_unsigned_commit();
212 let result = verify_commit(&commit, "refs/heads/main", None);
213 assert_eq!(result, VerifyResult::Unsigned);
214 assert!(result.is_unsigned());
215 }
216
217 #[test]
218 fn verify_valid_signature_no_manifest() {
219 let signing_key = SigningKey::from_bytes(&[0x42; 32]);
220 let commit = create_signed_commit(&signing_key);
221
222 let result = verify_commit(&commit, "refs/heads/main", None);
223 assert_eq!(result, VerifyResult::Ok);
224 assert!(result.is_ok());
225 }
226
227 #[test]
228 fn verify_invalid_signature() {
229 let commit = create_invalid_signed_commit();
230 let result = verify_commit(&commit, "refs/heads/main", None);
231 assert_eq!(result, VerifyResult::InvalidSignature);
232 }
233
234 #[test]
235 fn verify_authorized_signer() {
236 let signing_key = SigningKey::from_bytes(&[0x42; 32]);
237 let pubkey = SigningPubKey::from_bytes(signing_key.verifying_key().to_bytes());
238 let commit = create_signed_commit(&signing_key);
239
240 let manifest = Manifest::new(pubkey, None);
242
243 let result = verify_commit(&commit, "refs/heads/main", Some(&manifest));
244 assert_eq!(result, VerifyResult::Ok);
245 }
246
247 #[test]
248 fn verify_unauthorized_signer() {
249 let signing_key = SigningKey::from_bytes(&[0x42; 32]);
250 let commit = create_signed_commit(&signing_key);
251
252 let manifest = Manifest::new(test_signing_key(0xaa), None);
254
255 let result = verify_commit(&commit, "refs/heads/main", Some(&manifest));
256 assert!(matches!(result, VerifyResult::Unauthorized { .. }));
257 }
258
259 #[test]
260 fn verify_commits_all_valid() {
261 let signing_key = SigningKey::from_bytes(&[0x42; 32]);
262 let commit1 = create_signed_commit(&signing_key);
263 let commit2 = create_signed_commit(&signing_key);
264 let commits = vec![commit1, commit2];
265
266 let result = verify_commits(commits.iter(), "refs/heads/main", None);
267 assert_eq!(result, VerifyResult::Ok);
268 }
269
270 #[test]
271 fn verify_commits_first_failure() {
272 let signing_key = SigningKey::from_bytes(&[0x42; 32]);
273 let valid_commit = create_signed_commit(&signing_key);
274 let invalid_commit = create_invalid_signed_commit();
275 let commits = vec![valid_commit, invalid_commit];
276
277 let result = verify_commits(commits.iter(), "refs/heads/main", None);
278 assert_eq!(result, VerifyResult::InvalidSignature);
280 }
281
282 #[test]
283 fn extract_signer_from_signed() {
284 let signing_key = SigningKey::from_bytes(&[0x42; 32]);
285 let commit = create_signed_commit(&signing_key);
286
287 let signer = extract_signer(&commit);
288 assert!(signer.is_some());
289 assert_eq!(
290 signer.unwrap().as_bytes(),
291 &signing_key.verifying_key().to_bytes()
292 );
293 }
294
295 #[test]
296 fn extract_signer_from_unsigned() {
297 let commit = create_unsigned_commit();
298 let signer = extract_signer(&commit);
299 assert!(signer.is_none());
300 }
301
302 #[test]
303 fn verify_signature_only_valid() {
304 let signing_key = SigningKey::from_bytes(&[0x42; 32]);
305 let commit = create_signed_commit(&signing_key);
306
307 let result = verify_signature_only(&commit);
308 assert_eq!(result, VerifyResult::Ok);
309 }
310
311 #[test]
312 fn verify_signature_only_unsigned() {
313 let commit = create_unsigned_commit();
314 let result = verify_signature_only(&commit);
315 assert_eq!(result, VerifyResult::Unsigned);
316 }
317
318 #[test]
319 fn partial_signature_is_invalid() {
320 let mut commit = create_unsigned_commit();
322 commit.author = Some(SigningPubKey::from_bytes([0xaa; 32]));
323 let result = verify_signature_only(&commit);
324 assert_eq!(result, VerifyResult::InvalidSignature);
325
326 let mut commit = create_unsigned_commit();
328 commit.signature = Some(CommitSignature::from_bytes([0xbb; 64]));
329 let result = verify_signature_only(&commit);
330 assert_eq!(result, VerifyResult::InvalidSignature);
331 }
332
333 #[test]
334 fn verify_result_methods() {
335 assert!(VerifyResult::Ok.is_ok());
336 assert!(!VerifyResult::InvalidSignature.is_ok());
337 assert!(!VerifyResult::Unsigned.is_ok());
338
339 assert!(!VerifyResult::Ok.is_unsigned());
340 assert!(VerifyResult::Unsigned.is_unsigned());
341 }
342}