1use crate::context::AuthsContext;
7use crate::ports::artifact::ArtifactSource;
8use auths_core::crypto::ssh::{self, SecureSeed};
9use auths_core::crypto::{provider_bridge, signer as core_signer};
10use auths_core::signing::{PassphraseProvider, SecureSigner};
11use auths_core::storage::keychain::{IdentityDID, KeyAlias, KeyStorage};
12use auths_id::attestation::core::resign_attestation;
13use auths_id::attestation::create::create_signed_attestation;
14use auths_id::storage::git_refs::AttestationMetadata;
15use auths_verifier::core::Capability;
16use auths_verifier::types::DeviceDID;
17use std::collections::HashMap;
18use std::path::Path;
19use std::sync::Arc;
20
21#[derive(Debug, thiserror::Error)]
23#[non_exhaustive]
24pub enum SigningError {
25 #[error("identity is frozen: {0}")]
27 IdentityFrozen(String),
28 #[error("key resolution failed: {0}")]
30 KeyResolution(String),
31 #[error("signing operation failed: {0}")]
33 SigningFailed(String),
34 #[error("invalid passphrase")]
36 InvalidPassphrase,
37 #[error("PEM encoding failed: {0}")]
39 PemEncoding(String),
40 #[error("agent unavailable: {0}")]
42 AgentUnavailable(String),
43 #[error("agent signing failed")]
45 AgentSigningFailed(#[source] crate::ports::agent::AgentSigningError),
46 #[error("passphrase exhausted after {attempts} attempt(s)")]
48 PassphraseExhausted {
49 attempts: usize,
51 },
52 #[error("keychain unavailable: {0}")]
54 KeychainUnavailable(String),
55 #[error("key decryption failed: {0}")]
57 KeyDecryptionFailed(String),
58}
59
60pub struct SigningConfig {
72 pub namespace: String,
74}
75
76pub fn validate_freeze_state(
87 repo_path: &Path,
88 now: chrono::DateTime<chrono::Utc>,
89) -> Result<(), SigningError> {
90 use auths_id::freeze::load_active_freeze;
91
92 if let Some(state) = load_active_freeze(repo_path, now)
93 .map_err(|e| SigningError::IdentityFrozen(e.to_string()))?
94 {
95 return Err(SigningError::IdentityFrozen(format!(
96 "frozen until {}. Remaining: {}. To unfreeze: auths emergency unfreeze",
97 state.frozen_until.format("%Y-%m-%d %H:%M UTC"),
98 state.expires_description(now),
99 )));
100 }
101
102 Ok(())
103}
104
105pub fn construct_signature_payload(data: &[u8], namespace: &str) -> Result<Vec<u8>, SigningError> {
116 ssh::construct_sshsig_signed_data(data, namespace)
117 .map_err(|e| SigningError::SigningFailed(e.to_string()))
118}
119
120pub fn sign_with_seed(
132 seed: &SecureSeed,
133 data: &[u8],
134 namespace: &str,
135) -> Result<String, SigningError> {
136 ssh::create_sshsig(seed, data, namespace).map_err(|e| SigningError::PemEncoding(e.to_string()))
137}
138
139pub enum SigningKeyMaterial {
149 Alias(KeyAlias),
151 Direct(SecureSeed),
153}
154
155pub struct ArtifactSigningParams {
168 pub artifact: Arc<dyn ArtifactSource>,
170 pub identity_key: Option<SigningKeyMaterial>,
172 pub device_key: SigningKeyMaterial,
174 pub expires_in_days: Option<u32>,
176 pub note: Option<String>,
178}
179
180#[derive(Debug)]
189pub struct ArtifactSigningResult {
190 pub attestation_json: String,
192 pub rid: String,
194 pub digest: String,
196}
197
198#[derive(Debug, thiserror::Error)]
200#[non_exhaustive]
201pub enum ArtifactSigningError {
202 #[error("identity not found in configured identity storage")]
204 IdentityNotFound,
205
206 #[error("key resolution failed: {0}")]
208 KeyResolutionFailed(String),
209
210 #[error("key decryption failed: {0}")]
212 KeyDecryptionFailed(String),
213
214 #[error("digest computation failed: {0}")]
216 DigestFailed(String),
217
218 #[error("attestation creation failed: {0}")]
220 AttestationFailed(String),
221
222 #[error("attestation re-signing failed: {0}")]
224 ResignFailed(String),
225}
226
227struct SeedMapSigner {
232 seeds: HashMap<String, SecureSeed>,
233}
234
235impl SecureSigner for SeedMapSigner {
236 fn sign_with_alias(
237 &self,
238 alias: &auths_core::storage::keychain::KeyAlias,
239 _passphrase_provider: &dyn PassphraseProvider,
240 message: &[u8],
241 ) -> Result<Vec<u8>, auths_core::AgentError> {
242 let seed = self
243 .seeds
244 .get(alias.as_str())
245 .ok_or(auths_core::AgentError::KeyNotFound)?;
246 provider_bridge::sign_ed25519_sync(seed, message)
247 .map_err(|e| auths_core::AgentError::CryptoError(e.to_string()))
248 }
249
250 fn sign_for_identity(
251 &self,
252 _identity_did: &IdentityDID,
253 _passphrase_provider: &dyn PassphraseProvider,
254 _message: &[u8],
255 ) -> Result<Vec<u8>, auths_core::AgentError> {
256 Err(auths_core::AgentError::KeyNotFound)
257 }
258}
259
260struct ResolvedKey {
261 alias: KeyAlias,
262 seed: SecureSeed,
263 public_key_bytes: Vec<u8>,
264}
265
266fn resolve_optional_key(
267 material: Option<&SigningKeyMaterial>,
268 synthetic_alias: &'static str,
269 keychain: &(dyn KeyStorage + Send + Sync),
270 passphrase_provider: &dyn PassphraseProvider,
271 passphrase_prompt: &str,
272) -> Result<Option<ResolvedKey>, ArtifactSigningError> {
273 match material {
274 None => Ok(None),
275 Some(SigningKeyMaterial::Alias(alias)) => {
276 let (_, encrypted) = keychain
277 .load_key(alias)
278 .map_err(|e| ArtifactSigningError::KeyResolutionFailed(e.to_string()))?;
279 let passphrase = passphrase_provider
280 .get_passphrase(passphrase_prompt)
281 .map_err(|e| ArtifactSigningError::KeyDecryptionFailed(e.to_string()))?;
282 let pkcs8 = core_signer::decrypt_keypair(&encrypted, &passphrase)
283 .map_err(|e| ArtifactSigningError::KeyDecryptionFailed(e.to_string()))?;
284 let (seed, pubkey) = core_signer::load_seed_and_pubkey(&pkcs8)
285 .map_err(|e| ArtifactSigningError::KeyDecryptionFailed(e.to_string()))?;
286 Ok(Some(ResolvedKey {
287 alias: alias.clone(),
288 seed,
289 public_key_bytes: pubkey.to_vec(),
290 }))
291 }
292 Some(SigningKeyMaterial::Direct(seed)) => {
293 let pubkey = provider_bridge::ed25519_public_key_from_seed_sync(seed)
294 .map_err(|e| ArtifactSigningError::KeyDecryptionFailed(e.to_string()))?;
295 Ok(Some(ResolvedKey {
296 alias: KeyAlias::new_unchecked(synthetic_alias),
297 seed: SecureSeed::new(*seed.as_bytes()),
298 public_key_bytes: pubkey.to_vec(),
299 }))
300 }
301 }
302}
303
304fn resolve_required_key(
305 material: &SigningKeyMaterial,
306 synthetic_alias: &'static str,
307 keychain: &(dyn KeyStorage + Send + Sync),
308 passphrase_provider: &dyn PassphraseProvider,
309 passphrase_prompt: &str,
310) -> Result<ResolvedKey, ArtifactSigningError> {
311 resolve_optional_key(
312 Some(material),
313 synthetic_alias,
314 keychain,
315 passphrase_provider,
316 passphrase_prompt,
317 )
318 .map(|opt| {
319 opt.ok_or(ArtifactSigningError::KeyDecryptionFailed(
320 "expected key material but got None".into(),
321 ))
322 })?
323}
324
325pub fn sign_artifact_attestation(
347 params: ArtifactSigningParams,
348 ctx: &AuthsContext,
349) -> Result<ArtifactSigningResult, ArtifactSigningError> {
350 let managed = ctx
351 .identity_storage
352 .load_identity()
353 .map_err(|_| ArtifactSigningError::IdentityNotFound)?;
354
355 let keychain = ctx.key_storage.as_ref();
356 let passphrase_provider = ctx.passphrase_provider.as_ref();
357
358 let identity_resolved = resolve_optional_key(
359 params.identity_key.as_ref(),
360 "__artifact_identity__",
361 keychain,
362 passphrase_provider,
363 "Enter passphrase for identity key:",
364 )?;
365
366 let device_resolved = resolve_required_key(
367 ¶ms.device_key,
368 "__artifact_device__",
369 keychain,
370 passphrase_provider,
371 "Enter passphrase for device key:",
372 )?;
373
374 let mut seeds: HashMap<String, SecureSeed> = HashMap::new();
375 let identity_alias: Option<KeyAlias> = identity_resolved.map(|r| {
376 let alias = r.alias.clone();
377 seeds.insert(r.alias.into_inner(), r.seed);
378 alias
379 });
380 let device_alias = device_resolved.alias.clone();
381 seeds.insert(device_resolved.alias.into_inner(), device_resolved.seed);
382 let device_pk_bytes = device_resolved.public_key_bytes;
383
384 let device_did =
385 DeviceDID::from_ed25519(device_pk_bytes.as_slice().try_into().map_err(|_| {
386 ArtifactSigningError::AttestationFailed("device public key must be 32 bytes".into())
387 })?);
388
389 let artifact_meta = params
390 .artifact
391 .metadata()
392 .map_err(|e| ArtifactSigningError::DigestFailed(e.to_string()))?;
393
394 let rid = format!("sha256:{}", artifact_meta.digest.hex);
395 let now = ctx.clock.now();
396 let meta = AttestationMetadata {
397 timestamp: Some(now),
398 expires_at: params
399 .expires_in_days
400 .map(|d| now + chrono::Duration::days(d as i64)),
401 note: params.note,
402 };
403
404 let payload = serde_json::to_value(&artifact_meta)
405 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
406
407 let signer = SeedMapSigner { seeds };
408 let noop_provider = auths_core::PrefilledPassphraseProvider::new("");
410
411 let mut attestation = create_signed_attestation(
412 now,
413 &rid,
414 &managed.controller_did,
415 &device_did,
416 &device_pk_bytes,
417 Some(payload),
418 &meta,
419 &signer,
420 &noop_provider,
421 identity_alias.as_ref(),
422 Some(&device_alias),
423 vec![Capability::sign_release()],
424 None,
425 None,
426 )
427 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
428
429 resign_attestation(
430 &mut attestation,
431 &signer,
432 &noop_provider,
433 identity_alias.as_ref(),
434 &device_alias,
435 )
436 .map_err(|e| ArtifactSigningError::ResignFailed(e.to_string()))?;
437
438 let attestation_json = serde_json::to_string_pretty(&attestation)
439 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
440
441 Ok(ArtifactSigningResult {
442 attestation_json,
443 rid,
444 digest: artifact_meta.digest.hex,
445 })
446}