commit_sign.rs
1 use std::path::PathBuf; 2 use std::sync::Arc; 3 4 use auths_core::signing::PrefilledPassphraseProvider; 5 use auths_core::storage::keychain::get_platform_keychain_with_config; 6 use auths_sdk::workflows::signing::{ 7 CommitSigningContext, CommitSigningParams, CommitSigningWorkflow, 8 }; 9 use pyo3::exceptions::PyRuntimeError; 10 use pyo3::prelude::*; 11 12 use crate::identity::{make_keychain_config, resolve_passphrase}; 13 14 #[pyclass] 15 #[derive(Clone)] 16 pub struct PyCommitSignResult { 17 #[pyo3(get)] 18 pub signature_pem: String, 19 #[pyo3(get)] 20 pub method: String, 21 #[pyo3(get)] 22 pub namespace: String, 23 } 24 25 #[pymethods] 26 impl PyCommitSignResult { 27 fn __repr__(&self) -> String { 28 let pem_preview = if self.signature_pem.len() > 40 { 29 format!("{}...", &self.signature_pem[..40]) 30 } else { 31 self.signature_pem.clone() 32 }; 33 format!( 34 "CommitSignResult(method='{}', pem='{}')", 35 self.method, pem_preview, 36 ) 37 } 38 } 39 40 /// Sign git commit/tag data, producing an SSHSIG PEM signature. 41 /// 42 /// Uses a 3-tier fallback: ssh-agent -> auto-start -> direct signing. 43 /// In headless environments (Python SDK), falls through to direct signing. 44 /// 45 /// Args: 46 /// * `data`: The raw commit or tag bytes to sign. 47 /// * `identity_key_alias`: Keychain alias for the identity key. 48 /// * `repo_path`: Path to the auths repository. 49 /// * `passphrase`: Optional passphrase for the keychain. 50 /// 51 /// Usage: 52 /// ```ignore 53 /// let result = sign_commit(py, b"commit data", "main", "~/.auths", None)?; 54 /// ``` 55 #[pyfunction] 56 #[pyo3(signature = (data, identity_key_alias, repo_path, passphrase=None))] 57 pub fn sign_commit( 58 py: Python<'_>, 59 data: &[u8], 60 identity_key_alias: &str, 61 repo_path: &str, 62 passphrase: Option<String>, 63 ) -> PyResult<PyCommitSignResult> { 64 let passphrase_str = resolve_passphrase(passphrase); 65 let env_config = make_keychain_config(&passphrase_str); 66 let provider = Arc::new(PrefilledPassphraseProvider::new(&passphrase_str)); 67 68 let keychain = get_platform_keychain_with_config(&env_config) 69 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_KEYCHAIN_ERROR] Keychain error: {e}")))?; 70 let keychain = Arc::from(keychain); 71 72 let repo = PathBuf::from(shellexpand::tilde(repo_path).as_ref()); 73 74 let params = 75 CommitSigningParams::new(identity_key_alias, "git", data.to_vec()).with_repo_path(repo); 76 77 let signing_ctx = CommitSigningContext { 78 key_storage: keychain, 79 passphrase_provider: provider, 80 agent_signing: Arc::new(auths_sdk::ports::agent::NoopAgentProvider), 81 }; 82 83 #[allow(clippy::disallowed_methods)] // Presentation boundary 84 let now = chrono::Utc::now(); 85 86 py.allow_threads(move || { 87 let pem = CommitSigningWorkflow::execute(&signing_ctx, params, now) 88 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_SIGNING_FAILED] Commit signing failed: {e}")))?; 89 90 Ok(PyCommitSignResult { 91 signature_pem: pem, 92 method: "direct".to_string(), 93 namespace: "git".to_string(), 94 }) 95 }) 96 }