auths_sdk/workflows/
signing.rs1use std::path::PathBuf;
8use std::sync::Arc;
9
10use chrono::{DateTime, Utc};
11use zeroize::Zeroizing;
12
13use auths_core::AgentError;
14use auths_core::crypto::signer::decrypt_keypair;
15use auths_core::crypto::ssh::{SecureSeed, extract_seed_from_pkcs8};
16use auths_core::signing::PassphraseProvider;
17use auths_core::storage::keychain::{KeyAlias, KeyStorage};
18
19use crate::ports::agent::{AgentSigningError, AgentSigningPort};
20use crate::signing::{self, SigningError};
21
22const DEFAULT_MAX_PASSPHRASE_ATTEMPTS: usize = 3;
23
24pub struct CommitSigningContext {
39 pub key_storage: Arc<dyn KeyStorage + Send + Sync>,
41 pub passphrase_provider: Arc<dyn PassphraseProvider + Send + Sync>,
43 pub agent_signing: Arc<dyn AgentSigningPort + Send + Sync>,
45}
46
47impl From<&crate::context::AuthsContext> for CommitSigningContext {
48 fn from(ctx: &crate::context::AuthsContext) -> Self {
49 Self {
50 key_storage: ctx.key_storage.clone(),
51 passphrase_provider: ctx.passphrase_provider.clone(),
52 agent_signing: ctx.agent_signing.clone(),
53 }
54 }
55}
56
57pub struct CommitSigningParams {
74 pub key_alias: String,
76 pub namespace: String,
78 pub data: Vec<u8>,
80 pub pubkey: Vec<u8>,
82 pub repo_path: Option<PathBuf>,
84 pub max_passphrase_attempts: usize,
86}
87
88impl CommitSigningParams {
89 pub fn new(key_alias: impl Into<String>, namespace: impl Into<String>, data: Vec<u8>) -> Self {
96 Self {
97 key_alias: key_alias.into(),
98 namespace: namespace.into(),
99 data,
100 pubkey: Vec::new(),
101 repo_path: None,
102 max_passphrase_attempts: DEFAULT_MAX_PASSPHRASE_ATTEMPTS,
103 }
104 }
105
106 pub fn with_pubkey(mut self, pubkey: Vec<u8>) -> Self {
108 self.pubkey = pubkey;
109 self
110 }
111
112 pub fn with_repo_path(mut self, path: PathBuf) -> Self {
114 self.repo_path = Some(path);
115 self
116 }
117
118 pub fn with_max_passphrase_attempts(mut self, max: usize) -> Self {
120 self.max_passphrase_attempts = max;
121 self
122 }
123}
124
125pub struct CommitSigningWorkflow;
142
143impl CommitSigningWorkflow {
144 pub fn execute(
151 ctx: &CommitSigningContext,
152 params: CommitSigningParams,
153 now: DateTime<Utc>,
154 ) -> Result<String, SigningError> {
155 match try_agent_sign(ctx, ¶ms) {
157 Ok(pem) => return Ok(pem),
158 Err(SigningError::AgentUnavailable(_)) => {}
159 Err(e) => return Err(e),
160 }
161
162 let _ = ctx.agent_signing.ensure_running();
164
165 let pkcs8_der = load_key_with_passphrase_retry(ctx, ¶ms)?;
166 let seed = extract_seed_from_pkcs8(&pkcs8_der)
167 .map_err(|e| SigningError::KeyDecryptionFailed(e.to_string()))?;
168
169 let _ = ctx
171 .agent_signing
172 .add_identity(¶ms.namespace, &pkcs8_der);
173
174 direct_sign(¶ms, &seed, now)
176 }
177}
178
179fn try_agent_sign(
180 ctx: &CommitSigningContext,
181 params: &CommitSigningParams,
182) -> Result<String, SigningError> {
183 ctx.agent_signing
184 .try_sign(¶ms.namespace, ¶ms.pubkey, ¶ms.data)
185 .map_err(|e| match e {
186 AgentSigningError::Unavailable(msg) | AgentSigningError::ConnectionFailed(msg) => {
187 SigningError::AgentUnavailable(msg)
188 }
189 other => SigningError::AgentSigningFailed(other),
190 })
191}
192
193fn load_key_with_passphrase_retry(
194 ctx: &CommitSigningContext,
195 params: &CommitSigningParams,
196) -> Result<Zeroizing<Vec<u8>>, SigningError> {
197 let alias = KeyAlias::new_unchecked(¶ms.key_alias);
198 let (_identity_did, encrypted_data) = ctx
199 .key_storage
200 .load_key(&alias)
201 .map_err(|e| SigningError::KeychainUnavailable(e.to_string()))?;
202
203 let prompt = format!("Enter passphrase for '{}':", params.key_alias);
204
205 for attempt in 1..=params.max_passphrase_attempts {
206 let passphrase = ctx
207 .passphrase_provider
208 .get_passphrase(&prompt)
209 .map_err(|e| SigningError::KeyDecryptionFailed(e.to_string()))?;
210
211 match decrypt_keypair(&encrypted_data, &passphrase) {
212 Ok(decrypted) => return Ok(decrypted),
213 Err(AgentError::IncorrectPassphrase) => {
214 if attempt < params.max_passphrase_attempts {
215 ctx.passphrase_provider.on_incorrect_passphrase(&prompt);
216 }
217 }
218 Err(e) => return Err(SigningError::KeyDecryptionFailed(e.to_string())),
219 }
220 }
221
222 Err(SigningError::PassphraseExhausted {
223 attempts: params.max_passphrase_attempts,
224 })
225}
226
227fn direct_sign(
228 params: &CommitSigningParams,
229 seed: &SecureSeed,
230 now: DateTime<Utc>,
231) -> Result<String, SigningError> {
232 if let Some(ref repo_path) = params.repo_path {
233 signing::validate_freeze_state(repo_path, now)?;
234 }
235
236 signing::sign_with_seed(seed, ¶ms.data, ¶ms.namespace)
237}