use std::path::PathBuf;
use std::sync::Arc;
use chrono::{DateTime, Utc};
use auths_core::AgentError;
use auths_core::crypto::signer::decrypt_keypair;
use auths_core::crypto::ssh;
use auths_core::crypto::ssh::SecureSeed;
use auths_core::signing::PassphraseProvider;
use auths_core::storage::keychain::{KeyAlias, KeyStorage};
use auths_crypto::Pkcs8Der;
use crate::ports::agent::{AgentSigningError, AgentSigningPort};
use crate::signing::{self, SigningError};
const DEFAULT_MAX_PASSPHRASE_ATTEMPTS: usize = 3;
pub struct CommitSigningContext {
pub key_storage: Arc<dyn KeyStorage + Send + Sync>,
pub passphrase_provider: Arc<dyn PassphraseProvider + Send + Sync>,
pub agent_signing: Arc<dyn AgentSigningPort + Send + Sync>,
}
impl From<&crate::context::AuthsContext> for CommitSigningContext {
fn from(ctx: &crate::context::AuthsContext) -> Self {
Self {
key_storage: ctx.key_storage.clone(),
passphrase_provider: ctx.passphrase_provider.clone(),
agent_signing: ctx.agent_signing.clone(),
}
}
}
pub struct CommitSigningParams {
pub key_alias: String,
pub namespace: String,
pub data: Vec<u8>,
pub pubkey: Option<auths_verifier::DevicePublicKey>,
pub repo_path: Option<PathBuf>,
pub max_passphrase_attempts: usize,
}
impl CommitSigningParams {
pub fn new(key_alias: impl Into<String>, namespace: impl Into<String>, data: Vec<u8>) -> Self {
Self {
key_alias: key_alias.into(),
namespace: namespace.into(),
data,
pubkey: None,
repo_path: None,
max_passphrase_attempts: DEFAULT_MAX_PASSPHRASE_ATTEMPTS,
}
}
pub fn with_pubkey(mut self, pubkey: auths_verifier::DevicePublicKey) -> Self {
self.pubkey = Some(pubkey);
self
}
pub fn with_repo_path(mut self, path: PathBuf) -> Self {
self.repo_path = Some(path);
self
}
pub fn with_max_passphrase_attempts(mut self, max: usize) -> Self {
self.max_passphrase_attempts = max;
self
}
}
pub struct CommitSigningWorkflow;
impl CommitSigningWorkflow {
pub fn execute(
ctx: &CommitSigningContext,
params: CommitSigningParams,
now: DateTime<Utc>,
) -> Result<String, SigningError> {
match try_agent_sign(ctx, ¶ms) {
Ok(pem) => return Ok(pem),
Err(SigningError::AgentUnavailable(_)) => {}
Err(e) => return Err(e),
}
let _ = ctx.agent_signing.ensure_running();
if ctx.key_storage.is_hardware_backend() {
if let Some(ref repo_path) = params.repo_path {
signing::validate_freeze_state(repo_path, now)?;
}
let alias = KeyAlias::new_unchecked(¶ms.key_alias);
let sshsig_data = ssh::construct_sshsig_signed_data(¶ms.data, ¶ms.namespace)
.map_err(|e| SigningError::PemEncoding(e.to_string()))?;
let (sig, pubkey, curve) = auths_core::storage::keychain::sign_with_key(
ctx.key_storage.as_ref(),
&alias,
ctx.passphrase_provider.as_ref(),
&sshsig_data,
)
.map_err(|e| SigningError::KeyDecryptionFailed(e.to_string()))?;
return ssh::construct_sshsig_pem(&pubkey, &sig, ¶ms.namespace, curve)
.map_err(|e| SigningError::PemEncoding(e.to_string()));
}
let pkcs8 = load_key_with_passphrase_retry(ctx, ¶ms)?;
let (seed, _pubkey, curve) =
auths_core::crypto::signer::load_seed_and_pubkey(pkcs8.as_ref())
.map_err(|e| SigningError::KeyDecryptionFailed(e.to_string()))?;
let _ = ctx
.agent_signing
.add_identity(¶ms.namespace, pkcs8.as_ref());
direct_sign(¶ms, &seed, now, curve)
}
}
fn try_agent_sign(
ctx: &CommitSigningContext,
params: &CommitSigningParams,
) -> Result<String, SigningError> {
let pubkey = params.pubkey.as_ref().ok_or_else(|| {
SigningError::AgentUnavailable("no cached public key for agent signing".into())
})?;
ctx.agent_signing
.try_sign(¶ms.namespace, pubkey, ¶ms.data)
.map_err(|e| match e {
AgentSigningError::Unavailable(msg) | AgentSigningError::ConnectionFailed(msg) => {
SigningError::AgentUnavailable(msg)
}
other => SigningError::AgentSigningFailed(other),
})
}
fn load_key_with_passphrase_retry(
ctx: &CommitSigningContext,
params: &CommitSigningParams,
) -> Result<Pkcs8Der, SigningError> {
let alias = KeyAlias::new_unchecked(¶ms.key_alias);
let (_identity_did, _role, encrypted_data) = ctx
.key_storage
.load_key(&alias)
.map_err(|e| SigningError::KeychainUnavailable(e.to_string()))?;
let prompt = format!("Enter passphrase for '{}':", params.key_alias);
for attempt in 1..=params.max_passphrase_attempts {
let passphrase = ctx
.passphrase_provider
.get_passphrase(&prompt)
.map_err(|e| SigningError::KeyDecryptionFailed(e.to_string()))?;
match decrypt_keypair(&encrypted_data, &passphrase) {
Ok(decrypted) => return Ok(Pkcs8Der::new(&decrypted[..])),
Err(AgentError::IncorrectPassphrase) => {
if attempt < params.max_passphrase_attempts {
ctx.passphrase_provider.on_incorrect_passphrase(&prompt);
}
}
Err(e) => return Err(SigningError::KeyDecryptionFailed(e.to_string())),
}
}
Err(SigningError::PassphraseExhausted {
attempts: params.max_passphrase_attempts,
})
}
fn direct_sign(
params: &CommitSigningParams,
seed: &SecureSeed,
now: DateTime<Utc>,
curve: auths_crypto::CurveType,
) -> Result<String, SigningError> {
if let Some(ref repo_path) = params.repo_path {
signing::validate_freeze_state(repo_path, now)?;
}
signing::sign_with_seed(seed, ¶ms.data, ¶ms.namespace, curve)
}