auths_sdk/workflows/
signing.rs1use std::path::PathBuf;
8use std::sync::Arc;
9
10use chrono::{DateTime, Utc};
11
12use auths_core::AgentError;
13use auths_core::crypto::signer::decrypt_keypair;
14use auths_core::crypto::ssh;
15use auths_core::crypto::ssh::SecureSeed;
16use auths_core::signing::PassphraseProvider;
17use auths_core::storage::keychain::{KeyAlias, KeyStorage};
18use auths_crypto::Pkcs8Der;
19
20use crate::ports::agent::{AgentSigningError, AgentSigningPort};
21use crate::signing::{self, SigningError};
22
23const DEFAULT_MAX_PASSPHRASE_ATTEMPTS: usize = 3;
24
25pub struct CommitSigningContext {
40 pub key_storage: Arc<dyn KeyStorage + Send + Sync>,
42 pub passphrase_provider: Arc<dyn PassphraseProvider + Send + Sync>,
44 pub agent_signing: Arc<dyn AgentSigningPort + Send + Sync>,
46}
47
48impl From<&crate::context::AuthsContext> for CommitSigningContext {
49 fn from(ctx: &crate::context::AuthsContext) -> Self {
50 Self {
51 key_storage: ctx.key_storage.clone(),
52 passphrase_provider: ctx.passphrase_provider.clone(),
53 agent_signing: ctx.agent_signing.clone(),
54 }
55 }
56}
57
58pub struct CommitSigningParams {
75 pub key_alias: String,
77 pub namespace: String,
79 pub data: Vec<u8>,
81 pub pubkey: Option<auths_verifier::DevicePublicKey>,
83 pub repo_path: Option<PathBuf>,
85 pub max_passphrase_attempts: usize,
87}
88
89impl CommitSigningParams {
90 pub fn new(key_alias: impl Into<String>, namespace: impl Into<String>, data: Vec<u8>) -> Self {
97 Self {
98 key_alias: key_alias.into(),
99 namespace: namespace.into(),
100 data,
101 pubkey: None,
102 repo_path: None,
103 max_passphrase_attempts: DEFAULT_MAX_PASSPHRASE_ATTEMPTS,
104 }
105 }
106
107 pub fn with_pubkey(mut self, pubkey: auths_verifier::DevicePublicKey) -> Self {
109 self.pubkey = Some(pubkey);
110 self
111 }
112
113 pub fn with_repo_path(mut self, path: PathBuf) -> Self {
115 self.repo_path = Some(path);
116 self
117 }
118
119 pub fn with_max_passphrase_attempts(mut self, max: usize) -> Self {
121 self.max_passphrase_attempts = max;
122 self
123 }
124}
125
126pub struct CommitSigningWorkflow;
143
144impl CommitSigningWorkflow {
145 pub fn execute(
152 ctx: &CommitSigningContext,
153 params: CommitSigningParams,
154 now: DateTime<Utc>,
155 ) -> Result<String, SigningError> {
156 match try_agent_sign(ctx, ¶ms) {
158 Ok(pem) => return Ok(pem),
159 Err(SigningError::AgentUnavailable(_)) => {}
160 Err(e) => return Err(e),
161 }
162
163 let _ = ctx.agent_signing.ensure_running();
165
166 if ctx.key_storage.is_hardware_backend() {
169 if let Some(ref repo_path) = params.repo_path {
170 signing::validate_freeze_state(repo_path, now)?;
171 }
172
173 let alias = KeyAlias::new_unchecked(¶ms.key_alias);
174
175 let sshsig_data = ssh::construct_sshsig_signed_data(¶ms.data, ¶ms.namespace)
177 .map_err(|e| SigningError::PemEncoding(e.to_string()))?;
178
179 let (sig, pubkey, curve) = auths_core::storage::keychain::sign_with_key(
181 ctx.key_storage.as_ref(),
182 &alias,
183 ctx.passphrase_provider.as_ref(),
184 &sshsig_data,
185 )
186 .map_err(|e| SigningError::KeyDecryptionFailed(e.to_string()))?;
187
188 return ssh::construct_sshsig_pem(&pubkey, &sig, ¶ms.namespace, curve)
190 .map_err(|e| SigningError::PemEncoding(e.to_string()));
191 }
192
193 let pkcs8 = load_key_with_passphrase_retry(ctx, ¶ms)?;
194 let (seed, _pubkey, curve) =
195 auths_core::crypto::signer::load_seed_and_pubkey(pkcs8.as_ref())
196 .map_err(|e| SigningError::KeyDecryptionFailed(e.to_string()))?;
197
198 let _ = ctx
200 .agent_signing
201 .add_identity(¶ms.namespace, pkcs8.as_ref());
202
203 direct_sign(¶ms, &seed, now, curve)
205 }
206}
207
208fn try_agent_sign(
209 ctx: &CommitSigningContext,
210 params: &CommitSigningParams,
211) -> Result<String, SigningError> {
212 let pubkey = params.pubkey.as_ref().ok_or_else(|| {
213 SigningError::AgentUnavailable("no cached public key for agent signing".into())
214 })?;
215 ctx.agent_signing
216 .try_sign(¶ms.namespace, pubkey, ¶ms.data)
217 .map_err(|e| match e {
218 AgentSigningError::Unavailable(msg) | AgentSigningError::ConnectionFailed(msg) => {
219 SigningError::AgentUnavailable(msg)
220 }
221 other => SigningError::AgentSigningFailed(other),
222 })
223}
224
225fn load_key_with_passphrase_retry(
226 ctx: &CommitSigningContext,
227 params: &CommitSigningParams,
228) -> Result<Pkcs8Der, SigningError> {
229 let alias = KeyAlias::new_unchecked(¶ms.key_alias);
230 let (_identity_did, _role, encrypted_data) = ctx
231 .key_storage
232 .load_key(&alias)
233 .map_err(|e| SigningError::KeychainUnavailable(e.to_string()))?;
234
235 let prompt = format!("Enter passphrase for '{}':", params.key_alias);
236
237 for attempt in 1..=params.max_passphrase_attempts {
238 let passphrase = ctx
239 .passphrase_provider
240 .get_passphrase(&prompt)
241 .map_err(|e| SigningError::KeyDecryptionFailed(e.to_string()))?;
242
243 match decrypt_keypair(&encrypted_data, &passphrase) {
244 Ok(decrypted) => return Ok(Pkcs8Der::new(&decrypted[..])),
245 Err(AgentError::IncorrectPassphrase) => {
246 if attempt < params.max_passphrase_attempts {
247 ctx.passphrase_provider.on_incorrect_passphrase(&prompt);
248 }
249 }
250 Err(e) => return Err(SigningError::KeyDecryptionFailed(e.to_string())),
251 }
252 }
253
254 Err(SigningError::PassphraseExhausted {
255 attempts: params.max_passphrase_attempts,
256 })
257}
258
259fn direct_sign(
260 params: &CommitSigningParams,
261 seed: &SecureSeed,
262 now: DateTime<Utc>,
263 curve: auths_crypto::CurveType,
264) -> Result<String, SigningError> {
265 if let Some(ref repo_path) = params.repo_path {
266 signing::validate_freeze_state(repo_path, now)?;
267 }
268
269 signing::sign_with_seed(seed, ¶ms.data, ¶ms.namespace, curve)
270}