/ packages / auths-python / src / commit_verify.rs
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  }