1use crate::context::AuthsContext;
10use crate::ports::artifact::{ArtifactDigest, ArtifactMetadata, ArtifactSource};
11use auths_core::crypto::signer as core_signer;
12use auths_core::crypto::ssh::{self, SecureSeed};
13use auths_core::signing::{PassphraseProvider, SecureSigner};
14use auths_core::storage::keychain::{IdentityDID, KeyAlias, KeyStorage};
15use auths_id::attestation::core::resign_attestation;
16use auths_id::attestation::create::create_signed_attestation;
17use auths_id::storage::git_refs::AttestationMetadata;
18use auths_verifier::core::{OidcBinding, ResourceId, SignerType};
19use auths_verifier::types::CanonicalDid;
20use chrono::{DateTime, Utc};
21use sha2::{Digest, Sha256};
22use std::collections::HashMap;
23use std::path::Path;
24use std::sync::Arc;
25use zeroize::Zeroizing;
26
27#[derive(Debug, thiserror::Error)]
29#[non_exhaustive]
30pub enum SigningError {
31 #[error("identity is frozen: {0}")]
33 IdentityFrozen(String),
34 #[error("key resolution failed: {0}")]
36 KeyResolution(String),
37 #[error("signing operation failed: {0}")]
39 SigningFailed(String),
40 #[error("invalid passphrase")]
42 InvalidPassphrase,
43 #[error("PEM encoding failed: {0}")]
45 PemEncoding(String),
46 #[error("agent unavailable: {0}")]
48 AgentUnavailable(String),
49 #[error("agent signing failed")]
51 AgentSigningFailed(#[source] crate::ports::agent::AgentSigningError),
52 #[error("passphrase exhausted after {attempts} attempt(s)")]
54 PassphraseExhausted {
55 attempts: usize,
57 },
58 #[error("keychain unavailable: {0}")]
60 KeychainUnavailable(String),
61 #[error("key decryption failed: {0}")]
63 KeyDecryptionFailed(String),
64}
65
66impl auths_core::error::AuthsErrorInfo for SigningError {
67 fn error_code(&self) -> &'static str {
68 match self {
69 Self::IdentityFrozen(_) => "AUTHS-E5901",
70 Self::KeyResolution(_) => "AUTHS-E5902",
71 Self::SigningFailed(_) => "AUTHS-E5903",
72 Self::InvalidPassphrase => "AUTHS-E5904",
73 Self::PemEncoding(_) => "AUTHS-E5905",
74 Self::AgentUnavailable(_) => "AUTHS-E5906",
75 Self::AgentSigningFailed(_) => "AUTHS-E5907",
76 Self::PassphraseExhausted { .. } => "AUTHS-E5908",
77 Self::KeychainUnavailable(_) => "AUTHS-E5909",
78 Self::KeyDecryptionFailed(_) => "AUTHS-E5910",
79 }
80 }
81
82 fn suggestion(&self) -> Option<&'static str> {
83 match self {
84 Self::IdentityFrozen(_) => Some("To unfreeze: auths emergency unfreeze"),
85 Self::KeyResolution(_) => Some("Run `auths key list` to check available keys"),
86 Self::SigningFailed(_) => Some(
87 "The signing operation failed; verify your key is accessible with `auths key list`",
88 ),
89 Self::InvalidPassphrase => Some("Check your passphrase and try again"),
90 Self::PemEncoding(_) => {
91 Some("Failed to encode the key in PEM format; the key material may be corrupted")
92 }
93 Self::AgentUnavailable(_) => Some("Start the agent with `auths agent start`"),
94 Self::AgentSigningFailed(_) => Some("Check agent logs with `auths agent status`"),
95 Self::PassphraseExhausted { .. } => Some(
96 "The passphrase you entered is incorrect (tried 3 times). Verify it matches what you set during init, or try: auths key export --key-alias <alias> --format pub",
97 ),
98 Self::KeychainUnavailable(_) => Some("Run `auths doctor` to diagnose keychain issues"),
99 Self::KeyDecryptionFailed(_) => Some("Check your passphrase and try again"),
100 }
101 }
102}
103
104pub struct SigningConfig {
116 pub namespace: String,
118}
119
120pub fn validate_freeze_state(
131 repo_path: &Path,
132 now: chrono::DateTime<chrono::Utc>,
133) -> Result<(), SigningError> {
134 use auths_id::freeze::load_active_freeze;
135
136 if let Some(state) = load_active_freeze(repo_path, now)
137 .map_err(|e| SigningError::IdentityFrozen(e.to_string()))?
138 {
139 return Err(SigningError::IdentityFrozen(format!(
140 "frozen until {}. Remaining: {}. To unfreeze: auths emergency unfreeze",
141 state.frozen_until.format("%Y-%m-%d %H:%M UTC"),
142 state.expires_description(now),
143 )));
144 }
145
146 Ok(())
147}
148
149pub fn construct_signature_payload(data: &[u8], namespace: &str) -> Result<Vec<u8>, SigningError> {
160 ssh::construct_sshsig_signed_data(data, namespace)
161 .map_err(|e| SigningError::SigningFailed(e.to_string()))
162}
163
164pub fn sign_with_seed(
176 seed: &SecureSeed,
177 data: &[u8],
178 namespace: &str,
179 curve: auths_crypto::CurveType,
180) -> Result<String, SigningError> {
181 ssh::create_sshsig(seed, data, namespace, curve)
182 .map_err(|e| SigningError::PemEncoding(e.to_string()))
183}
184
185pub enum SigningKeyMaterial {
195 Alias(KeyAlias),
197 Direct(SecureSeed),
199}
200
201pub struct ArtifactSigningParams {
215 pub artifact: Arc<dyn ArtifactSource>,
217 pub identity_key: Option<SigningKeyMaterial>,
219 pub device_key: SigningKeyMaterial,
221 pub expires_in: Option<u64>,
223 pub note: Option<String>,
225 pub commit_sha: Option<String>,
227}
228
229#[derive(Debug)]
238pub struct ArtifactSigningResult {
239 pub attestation_json: String,
241 pub rid: ResourceId,
243 pub digest: String,
245 pub dsse_signature: Option<Vec<u8>>,
248}
249
250#[derive(Debug, thiserror::Error)]
252#[non_exhaustive]
253pub enum ArtifactSigningError {
254 #[error("identity not found in configured identity storage")]
256 IdentityNotFound,
257
258 #[error("key resolution failed: {0}")]
260 KeyResolutionFailed(String),
261
262 #[error("key decryption failed: {0}")]
264 KeyDecryptionFailed(String),
265
266 #[error("digest computation failed: {0}")]
268 DigestFailed(String),
269
270 #[error("attestation creation failed: {0}")]
272 AttestationFailed(String),
273
274 #[error("attestation re-signing failed: {0}")]
276 ResignFailed(String),
277
278 #[error("invalid commit SHA: {0} (expected 40 or 64 hex characters)")]
280 InvalidCommitSha(String),
281
282 #[error("device revoked: {0}")]
284 DeviceRevoked(String),
285
286 #[error("signing key rotated out of the KEL: {0}")]
288 KeyRotatedOut(String),
289}
290
291impl auths_core::error::AuthsErrorInfo for ArtifactSigningError {
292 fn error_code(&self) -> &'static str {
293 match self {
294 Self::IdentityNotFound => "AUTHS-E5850",
295 Self::KeyResolutionFailed(_) => "AUTHS-E5851",
296 Self::KeyDecryptionFailed(_) => "AUTHS-E5852",
297 Self::DigestFailed(_) => "AUTHS-E5853",
298 Self::AttestationFailed(_) => "AUTHS-E5854",
299 Self::ResignFailed(_) => "AUTHS-E5855",
300 Self::InvalidCommitSha(_) => "AUTHS-E5856",
301 Self::DeviceRevoked(_) => "AUTHS-E5857",
302 Self::KeyRotatedOut(_) => "AUTHS-E5858",
303 }
304 }
305
306 fn suggestion(&self) -> Option<&'static str> {
307 match self {
308 Self::IdentityNotFound => {
309 Some("Run `auths init` to create an identity, or `auths key import` to restore one")
310 }
311 Self::KeyResolutionFailed(_) => {
312 Some("Run `auths status` to see available device aliases")
313 }
314 Self::KeyDecryptionFailed(_) => Some("Check your passphrase and try again"),
315 Self::DigestFailed(_) => Some("Verify the file exists and is readable"),
316 Self::AttestationFailed(_) => Some("Check identity storage with `auths status`"),
317 Self::ResignFailed(_) => {
318 Some("Verify your device key is accessible with `auths status`")
319 }
320 Self::InvalidCommitSha(_) => {
321 Some("Provide a full SHA-1 (40 hex chars) or SHA-256 (64 hex chars) commit hash")
322 }
323 Self::DeviceRevoked(_) => {
324 Some("This device has been removed; pair a new device or sign from an active one")
325 }
326 Self::KeyRotatedOut(_) => {
327 Some("This key was rotated out; sign with the identity's current key")
328 }
329 }
330 }
331}
332
333struct SeedMapSigner {
339 seeds: HashMap<String, (SecureSeed, auths_crypto::CurveType)>,
340}
341
342impl SecureSigner for SeedMapSigner {
343 fn sign_with_alias(
344 &self,
345 alias: &auths_core::storage::keychain::KeyAlias,
346 _passphrase_provider: &dyn PassphraseProvider,
347 message: &[u8],
348 ) -> Result<Vec<u8>, auths_core::AgentError> {
349 let (seed, curve) = self
350 .seeds
351 .get(alias.as_str())
352 .ok_or(auths_core::AgentError::KeyNotFound)?;
353 let typed = match curve {
354 auths_crypto::CurveType::Ed25519 => auths_crypto::TypedSeed::Ed25519(*seed.as_bytes()),
355 auths_crypto::CurveType::P256 => auths_crypto::TypedSeed::P256(*seed.as_bytes()),
356 };
357 auths_crypto::typed_sign(&typed, message)
358 .map_err(|e| auths_core::AgentError::CryptoError(e.to_string()))
359 }
360
361 fn sign_for_identity(
362 &self,
363 _identity_did: &IdentityDID,
364 _passphrase_provider: &dyn PassphraseProvider,
365 _message: &[u8],
366 ) -> Result<Vec<u8>, auths_core::AgentError> {
367 Err(auths_core::AgentError::KeyNotFound)
368 }
369}
370
371struct ResolvedKey {
372 alias: KeyAlias,
373 seed: Option<SecureSeed>,
375 public_key_bytes: Vec<u8>,
376 curve: auths_crypto::CurveType,
377 is_hardware: bool,
379 identity_did: Option<IdentityDID>,
383}
384
385fn resolve_optional_key(
386 material: Option<&SigningKeyMaterial>,
387 synthetic_alias: &'static str,
388 keychain: &(dyn KeyStorage + Send + Sync),
389 passphrase_provider: &dyn PassphraseProvider,
390 passphrase_prompt: &str,
391) -> Result<Option<ResolvedKey>, ArtifactSigningError> {
392 match material {
393 None => Ok(None),
394 Some(SigningKeyMaterial::Alias(alias)) => {
395 if keychain.is_hardware_backend() {
399 let (pubkey, curve) = auths_core::storage::keychain::extract_public_key_bytes(
400 keychain,
401 alias,
402 passphrase_provider,
403 )
404 .map_err(|e| ArtifactSigningError::KeyResolutionFailed(e.to_string()))?;
405 return Ok(Some(ResolvedKey {
406 alias: alias.clone(),
407 seed: None,
408 public_key_bytes: pubkey,
409 curve,
410 is_hardware: true,
411 identity_did: None,
412 }));
413 }
414
415 let (device_did, _role, encrypted) = keychain
416 .load_key(alias)
417 .map_err(|e| ArtifactSigningError::KeyResolutionFailed(e.to_string()))?;
418 let passphrase = passphrase_provider
419 .get_passphrase(passphrase_prompt)
420 .map_err(|e| ArtifactSigningError::KeyDecryptionFailed(e.to_string()))?;
421 debug_assert!(
423 !keychain.is_hardware_backend(),
424 "SE keys must never reach decrypt_keypair"
425 );
426 let pkcs8 = core_signer::decrypt_keypair(&encrypted, &passphrase)
427 .map_err(|e| ArtifactSigningError::KeyDecryptionFailed(e.to_string()))?;
428 let (seed, pubkey, curve) = core_signer::load_seed_and_pubkey(&pkcs8)
429 .map_err(|e| ArtifactSigningError::KeyDecryptionFailed(e.to_string()))?;
430 Ok(Some(ResolvedKey {
431 alias: alias.clone(),
432 seed: Some(seed),
433 public_key_bytes: pubkey.to_vec(),
434 curve,
435 is_hardware: false,
436 identity_did: Some(device_did),
437 }))
438 }
439 Some(SigningKeyMaterial::Direct(seed)) => {
440 let typed = auths_crypto::TypedSeed::Ed25519(*seed.as_bytes());
441 let pubkey = auths_crypto::typed_public_key(&typed)
442 .map_err(|e| ArtifactSigningError::KeyDecryptionFailed(e.to_string()))?;
443 Ok(Some(ResolvedKey {
444 alias: KeyAlias::new_unchecked(synthetic_alias),
445 seed: Some(SecureSeed::new(*seed.as_bytes())),
446 public_key_bytes: pubkey,
447 curve: auths_crypto::CurveType::Ed25519,
448 is_hardware: false,
449 identity_did: None,
450 }))
451 }
452 }
453}
454
455fn resolve_required_key(
456 material: &SigningKeyMaterial,
457 synthetic_alias: &'static str,
458 keychain: &(dyn KeyStorage + Send + Sync),
459 passphrase_provider: &dyn PassphraseProvider,
460 passphrase_prompt: &str,
461) -> Result<ResolvedKey, ArtifactSigningError> {
462 resolve_optional_key(
463 Some(material),
464 synthetic_alias,
465 keychain,
466 passphrase_provider,
467 passphrase_prompt,
468 )
469 .map(|opt| {
470 opt.ok_or(ArtifactSigningError::KeyDecryptionFailed(
471 "expected key material but got None".into(),
472 ))
473 })?
474}
475
476fn resolve_root_issuer_key(
491 controller_did: &IdentityDID,
492 keychain: &(dyn KeyStorage + Send + Sync),
493 passphrase_provider: &dyn PassphraseProvider,
494) -> Result<ResolvedKey, ArtifactSigningError> {
495 let alias = keychain
496 .list_aliases_for_identity(controller_did)
497 .map_err(|e| ArtifactSigningError::KeyResolutionFailed(e.to_string()))?
498 .into_iter()
499 .find(|a| !a.as_str().contains("--next-"))
500 .ok_or_else(|| {
501 ArtifactSigningError::KeyResolutionFailed(format!(
502 "no signing key found for root identity {}",
503 controller_did.as_str()
504 ))
505 })?;
506 resolve_required_key(
507 &SigningKeyMaterial::Alias(alias),
508 "__artifact_issuer__",
509 keychain,
510 passphrase_provider,
511 "Enter passphrase for identity key:",
512 )
513}
514
515fn enforce_signer_authority(
534 ctx: &AuthsContext,
535 controller_did: &IdentityDID,
536 device_did: &IdentityDID,
537 device_pk_bytes: &[u8],
538) -> Result<(), ArtifactSigningError> {
539 use std::ops::ControlFlow;
540
541 let device_prefix = auths_id::keri::parse_did_keri(device_did.as_str())
542 .map_err(|e| ArtifactSigningError::KeyResolutionFailed(e.to_string()))?;
543 let root_prefix = auths_id::keri::parse_did_keri(controller_did.as_str())
544 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
545
546 let mut revoked = false;
549 ctx.registry
550 .visit_events(&root_prefix, 0, &mut |event| {
551 let hit = event.anchors().iter().any(|seal| {
552 matches!(seal, auths_id::keri::Seal::Digest { d } if d.as_str() == device_prefix.as_str())
553 });
554 if hit {
555 revoked = true;
556 ControlFlow::Break(())
557 } else {
558 ControlFlow::Continue(())
559 }
560 })
561 .map_err(|e| {
562 ArtifactSigningError::KeyResolutionFailed(format!(
563 "cannot read root KEL to check revocation: {e}"
564 ))
565 })?;
566 if revoked {
567 return Err(ArtifactSigningError::DeviceRevoked(
568 device_did.as_str().to_string(),
569 ));
570 }
571
572 let state = ctx.registry.get_key_state(&device_prefix).map_err(|e| {
575 ArtifactSigningError::KeyResolutionFailed(format!(
576 "cannot read KEL state for {} to verify the signing key is current: {e}",
577 device_did.as_str()
578 ))
579 })?;
580 let is_current = state.current_keys.iter().any(|k| match k.parse() {
581 Ok(pk) => {
584 let key_bytes: &[u8] = pk.as_bytes();
585 key_bytes == device_pk_bytes
586 }
587 Err(_) => false,
588 });
589 if !is_current {
590 return Err(ArtifactSigningError::KeyRotatedOut(
591 device_did.as_str().to_string(),
592 ));
593 }
594 Ok(())
595}
596
597pub fn validate_commit_sha(sha: &str) -> Result<String, ArtifactSigningError> {
607 let normalized = sha.to_ascii_lowercase();
608 let len = normalized.len();
609 if (len != 40 && len != 64) || !normalized.bytes().all(|b| b.is_ascii_hexdigit()) {
610 return Err(ArtifactSigningError::InvalidCommitSha(sha.to_string()));
611 }
612 Ok(normalized)
613}
614
615#[allow(clippy::too_many_lines)]
642pub fn sign_artifact(
643 params: ArtifactSigningParams,
644 ctx: &AuthsContext,
645) -> Result<ArtifactSigningResult, ArtifactSigningError> {
646 let managed = ctx
647 .identity_storage
648 .load_identity()
649 .map_err(|_| ArtifactSigningError::IdentityNotFound)?;
650
651 let keychain = ctx.key_storage.as_ref();
652 let passphrase_provider = ctx.passphrase_provider.as_ref();
653
654 let issuer_did = CanonicalDid::from(managed.controller_did.clone());
660 let identity_resolved = match params.identity_key.as_ref() {
661 Some(explicit) => resolve_optional_key(
662 Some(explicit),
663 "__artifact_identity__",
664 keychain,
665 passphrase_provider,
666 "Enter passphrase for identity key:",
667 )?,
668 None => Some(resolve_root_issuer_key(
669 &managed.controller_did,
670 keychain,
671 passphrase_provider,
672 )?),
673 };
674
675 let device_resolved = resolve_required_key(
676 ¶ms.device_key,
677 "__artifact_device__",
678 keychain,
679 passphrase_provider,
680 "Enter passphrase for device key:",
681 )?;
682
683 if let Some(ref device_did) = device_resolved.identity_did {
684 enforce_signer_authority(
685 ctx,
686 &managed.controller_did,
687 device_did,
688 &device_resolved.public_key_bytes,
689 )?;
690 }
691
692 let mut seeds: HashMap<String, (SecureSeed, auths_crypto::CurveType)> = HashMap::new();
693 let identity_alias: Option<KeyAlias> = identity_resolved.map(|r| {
694 let alias = r.alias.clone();
695 let curve = r.curve;
696 if let Some(seed) = r.seed {
697 seeds.insert(r.alias.into_inner(), (seed, curve));
698 }
699 alias
700 });
701 let device_alias = device_resolved.alias.clone();
702 let device_is_hardware = device_resolved.is_hardware;
703 let device_curve = device_resolved.curve;
704 if let Some(seed) = device_resolved.seed {
705 seeds.insert(device_resolved.alias.into_inner(), (seed, device_curve));
706 }
707 let device_pk_bytes = device_resolved.public_key_bytes;
708
709 let device_did = device_resolved
714 .identity_did
715 .clone()
716 .map(CanonicalDid::from)
717 .unwrap_or_else(|| {
718 CanonicalDid::from_public_key_did_key(&device_pk_bytes, device_resolved.curve)
719 });
720
721 let artifact_meta = params
722 .artifact
723 .metadata()
724 .map_err(|e| ArtifactSigningError::DigestFailed(e.to_string()))?;
725
726 let rid = ResourceId::new(format!("sha256:{}", artifact_meta.digest.hex));
727 let now = ctx.clock.now();
728 let meta = AttestationMetadata {
729 timestamp: Some(now),
730 expires_at: params
731 .expires_in
732 .map(|s| now + chrono::Duration::seconds(s as i64)),
733 note: params.note,
734 };
735
736 let payload = serde_json::to_value(&artifact_meta)
737 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
738
739 let validated_commit_sha = params
740 .commit_sha
741 .map(|sha| validate_commit_sha(&sha))
742 .transpose()?;
743
744 let attestation_json = create_and_sign_attestation(
745 ctx,
746 seeds,
747 device_is_hardware,
748 now,
749 &rid,
750 &issuer_did,
754 &device_did,
755 &device_pk_bytes,
756 device_curve,
757 payload,
758 &meta,
759 identity_alias.as_ref(),
760 &device_alias,
761 validated_commit_sha,
762 )?;
763
764 if let Some(ref alias) = identity_alias {
768 anchor_artifact_attestation(ctx, &managed.controller_did, alias, &attestation_json, now)?;
769 }
770
771 Ok(ArtifactSigningResult {
772 attestation_json,
773 rid,
774 digest: artifact_meta.digest.hex,
775 dsse_signature: None,
776 })
777}
778
779fn anchor_artifact_attestation(
780 ctx: &AuthsContext,
781 controller_did: &auths_core::storage::keychain::IdentityDID,
782 alias: &KeyAlias,
783 attestation_json: &str,
784 now: DateTime<Utc>,
785) -> Result<(), ArtifactSigningError> {
786 let prefix = auths_id::keri::parse_did_keri(controller_did.as_str())
787 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
788 let att: auths_verifier::core::Attestation = serde_json::from_str(attestation_json)
789 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
790 let storage_signer = auths_core::signing::StorageSigner::new(Arc::clone(&ctx.key_storage));
791 let mut batch = auths_id::storage::registry::backend::AtomicWriteBatch::new();
792 batch.stage_attestation(att.clone());
793 auths_id::keri::anchor_and_persist_via_backend(
794 ctx.registry.as_ref(),
795 &storage_signer,
796 alias,
797 ctx.passphrase_provider.as_ref(),
798 &prefix,
799 &att,
800 &mut batch,
801 &ctx.witness_params(),
802 now,
803 )
804 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
805 Ok(())
806}
807
808#[allow(clippy::too_many_arguments)]
810fn create_and_sign_attestation(
811 ctx: &AuthsContext,
812 seeds: HashMap<String, (SecureSeed, auths_crypto::CurveType)>,
813 device_is_hardware: bool,
814 now: DateTime<Utc>,
815 rid: &ResourceId,
816 issuer: &CanonicalDid,
817 subject: &CanonicalDid,
818 device_pk_bytes: &[u8],
819 device_curve: auths_crypto::CurveType,
820 payload: serde_json::Value,
821 meta: &AttestationMetadata,
822 identity_alias: Option<&KeyAlias>,
823 device_alias: &KeyAlias,
824 commit_sha: Option<String>,
825) -> Result<String, ArtifactSigningError> {
826 let seed_signer = SeedMapSigner { seeds };
827 let storage_signer = auths_core::signing::StorageSigner::new(Arc::clone(&ctx.key_storage));
828 let signer: &dyn SecureSigner = if device_is_hardware {
829 &storage_signer
830 } else {
831 &seed_signer
832 };
833 let noop_provider = auths_core::PrefilledPassphraseProvider::new("");
834
835 let issuer_canonical = issuer.clone();
836 let mut attestation = create_signed_attestation(
837 now,
838 auths_id::attestation::create::AttestationInput {
839 rid: rid.as_str(),
840 issuer: &issuer_canonical,
841 subject,
842 device_public_key: device_pk_bytes,
843 device_curve,
844 payload: Some(payload),
845 meta,
846 identity_alias,
847 device_alias: Some(device_alias),
848 delegated_by: None,
849 commit_sha,
850 signer_type: None,
851 oidc_binding: None,
852 },
853 signer,
854 &noop_provider,
855 )
856 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
857
858 resign_attestation(
859 &mut attestation,
860 signer,
861 &noop_provider,
862 identity_alias,
863 device_alias,
864 )
865 .map_err(|e| ArtifactSigningError::ResignFailed(e.to_string()))?;
866
867 serde_json::to_string_pretty(&attestation)
868 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))
869}
870
871pub struct EphemeralSignRequest<'a> {
876 pub data: &'a [u8],
878 pub artifact_name: Option<String>,
880 pub commit_sha: String,
882 pub curve: auths_crypto::CurveType,
887 pub expires_in: Option<u64>,
889 pub note: Option<String>,
891 pub ci_env: Option<serde_json::Value>,
893 pub oidc_binding: Option<OidcBinding>,
896}
897
898pub fn sign_artifact_ephemeral(
926 now: DateTime<Utc>,
927 req: EphemeralSignRequest<'_>,
928) -> Result<ArtifactSigningResult, ArtifactSigningError> {
929 let EphemeralSignRequest {
930 data,
931 artifact_name,
932 commit_sha,
933 curve,
934 expires_in,
935 note,
936 ci_env,
937 oidc_binding,
938 } = req;
939 let mut seed_bytes = Zeroizing::new([0u8; 32]);
941 ring::rand::SecureRandom::fill(&ring::rand::SystemRandom::new(), seed_bytes.as_mut())
942 .map_err(|_| ArtifactSigningError::AttestationFailed("RNG failure".into()))?;
943
944 let typed_seed = auths_crypto::TypedSeed::from_curve(curve, *seed_bytes);
946 let pubkey_vec = auths_crypto::typed_public_key(&typed_seed)
947 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
948
949 let device_did = CanonicalDid::from_public_key_did_key(&pubkey_vec, curve);
952
953 let digest_hex = hex::encode(Sha256::digest(data));
955 let artifact_meta = ArtifactMetadata {
956 artifact_type: "file".to_string(),
957 digest: ArtifactDigest {
958 algorithm: "sha256".to_string(),
959 hex: digest_hex,
960 },
961 name: artifact_name,
962 size: Some(data.len() as u64),
963 };
964
965 let mut payload_value = serde_json::to_value(&artifact_meta)
966 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
967
968 if let Some(env) = ci_env
969 && let serde_json::Value::Object(ref mut map) = payload_value
970 {
971 map.insert("ci_environment".to_string(), env);
972 }
973
974 let rid = ResourceId::new(format!("sha256:{}", artifact_meta.digest.hex));
975 let meta = AttestationMetadata {
976 timestamp: Some(now),
977 expires_at: expires_in.map(|s| now + chrono::Duration::seconds(s as i64)),
978 note,
979 };
980
981 let validated_sha = validate_commit_sha(&commit_sha)?;
983
984 let identity_alias = KeyAlias::new_unchecked("__ephemeral_identity__");
986 let device_alias = KeyAlias::new_unchecked("__ephemeral_device__");
987
988 let mut seeds: HashMap<String, (SecureSeed, auths_crypto::CurveType)> = HashMap::new();
989 seeds.insert(
990 identity_alias.as_str().to_string(),
991 (SecureSeed::new(*seed_bytes), curve),
992 );
993 seeds.insert(
994 device_alias.as_str().to_string(),
995 (SecureSeed::new(*seed_bytes), curve),
996 );
997 let signer = SeedMapSigner { seeds };
998 let noop_provider = auths_core::PrefilledPassphraseProvider::new("");
999
1000 let attestation = create_signed_attestation(
1002 now,
1003 auths_id::attestation::create::AttestationInput {
1004 rid: rid.as_str(),
1005 issuer: &device_did,
1006 subject: &device_did,
1007 device_public_key: &pubkey_vec,
1008 device_curve: curve,
1009 payload: Some(payload_value),
1010 meta: &meta,
1011 identity_alias: Some(&identity_alias),
1012 device_alias: Some(&device_alias),
1013 delegated_by: None,
1014 commit_sha: Some(validated_sha),
1015 signer_type: Some(SignerType::Workload),
1016 oidc_binding,
1017 },
1018 &signer,
1019 &noop_provider,
1020 )
1021 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
1022
1023 let attestation_json = serde_json::to_string_pretty(&attestation)
1024 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
1025
1026 let pae = dsse_pae("application/vnd.auths+json", attestation_json.as_bytes());
1028 let dsse_sig = auths_crypto::typed_sign(&typed_seed, &pae)
1029 .map_err(|e| ArtifactSigningError::AttestationFailed(format!("DSSE sign: {e}")))?;
1030
1031 Ok(ArtifactSigningResult {
1032 attestation_json,
1033 rid,
1034 digest: artifact_meta.digest.hex,
1035 dsse_signature: Some(dsse_sig),
1036 })
1037}
1038
1039pub fn sign_artifact_raw(
1060 now: DateTime<Utc>,
1061 seed: &SecureSeed,
1062 identity_did: &IdentityDID,
1063 data: &[u8],
1064 expires_in: Option<u64>,
1065 note: Option<String>,
1066 commit_sha: Option<String>,
1067) -> Result<ArtifactSigningResult, ArtifactSigningError> {
1068 let curve = auths_crypto::CurveType::default();
1070 let typed = auths_crypto::TypedSeed::from_curve(curve, *seed.as_bytes());
1071 let pubkey = auths_crypto::typed_public_key(&typed)
1072 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
1073
1074 let device_did = CanonicalDid::from_public_key_did_key(&pubkey, curve);
1075
1076 let digest_hex = hex::encode(Sha256::digest(data));
1077 let artifact_meta = ArtifactMetadata {
1078 artifact_type: "bytes".to_string(),
1079 digest: ArtifactDigest {
1080 algorithm: "sha256".to_string(),
1081 hex: digest_hex,
1082 },
1083 name: None,
1084 size: Some(data.len() as u64),
1085 };
1086
1087 let rid = ResourceId::new(format!("sha256:{}", artifact_meta.digest.hex));
1088 let meta = AttestationMetadata {
1089 timestamp: Some(now),
1090 expires_at: expires_in.map(|s| now + chrono::Duration::seconds(s as i64)),
1091 note,
1092 };
1093
1094 let payload = serde_json::to_value(&artifact_meta)
1095 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
1096
1097 let identity_alias = KeyAlias::new_unchecked("__raw_identity__");
1098 let device_alias = KeyAlias::new_unchecked("__raw_device__");
1099
1100 let mut seeds: HashMap<String, (SecureSeed, auths_crypto::CurveType)> = HashMap::new();
1101 seeds.insert(
1102 identity_alias.as_str().to_string(),
1103 (SecureSeed::new(*seed.as_bytes()), curve),
1104 );
1105 seeds.insert(
1106 device_alias.as_str().to_string(),
1107 (SecureSeed::new(*seed.as_bytes()), curve),
1108 );
1109 let signer = SeedMapSigner { seeds };
1110 let noop_provider = auths_core::PrefilledPassphraseProvider::new("");
1112
1113 let validated_commit_sha = commit_sha
1114 .map(|sha| validate_commit_sha(&sha))
1115 .transpose()?;
1116
1117 let issuer_canonical = CanonicalDid::from(identity_did.clone());
1118 let attestation = create_signed_attestation(
1119 now,
1120 auths_id::attestation::create::AttestationInput {
1121 rid: rid.as_str(),
1122 issuer: &issuer_canonical,
1123 subject: &device_did,
1124 device_public_key: &pubkey,
1125 device_curve: curve,
1126 payload: Some(payload),
1127 meta: &meta,
1128 identity_alias: Some(&identity_alias),
1129 device_alias: Some(&device_alias),
1130 delegated_by: None,
1131 commit_sha: validated_commit_sha,
1132 signer_type: None,
1133 oidc_binding: None,
1134 },
1135 &signer,
1136 &noop_provider,
1137 )
1138 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
1139
1140 let attestation_json = serde_json::to_string_pretty(&attestation)
1141 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
1142
1143 Ok(ArtifactSigningResult {
1144 attestation_json,
1145 rid,
1146 digest: artifact_meta.digest.hex,
1147 dsse_signature: None,
1148 })
1149}
1150
1151pub fn dsse_pae(payload_type: &str, payload: &[u8]) -> Vec<u8> {
1156 let header = format!(
1157 "DSSEv1 {} {} {} ",
1158 payload_type.len(),
1159 payload_type,
1160 payload.len()
1161 );
1162 let mut result = Vec::with_capacity(header.len() + payload.len());
1163 result.extend_from_slice(header.as_bytes());
1164 result.extend_from_slice(payload);
1165 result
1166}