commit_verify.rs
1 //! Python FFI bridge for native commit signature verification. 2 3 use auths_verifier::commit::verify_commit_signature; 4 use auths_verifier::commit_error::CommitVerificationError; 5 use auths_verifier::core::Ed25519PublicKey; 6 use pyo3::exceptions::PyValueError; 7 use pyo3::prelude::*; 8 9 use crate::runtime::runtime; 10 11 /// Result of native commit signature verification. 12 #[pyclass] 13 #[derive(Clone)] 14 pub struct PyCommitVerificationResult { 15 #[pyo3(get)] 16 /// Whether the signature is valid. 17 pub valid: bool, 18 #[pyo3(get)] 19 /// Hex-encoded signer public key (present on success). 20 pub signer_hex: Option<String>, 21 #[pyo3(get)] 22 /// Error message (present on failure). 23 pub error: Option<String>, 24 #[pyo3(get)] 25 /// Error code matching Python's ErrorCode values. 26 pub error_code: Option<String>, 27 } 28 29 #[pymethods] 30 impl PyCommitVerificationResult { 31 fn __repr__(&self) -> String { 32 if self.valid { 33 format!( 34 "CommitVerificationResult(valid=True, signer='{}')", 35 self.signer_hex.as_deref().unwrap_or("?") 36 ) 37 } else { 38 format!( 39 "CommitVerificationResult(valid=False, error='{}', code='{}')", 40 self.error.as_deref().unwrap_or("?"), 41 self.error_code.as_deref().unwrap_or("?") 42 ) 43 } 44 } 45 46 fn __bool__(&self) -> bool { 47 self.valid 48 } 49 } 50 51 fn error_to_code(err: &CommitVerificationError) -> &'static str { 52 match err { 53 CommitVerificationError::UnsignedCommit => "UNSIGNED", 54 CommitVerificationError::GpgNotSupported => "GPG_NOT_SUPPORTED", 55 CommitVerificationError::UnknownSigner => "UNKNOWN_SIGNER", 56 CommitVerificationError::SignatureInvalid => "INVALID_SIGNATURE", 57 CommitVerificationError::SshSigParseFailed(_) => "INVALID_SIGNATURE", 58 CommitVerificationError::UnsupportedKeyType { .. } => "INVALID_SIGNATURE", 59 CommitVerificationError::NamespaceMismatch { .. } => "INVALID_SIGNATURE", 60 CommitVerificationError::HashAlgorithmUnsupported(_) => "INVALID_SIGNATURE", 61 CommitVerificationError::CommitParseFailed(_) => "INVALID_SIGNATURE", 62 } 63 } 64 65 /// Verify an SSH-signed git commit against allowed Ed25519 public keys. 66 /// 67 /// Args: 68 /// * `commit_content`: Raw bytes from `git cat-file commit <sha>`. 69 /// * `allowed_keys_hex`: List of hex-encoded 32-byte Ed25519 public keys. 70 /// 71 /// Usage: 72 /// ```python 73 /// from auths._native import verify_commit_native 74 /// result = verify_commit_native(commit_bytes, ["aabbcc..."]) 75 /// ``` 76 #[pyfunction] 77 pub fn verify_commit_native( 78 py: Python<'_>, 79 commit_content: &[u8], 80 allowed_keys_hex: Vec<String>, 81 ) -> PyResult<PyCommitVerificationResult> { 82 let keys: Vec<Ed25519PublicKey> = allowed_keys_hex 83 .iter() 84 .enumerate() 85 .map(|(i, h)| { 86 let bytes = hex::decode(h) 87 .map_err(|e| PyValueError::new_err(format!("invalid hex key at index {i}: {e}")))?; 88 Ed25519PublicKey::try_from_slice(&bytes).map_err(|e| { 89 PyValueError::new_err(format!("invalid Ed25519 key at index {i}: {e}")) 90 }) 91 }) 92 .collect::<PyResult<Vec<_>>>()?; 93 94 let content = commit_content.to_vec(); 95 py.allow_threads(|| { 96 let provider = auths_crypto::RingCryptoProvider; 97 let result = runtime().block_on(verify_commit_signature(&content, &keys, &provider)); 98 99 match result { 100 Ok(verified) => Ok(PyCommitVerificationResult { 101 valid: true, 102 signer_hex: Some(hex::encode(verified.signer_key.as_bytes())), 103 error: None, 104 error_code: None, 105 }), 106 Err(err) => Ok(PyCommitVerificationResult { 107 valid: false, 108 signer_hex: None, 109 error_code: Some(error_to_code(&err).to_string()), 110 error: Some(err.to_string()), 111 }), 112 } 113 }) 114 }