Skip to main content

entrenar/cli/commands/research/
verify.rs

1//! Research verify subcommand
2
3use crate::cli::logging::log;
4use crate::cli::LogLevel;
5use crate::config::VerifyArgs;
6use crate::research::{PreRegistration, SignedPreRegistration, TimestampProof};
7
8/// Result of signature verification
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum SignatureStatus {
11    /// Signature is valid
12    Valid,
13    /// Signature is invalid
14    Invalid,
15    /// Verification error occurred
16    Error(String),
17}
18
19/// Verify signature on a signed pre-registration
20pub fn verify_signature(signed: &SignedPreRegistration) -> SignatureStatus {
21    match signed.verify() {
22        Ok(true) => SignatureStatus::Valid,
23        Ok(false) => SignatureStatus::Invalid,
24        Err(e) => SignatureStatus::Error(e.to_string()),
25    }
26}
27
28/// Log git timestamp proof information
29pub fn log_git_proof(level: LogLevel, proof: &TimestampProof) {
30    if proof.is_git() {
31        log(level, LogLevel::Normal, "Git timestamp proof: GitCommit");
32        if let Some(commit) = proof.git_commit() {
33            log(level, LogLevel::Verbose, &format!("  Commit: {commit}"));
34        }
35    } else {
36        log(level, LogLevel::Normal, "Timestamp proof is not a git commit");
37    }
38}
39
40/// Verify signed pre-registration content
41pub fn verify_signed_content(
42    signed: &SignedPreRegistration,
43    verify_git: bool,
44    level: LogLevel,
45) -> Result<(), String> {
46    match verify_signature(signed) {
47        SignatureStatus::Valid => {
48            log(level, LogLevel::Normal, "Signature verification: VALID");
49        }
50        SignatureStatus::Invalid => {
51            log(level, LogLevel::Normal, "Signature verification: INVALID");
52            return Err("Signature verification failed".to_string());
53        }
54        SignatureStatus::Error(e) => {
55            return Err(format!("Verification error: {e}"));
56        }
57    }
58
59    if verify_git {
60        if let Some(proof) = &signed.timestamp_proof {
61            log_git_proof(level, proof);
62        } else {
63            log(level, LogLevel::Normal, "No git timestamp proof found");
64        }
65    }
66
67    log(level, LogLevel::Normal, "Pre-registration verified successfully");
68    Ok(())
69}
70
71/// Compute and log commitment from pre-registration
72pub fn compute_commitment(prereg: &PreRegistration, level: LogLevel) {
73    let commitment = prereg.commit();
74    log(level, LogLevel::Normal, &format!("Computed commitment: {}...", &commitment.hash[..32]));
75}
76
77pub fn run_research_verify(args: VerifyArgs, level: LogLevel) -> Result<(), String> {
78    log(level, LogLevel::Normal, &format!("Verifying: {}", args.file.display()));
79
80    let content =
81        std::fs::read_to_string(&args.file).map_err(|e| format!("Failed to read file: {e}"))?;
82
83    if let Ok(signed) = serde_yaml::from_str::<SignedPreRegistration>(&content) {
84        return verify_signed_content(&signed, args.verify_git, level);
85    }
86
87    log(level, LogLevel::Normal, "File does not contain a signed pre-registration");
88
89    if let Some(original_path) = &args.original {
90        let original_content = std::fs::read_to_string(original_path)
91            .map_err(|e| format!("Failed to read original: {e}"))?;
92
93        let prereg: PreRegistration = serde_yaml::from_str(&original_content)
94            .map_err(|e| format!("Failed to parse original: {e}"))?;
95
96        compute_commitment(&prereg, level);
97    }
98
99    Ok(())
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn test_signature_status_eq() {
108        assert_eq!(SignatureStatus::Valid, SignatureStatus::Valid);
109        assert_eq!(SignatureStatus::Invalid, SignatureStatus::Invalid);
110        assert_ne!(SignatureStatus::Valid, SignatureStatus::Invalid);
111    }
112
113    #[test]
114    fn test_signature_status_error() {
115        let err = SignatureStatus::Error("test error".to_string());
116        assert_eq!(err, SignatureStatus::Error("test error".to_string()));
117        assert_ne!(err, SignatureStatus::Valid);
118    }
119
120    #[test]
121    fn test_signature_status_debug() {
122        let valid = SignatureStatus::Valid;
123        assert!(format!("{valid:?}").contains("Valid"));
124    }
125
126    #[test]
127    fn test_signature_status_clone() {
128        let orig = SignatureStatus::Error("test".to_string());
129        let cloned = orig.clone();
130        assert_eq!(orig, cloned);
131    }
132
133    #[test]
134    fn test_log_git_proof_with_git_commit() {
135        let proof = TimestampProof::GitCommit("abc123def456".to_string());
136        // Should not panic
137        log_git_proof(LogLevel::Quiet, &proof);
138        log_git_proof(LogLevel::Verbose, &proof);
139    }
140
141    #[test]
142    fn test_log_git_proof_with_non_git() {
143        let proof = TimestampProof::Rfc3161(vec![1, 2, 3]);
144        // Should not panic
145        log_git_proof(LogLevel::Quiet, &proof);
146    }
147
148    #[test]
149    fn test_log_git_proof_opentimestamps() {
150        let proof = TimestampProof::OpenTimestamps(vec![4, 5, 6]);
151        // Should not panic, logs "not a git commit"
152        log_git_proof(LogLevel::Normal, &proof);
153    }
154
155    #[test]
156    fn test_compute_commitment() {
157        let prereg =
158            PreRegistration::new("Test Title", "Test Hypothesis", "Test Methods", "Test Analysis");
159        // Should not panic
160        compute_commitment(&prereg, LogLevel::Quiet);
161    }
162
163    #[test]
164    fn test_verify_signature_with_valid_signed() {
165        use ed25519_dalek::SigningKey;
166        let prereg =
167            PreRegistration::new("Test Title", "Test Hypothesis", "Test Methods", "Test Analysis");
168        let signing_key = SigningKey::from_bytes(&[1u8; 32]);
169        let signed = SignedPreRegistration::sign(&prereg, &signing_key);
170        assert_eq!(verify_signature(&signed), SignatureStatus::Valid);
171    }
172
173    #[test]
174    fn test_verify_signature_with_invalid_signature() {
175        let prereg =
176            PreRegistration::new("Test Title", "Test Hypothesis", "Test Methods", "Test Analysis");
177        let commitment = prereg.commit();
178        // Create signed with invalid signature
179        let signed = SignedPreRegistration {
180            registration: prereg,
181            commitment,
182            signature: "0".repeat(128), // Invalid but properly formatted signature
183            public_key: "0".repeat(64), // Invalid but properly formatted public key
184            timestamp_proof: None,
185        };
186        // This should be Invalid or Error
187        let status = verify_signature(&signed);
188        assert!(matches!(status, SignatureStatus::Invalid | SignatureStatus::Error(_)));
189    }
190
191    #[test]
192    fn test_verify_signature_with_malformed_key() {
193        let prereg =
194            PreRegistration::new("Test Title", "Test Hypothesis", "Test Methods", "Test Analysis");
195        let commitment = prereg.commit();
196        // Create signed with malformed key (not valid hex)
197        let signed = SignedPreRegistration {
198            registration: prereg,
199            commitment,
200            signature: "not-hex".to_string(),
201            public_key: "also-not-hex".to_string(),
202            timestamp_proof: None,
203        };
204        let status = verify_signature(&signed);
205        assert!(matches!(status, SignatureStatus::Error(_)));
206    }
207
208    #[test]
209    fn test_verify_signed_content_valid() {
210        use ed25519_dalek::SigningKey;
211        let prereg =
212            PreRegistration::new("Test Title", "Test Hypothesis", "Test Methods", "Test Analysis");
213        let signing_key = SigningKey::from_bytes(&[2u8; 32]);
214        let signed = SignedPreRegistration::sign(&prereg, &signing_key);
215        let result = verify_signed_content(&signed, false, LogLevel::Quiet);
216        assert!(result.is_ok());
217    }
218
219    #[test]
220    fn test_verify_signed_content_invalid() {
221        let prereg =
222            PreRegistration::new("Test Title", "Test Hypothesis", "Test Methods", "Test Analysis");
223        let commitment = prereg.commit();
224        let signed = SignedPreRegistration {
225            registration: prereg,
226            commitment,
227            signature: "0".repeat(128),
228            public_key: "0".repeat(64),
229            timestamp_proof: None,
230        };
231        let result = verify_signed_content(&signed, false, LogLevel::Quiet);
232        assert!(result.is_err());
233    }
234
235    #[test]
236    fn test_verify_signed_content_with_git_proof() {
237        use ed25519_dalek::SigningKey;
238        let prereg =
239            PreRegistration::new("Test Title", "Test Hypothesis", "Test Methods", "Test Analysis");
240        let signing_key = SigningKey::from_bytes(&[3u8; 32]);
241        let mut signed = SignedPreRegistration::sign(&prereg, &signing_key);
242        signed.timestamp_proof = Some(TimestampProof::GitCommit("abc123".to_string()));
243        let result = verify_signed_content(&signed, true, LogLevel::Quiet);
244        assert!(result.is_ok());
245    }
246
247    #[test]
248    fn test_verify_signed_content_no_git_proof() {
249        use ed25519_dalek::SigningKey;
250        let prereg =
251            PreRegistration::new("Test Title", "Test Hypothesis", "Test Methods", "Test Analysis");
252        let signing_key = SigningKey::from_bytes(&[4u8; 32]);
253        let signed = SignedPreRegistration::sign(&prereg, &signing_key);
254        // signed has no timestamp_proof by default
255        let result = verify_signed_content(&signed, true, LogLevel::Quiet);
256        assert!(result.is_ok());
257    }
258
259    #[test]
260    fn test_verify_signed_content_error_propagation() {
261        let prereg =
262            PreRegistration::new("Test Title", "Test Hypothesis", "Test Methods", "Test Analysis");
263        let commitment = prereg.commit();
264        let signed = SignedPreRegistration {
265            registration: prereg,
266            commitment,
267            signature: "invalid".to_string(),
268            public_key: "invalid".to_string(),
269            timestamp_proof: None,
270        };
271        let result = verify_signed_content(&signed, false, LogLevel::Quiet);
272        assert!(result.is_err());
273        assert!(result.unwrap_err().contains("error"));
274    }
275}