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 #[error("no signing key under alias '{alias}'")]
66 KeyNotFound {
67 alias: String,
69 },
70}
71
72impl auths_core::error::AuthsErrorInfo for SigningError {
73 fn error_code(&self) -> &'static str {
74 match self {
75 Self::IdentityFrozen(_) => "AUTHS-E5901",
76 Self::KeyResolution(_) => "AUTHS-E5902",
77 Self::SigningFailed(_) => "AUTHS-E5903",
78 Self::InvalidPassphrase => "AUTHS-E5904",
79 Self::PemEncoding(_) => "AUTHS-E5905",
80 Self::AgentUnavailable(_) => "AUTHS-E5906",
81 Self::AgentSigningFailed(_) => "AUTHS-E5907",
82 Self::PassphraseExhausted { .. } => "AUTHS-E5908",
83 Self::KeychainUnavailable(_) => "AUTHS-E5909",
84 Self::KeyDecryptionFailed(_) => "AUTHS-E5910",
85 Self::KeyNotFound { .. } => "AUTHS-E5911",
86 }
87 }
88
89 fn suggestion(&self) -> Option<&'static str> {
90 match self {
91 Self::IdentityFrozen(_) => Some("To unfreeze: auths emergency unfreeze"),
92 Self::KeyResolution(_) => Some("Run `auths key list` to check available keys"),
93 Self::SigningFailed(_) => Some(
94 "The signing operation failed; verify your key is accessible with `auths key list`",
95 ),
96 Self::InvalidPassphrase => Some("Check your passphrase and try again"),
97 Self::PemEncoding(_) => {
98 Some("Failed to encode the key in PEM format; the key material may be corrupted")
99 }
100 Self::AgentUnavailable(_) => Some("Start the agent with `auths agent start`"),
101 Self::AgentSigningFailed(_) => Some("Check agent logs with `auths agent status`"),
102 Self::PassphraseExhausted { .. } => Some(
103 "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",
104 ),
105 Self::KeychainUnavailable(_) => Some("Run `auths doctor` to diagnose keychain issues"),
106 Self::KeyDecryptionFailed(_) => Some("Check your passphrase and try again"),
107 Self::KeyNotFound { .. } => {
108 Some("Run `auths key list` to see available aliases, or `auths init` to create one")
109 }
110 }
111 }
112}
113
114pub struct SigningConfig {
126 pub namespace: String,
128}
129
130pub fn validate_freeze_state(
141 repo_path: &Path,
142 now: chrono::DateTime<chrono::Utc>,
143) -> Result<(), SigningError> {
144 use auths_id::freeze::load_active_freeze;
145
146 if let Some(state) = load_active_freeze(repo_path, now)
147 .map_err(|e| SigningError::IdentityFrozen(e.to_string()))?
148 {
149 return Err(SigningError::IdentityFrozen(format!(
150 "frozen until {}. Remaining: {}. To unfreeze: auths emergency unfreeze",
151 state.frozen_until.format("%Y-%m-%d %H:%M UTC"),
152 state.expires_description(now),
153 )));
154 }
155
156 Ok(())
157}
158
159pub fn construct_signature_payload(data: &[u8], namespace: &str) -> Result<Vec<u8>, SigningError> {
170 ssh::construct_sshsig_signed_data(data, namespace)
171 .map_err(|e| SigningError::SigningFailed(e.to_string()))
172}
173
174pub fn sign_with_seed(
186 seed: &SecureSeed,
187 data: &[u8],
188 namespace: &str,
189 curve: auths_crypto::CurveType,
190) -> Result<String, SigningError> {
191 ssh::create_sshsig(seed, data, namespace, curve)
192 .map_err(|e| SigningError::PemEncoding(e.to_string()))
193}
194
195pub enum SigningKeyMaterial {
205 Alias(KeyAlias),
207 Direct(SecureSeed),
209}
210
211pub struct ArtifactSigningParams {
225 pub artifact: Arc<dyn ArtifactSource>,
227 pub identity_key: Option<SigningKeyMaterial>,
229 pub device_key: SigningKeyMaterial,
231 pub expires_in: Option<u64>,
233 pub note: Option<String>,
235 pub commit_sha: Option<String>,
237}
238
239#[derive(Debug)]
248pub struct ArtifactSigningResult {
249 pub attestation_json: String,
251 pub rid: ResourceId,
253 pub digest: String,
255 pub dsse_signature: Option<Vec<u8>>,
258}
259
260#[derive(Debug, thiserror::Error)]
262#[non_exhaustive]
263pub enum ArtifactSigningError {
264 #[error("identity not found in configured identity storage")]
266 IdentityNotFound,
267
268 #[error("key resolution failed: {0}")]
270 KeyResolutionFailed(String),
271
272 #[error("key decryption failed: {0}")]
274 KeyDecryptionFailed(String),
275
276 #[error("digest computation failed: {0}")]
278 DigestFailed(String),
279
280 #[error("attestation creation failed: {0}")]
282 AttestationFailed(String),
283
284 #[error("attestation re-signing failed: {0}")]
286 ResignFailed(String),
287
288 #[error("invalid commit SHA: {0} (expected 40 or 64 hex characters)")]
290 InvalidCommitSha(String),
291
292 #[error("device revoked: {0}")]
294 DeviceRevoked(String),
295
296 #[error("signing key rotated out of the KEL: {0}")]
298 KeyRotatedOut(String),
299}
300
301impl auths_core::error::AuthsErrorInfo for ArtifactSigningError {
302 fn error_code(&self) -> &'static str {
303 match self {
304 Self::IdentityNotFound => "AUTHS-E5850",
305 Self::KeyResolutionFailed(_) => "AUTHS-E5851",
306 Self::KeyDecryptionFailed(_) => "AUTHS-E5852",
307 Self::DigestFailed(_) => "AUTHS-E5853",
308 Self::AttestationFailed(_) => "AUTHS-E5854",
309 Self::ResignFailed(_) => "AUTHS-E5855",
310 Self::InvalidCommitSha(_) => "AUTHS-E5856",
311 Self::DeviceRevoked(_) => "AUTHS-E5857",
312 Self::KeyRotatedOut(_) => "AUTHS-E5858",
313 }
314 }
315
316 fn suggestion(&self) -> Option<&'static str> {
317 match self {
318 Self::IdentityNotFound => {
319 Some("Run `auths init` to create an identity, or `auths key import` to restore one")
320 }
321 Self::KeyResolutionFailed(_) => {
322 Some("Run `auths status` to see available device aliases")
323 }
324 Self::KeyDecryptionFailed(_) => Some("Check your passphrase and try again"),
325 Self::DigestFailed(_) => Some("Verify the file exists and is readable"),
326 Self::AttestationFailed(_) => Some("Check identity storage with `auths status`"),
327 Self::ResignFailed(_) => {
328 Some("Verify your device key is accessible with `auths status`")
329 }
330 Self::InvalidCommitSha(_) => {
331 Some("Provide a full SHA-1 (40 hex chars) or SHA-256 (64 hex chars) commit hash")
332 }
333 Self::DeviceRevoked(_) => {
334 Some("This device has been removed; pair a new device or sign from an active one")
335 }
336 Self::KeyRotatedOut(_) => {
337 Some("This key was rotated out; sign with the identity's current key")
338 }
339 }
340 }
341}
342
343struct SeedMapSigner {
349 seeds: HashMap<String, (SecureSeed, auths_crypto::CurveType)>,
350}
351
352impl SecureSigner for SeedMapSigner {
353 fn sign_with_alias(
354 &self,
355 alias: &auths_core::storage::keychain::KeyAlias,
356 _passphrase_provider: &dyn PassphraseProvider,
357 message: &[u8],
358 ) -> Result<Vec<u8>, auths_core::AgentError> {
359 let (seed, curve) = self
360 .seeds
361 .get(alias.as_str())
362 .ok_or(auths_core::AgentError::KeyNotFound)?;
363 let typed = match curve {
364 auths_crypto::CurveType::Ed25519 => auths_crypto::TypedSeed::Ed25519(*seed.as_bytes()),
365 auths_crypto::CurveType::P256 => auths_crypto::TypedSeed::P256(*seed.as_bytes()),
366 };
367 auths_crypto::typed_sign(&typed, message)
368 .map_err(|e| auths_core::AgentError::CryptoError(e.to_string()))
369 }
370
371 fn sign_for_identity(
372 &self,
373 _identity_did: &IdentityDID,
374 _passphrase_provider: &dyn PassphraseProvider,
375 _message: &[u8],
376 ) -> Result<Vec<u8>, auths_core::AgentError> {
377 Err(auths_core::AgentError::KeyNotFound)
378 }
379}
380
381struct ResolvedKey {
382 alias: KeyAlias,
383 seed: Option<SecureSeed>,
385 public_key_bytes: Vec<u8>,
386 curve: auths_crypto::CurveType,
387 is_hardware: bool,
389 identity_did: Option<IdentityDID>,
393}
394
395fn resolve_optional_key(
396 material: Option<&SigningKeyMaterial>,
397 synthetic_alias: &'static str,
398 keychain: &(dyn KeyStorage + Send + Sync),
399 passphrase_provider: &dyn PassphraseProvider,
400 passphrase_prompt: &str,
401) -> Result<Option<ResolvedKey>, ArtifactSigningError> {
402 match material {
403 None => Ok(None),
404 Some(SigningKeyMaterial::Alias(alias)) => {
405 if keychain.is_hardware_backend() {
409 let (pubkey, curve) = auths_core::storage::keychain::extract_public_key_bytes(
410 keychain,
411 alias,
412 passphrase_provider,
413 )
414 .map_err(|e| ArtifactSigningError::KeyResolutionFailed(e.to_string()))?;
415 return Ok(Some(ResolvedKey {
416 alias: alias.clone(),
417 seed: None,
418 public_key_bytes: pubkey,
419 curve,
420 is_hardware: true,
421 identity_did: None,
422 }));
423 }
424
425 let (device_did, _role, encrypted) = keychain
426 .load_key(alias)
427 .map_err(|e| ArtifactSigningError::KeyResolutionFailed(e.to_string()))?;
428 let passphrase = passphrase_provider
429 .get_passphrase(passphrase_prompt)
430 .map_err(|e| ArtifactSigningError::KeyDecryptionFailed(e.to_string()))?;
431 debug_assert!(
433 !keychain.is_hardware_backend(),
434 "SE keys must never reach decrypt_keypair"
435 );
436 let pkcs8 = core_signer::decrypt_keypair(&encrypted, &passphrase)
437 .map_err(|e| ArtifactSigningError::KeyDecryptionFailed(e.to_string()))?;
438 let (seed, pubkey, curve) = core_signer::load_seed_and_pubkey(&pkcs8)
439 .map_err(|e| ArtifactSigningError::KeyDecryptionFailed(e.to_string()))?;
440 Ok(Some(ResolvedKey {
441 alias: alias.clone(),
442 seed: Some(seed),
443 public_key_bytes: pubkey.to_vec(),
444 curve,
445 is_hardware: false,
446 identity_did: Some(device_did),
447 }))
448 }
449 Some(SigningKeyMaterial::Direct(seed)) => {
450 let typed = auths_crypto::TypedSeed::Ed25519(*seed.as_bytes());
451 let pubkey = auths_crypto::typed_public_key(&typed)
452 .map_err(|e| ArtifactSigningError::KeyDecryptionFailed(e.to_string()))?;
453 Ok(Some(ResolvedKey {
454 alias: KeyAlias::new_unchecked(synthetic_alias),
455 seed: Some(SecureSeed::new(*seed.as_bytes())),
456 public_key_bytes: pubkey,
457 curve: auths_crypto::CurveType::Ed25519,
458 is_hardware: false,
459 identity_did: None,
460 }))
461 }
462 }
463}
464
465fn resolve_required_key(
466 material: &SigningKeyMaterial,
467 synthetic_alias: &'static str,
468 keychain: &(dyn KeyStorage + Send + Sync),
469 passphrase_provider: &dyn PassphraseProvider,
470 passphrase_prompt: &str,
471) -> Result<ResolvedKey, ArtifactSigningError> {
472 resolve_optional_key(
473 Some(material),
474 synthetic_alias,
475 keychain,
476 passphrase_provider,
477 passphrase_prompt,
478 )
479 .map(|opt| {
480 opt.ok_or(ArtifactSigningError::KeyDecryptionFailed(
481 "expected key material but got None".into(),
482 ))
483 })?
484}
485
486fn resolve_root_issuer_key(
501 controller_did: &IdentityDID,
502 keychain: &(dyn KeyStorage + Send + Sync),
503 passphrase_provider: &dyn PassphraseProvider,
504) -> Result<ResolvedKey, ArtifactSigningError> {
505 let alias = keychain
506 .list_aliases_for_identity(controller_did)
507 .map_err(|e| ArtifactSigningError::KeyResolutionFailed(e.to_string()))?
508 .into_iter()
509 .find(|a| !a.as_str().contains("--next-"))
510 .ok_or_else(|| {
511 ArtifactSigningError::KeyResolutionFailed(format!(
512 "no signing key found for root identity {}",
513 controller_did.as_str()
514 ))
515 })?;
516 resolve_required_key(
517 &SigningKeyMaterial::Alias(alias),
518 "__artifact_issuer__",
519 keychain,
520 passphrase_provider,
521 "Enter passphrase for identity key:",
522 )
523}
524
525fn enforce_signer_authority(
544 ctx: &AuthsContext,
545 controller_did: &IdentityDID,
546 device_did: &IdentityDID,
547 device_pk_bytes: &[u8],
548) -> Result<(), ArtifactSigningError> {
549 use std::ops::ControlFlow;
550
551 let device_prefix = auths_id::keri::parse_did_keri(device_did.as_str())
552 .map_err(|e| ArtifactSigningError::KeyResolutionFailed(e.to_string()))?;
553 let root_prefix = auths_id::keri::parse_did_keri(controller_did.as_str())
554 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
555
556 let mut revoked = false;
559 ctx.registry
560 .visit_events(&root_prefix, 0, &mut |event| {
561 let hit = event.anchors().iter().any(|seal| {
562 matches!(seal, auths_id::keri::Seal::Digest { d } if d.as_str() == device_prefix.as_str())
563 });
564 if hit {
565 revoked = true;
566 ControlFlow::Break(())
567 } else {
568 ControlFlow::Continue(())
569 }
570 })
571 .map_err(|e| {
572 ArtifactSigningError::KeyResolutionFailed(format!(
573 "cannot read root KEL to check revocation: {e}"
574 ))
575 })?;
576 if revoked {
577 return Err(ArtifactSigningError::DeviceRevoked(
578 device_did.as_str().to_string(),
579 ));
580 }
581
582 let state = ctx.registry.get_key_state(&device_prefix).map_err(|e| {
585 ArtifactSigningError::KeyResolutionFailed(format!(
586 "cannot read KEL state for {} to verify the signing key is current: {e}",
587 device_did.as_str()
588 ))
589 })?;
590 let is_current = state.current_keys.iter().any(|k| match k.parse() {
591 Ok(pk) => {
594 let key_bytes: &[u8] = pk.as_bytes();
595 key_bytes == device_pk_bytes
596 }
597 Err(_) => false,
598 });
599 if !is_current {
600 return Err(ArtifactSigningError::KeyRotatedOut(
601 device_did.as_str().to_string(),
602 ));
603 }
604 Ok(())
605}
606
607pub fn validate_commit_sha(sha: &str) -> Result<String, ArtifactSigningError> {
617 let normalized = sha.to_ascii_lowercase();
618 let len = normalized.len();
619 if (len != 40 && len != 64) || !normalized.bytes().all(|b| b.is_ascii_hexdigit()) {
620 return Err(ArtifactSigningError::InvalidCommitSha(sha.to_string()));
621 }
622 Ok(normalized)
623}
624
625#[allow(clippy::too_many_lines)]
652pub fn sign_artifact(
653 params: ArtifactSigningParams,
654 ctx: &AuthsContext,
655) -> Result<ArtifactSigningResult, ArtifactSigningError> {
656 let managed = ctx
657 .identity_storage
658 .load_identity()
659 .map_err(|_| ArtifactSigningError::IdentityNotFound)?;
660
661 let keychain = ctx.key_storage.as_ref();
662 let passphrase_provider = ctx.passphrase_provider.as_ref();
663
664 let issuer_did = CanonicalDid::from(managed.controller_did.clone());
670 let identity_resolved = match params.identity_key.as_ref() {
671 Some(explicit) => resolve_optional_key(
672 Some(explicit),
673 "__artifact_identity__",
674 keychain,
675 passphrase_provider,
676 "Enter passphrase for identity key:",
677 )?,
678 None => Some(resolve_root_issuer_key(
679 &managed.controller_did,
680 keychain,
681 passphrase_provider,
682 )?),
683 };
684
685 let device_resolved = resolve_required_key(
686 ¶ms.device_key,
687 "__artifact_device__",
688 keychain,
689 passphrase_provider,
690 "Enter passphrase for device key:",
691 )?;
692
693 if let Some(ref device_did) = device_resolved.identity_did {
694 enforce_signer_authority(
695 ctx,
696 &managed.controller_did,
697 device_did,
698 &device_resolved.public_key_bytes,
699 )?;
700 }
701
702 let mut seeds: HashMap<String, (SecureSeed, auths_crypto::CurveType)> = HashMap::new();
703 let identity_alias: Option<KeyAlias> = identity_resolved.map(|r| {
704 let alias = r.alias.clone();
705 let curve = r.curve;
706 if let Some(seed) = r.seed {
707 seeds.insert(r.alias.into_inner(), (seed, curve));
708 }
709 alias
710 });
711 let device_alias = device_resolved.alias.clone();
712 let device_is_hardware = device_resolved.is_hardware;
713 let device_curve = device_resolved.curve;
714 if let Some(seed) = device_resolved.seed {
715 seeds.insert(device_resolved.alias.into_inner(), (seed, device_curve));
716 }
717 let device_pk_bytes = device_resolved.public_key_bytes;
718
719 let device_did = device_resolved
724 .identity_did
725 .clone()
726 .map(CanonicalDid::from)
727 .unwrap_or_else(|| {
728 CanonicalDid::from_public_key_did_key(&device_pk_bytes, device_resolved.curve)
729 });
730
731 let artifact_meta = params
732 .artifact
733 .metadata()
734 .map_err(|e| ArtifactSigningError::DigestFailed(e.to_string()))?;
735
736 let rid = ResourceId::new(format!("sha256:{}", artifact_meta.digest.hex));
737 let now = ctx.clock.now();
738 let meta = AttestationMetadata {
739 timestamp: Some(now),
740 expires_at: params
741 .expires_in
742 .map(|s| now + chrono::Duration::seconds(s as i64)),
743 note: params.note,
744 };
745
746 let payload = serde_json::to_value(&artifact_meta)
747 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
748
749 let validated_commit_sha = params
750 .commit_sha
751 .map(|sha| validate_commit_sha(&sha))
752 .transpose()?;
753
754 let attestation_json = create_and_sign_attestation(
755 ctx,
756 seeds,
757 device_is_hardware,
758 now,
759 &rid,
760 &issuer_did,
764 &device_did,
765 &device_pk_bytes,
766 device_curve,
767 payload,
768 &meta,
769 identity_alias.as_ref(),
770 &device_alias,
771 validated_commit_sha,
772 )?;
773
774 if let Some(ref alias) = identity_alias {
778 anchor_artifact_attestation(ctx, &managed.controller_did, alias, &attestation_json, now)?;
779 }
780
781 Ok(ArtifactSigningResult {
782 attestation_json,
783 rid,
784 digest: artifact_meta.digest.hex,
785 dsse_signature: None,
786 })
787}
788
789fn anchor_artifact_attestation(
790 ctx: &AuthsContext,
791 controller_did: &auths_core::storage::keychain::IdentityDID,
792 alias: &KeyAlias,
793 attestation_json: &str,
794 now: DateTime<Utc>,
795) -> Result<(), ArtifactSigningError> {
796 let prefix = auths_id::keri::parse_did_keri(controller_did.as_str())
797 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
798 let att: auths_verifier::core::Attestation = serde_json::from_str(attestation_json)
799 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
800 let storage_signer = auths_core::signing::StorageSigner::new(Arc::clone(&ctx.key_storage));
801 let mut batch = auths_id::storage::registry::backend::AtomicWriteBatch::new();
802 batch.stage_attestation(att.clone());
803 auths_id::keri::anchor_and_persist_via_backend(
804 ctx.registry.as_ref(),
805 &storage_signer,
806 alias,
807 ctx.passphrase_provider.as_ref(),
808 &prefix,
809 &att,
810 &mut batch,
811 &ctx.witness_params(),
812 now,
813 )
814 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
815 Ok(())
816}
817
818#[allow(clippy::too_many_arguments)]
820fn create_and_sign_attestation(
821 ctx: &AuthsContext,
822 seeds: HashMap<String, (SecureSeed, auths_crypto::CurveType)>,
823 device_is_hardware: bool,
824 now: DateTime<Utc>,
825 rid: &ResourceId,
826 issuer: &CanonicalDid,
827 subject: &CanonicalDid,
828 device_pk_bytes: &[u8],
829 device_curve: auths_crypto::CurveType,
830 payload: serde_json::Value,
831 meta: &AttestationMetadata,
832 identity_alias: Option<&KeyAlias>,
833 device_alias: &KeyAlias,
834 commit_sha: Option<String>,
835) -> Result<String, ArtifactSigningError> {
836 let seed_signer = SeedMapSigner { seeds };
837 let storage_signer = auths_core::signing::StorageSigner::new(Arc::clone(&ctx.key_storage));
838 let signer: &dyn SecureSigner = if device_is_hardware {
839 &storage_signer
840 } else {
841 &seed_signer
842 };
843 let noop_provider = auths_core::PrefilledPassphraseProvider::new("");
844
845 let issuer_canonical = issuer.clone();
846 let mut attestation = create_signed_attestation(
847 now,
848 auths_id::attestation::create::AttestationInput {
849 rid: rid.as_str(),
850 issuer: &issuer_canonical,
851 subject,
852 device_public_key: device_pk_bytes,
853 device_curve,
854 payload: Some(payload),
855 meta,
856 identity_alias,
857 device_alias: Some(device_alias),
858 delegated_by: None,
859 commit_sha,
860 signer_type: None,
861 oidc_binding: None,
862 },
863 signer,
864 &noop_provider,
865 )
866 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
867
868 resign_attestation(
869 &mut attestation,
870 signer,
871 &noop_provider,
872 identity_alias,
873 device_alias,
874 )
875 .map_err(|e| ArtifactSigningError::ResignFailed(e.to_string()))?;
876
877 serde_json::to_string_pretty(&attestation)
878 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))
879}
880
881pub struct EphemeralSignRequest<'a> {
886 pub data: &'a [u8],
888 pub artifact_name: Option<String>,
890 pub commit_sha: String,
892 pub curve: auths_crypto::CurveType,
897 pub expires_in: Option<u64>,
899 pub note: Option<String>,
901 pub ci_env: Option<serde_json::Value>,
903 pub oidc_binding: Option<OidcBinding>,
906}
907
908pub fn sign_artifact_ephemeral(
936 now: DateTime<Utc>,
937 req: EphemeralSignRequest<'_>,
938) -> Result<ArtifactSigningResult, ArtifactSigningError> {
939 let EphemeralSignRequest {
940 data,
941 artifact_name,
942 commit_sha,
943 curve,
944 expires_in,
945 note,
946 ci_env,
947 oidc_binding,
948 } = req;
949 let mut seed_bytes = Zeroizing::new([0u8; 32]);
951 ring::rand::SecureRandom::fill(&ring::rand::SystemRandom::new(), seed_bytes.as_mut())
952 .map_err(|_| ArtifactSigningError::AttestationFailed("RNG failure".into()))?;
953
954 let typed_seed = auths_crypto::TypedSeed::from_curve(curve, *seed_bytes);
956 let pubkey_vec = auths_crypto::typed_public_key(&typed_seed)
957 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
958
959 let device_did = CanonicalDid::from_public_key_did_key(&pubkey_vec, curve);
962
963 let digest_hex = hex::encode(Sha256::digest(data));
965 let artifact_meta = ArtifactMetadata {
966 artifact_type: "file".to_string(),
967 digest: ArtifactDigest {
968 algorithm: "sha256".to_string(),
969 hex: digest_hex,
970 },
971 name: artifact_name,
972 size: Some(data.len() as u64),
973 };
974
975 let mut payload_value = serde_json::to_value(&artifact_meta)
976 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
977
978 if let Some(env) = ci_env
979 && let serde_json::Value::Object(ref mut map) = payload_value
980 {
981 map.insert("ci_environment".to_string(), env);
982 }
983
984 let rid = ResourceId::new(format!("sha256:{}", artifact_meta.digest.hex));
985 let meta = AttestationMetadata {
986 timestamp: Some(now),
987 expires_at: expires_in.map(|s| now + chrono::Duration::seconds(s as i64)),
988 note,
989 };
990
991 let validated_sha = validate_commit_sha(&commit_sha)?;
993
994 let identity_alias = KeyAlias::new_unchecked("__ephemeral_identity__");
996 let device_alias = KeyAlias::new_unchecked("__ephemeral_device__");
997
998 let mut seeds: HashMap<String, (SecureSeed, auths_crypto::CurveType)> = HashMap::new();
999 seeds.insert(
1000 identity_alias.as_str().to_string(),
1001 (SecureSeed::new(*seed_bytes), curve),
1002 );
1003 seeds.insert(
1004 device_alias.as_str().to_string(),
1005 (SecureSeed::new(*seed_bytes), curve),
1006 );
1007 let signer = SeedMapSigner { seeds };
1008 let noop_provider = auths_core::PrefilledPassphraseProvider::new("");
1009
1010 let attestation = create_signed_attestation(
1012 now,
1013 auths_id::attestation::create::AttestationInput {
1014 rid: rid.as_str(),
1015 issuer: &device_did,
1016 subject: &device_did,
1017 device_public_key: &pubkey_vec,
1018 device_curve: curve,
1019 payload: Some(payload_value),
1020 meta: &meta,
1021 identity_alias: Some(&identity_alias),
1022 device_alias: Some(&device_alias),
1023 delegated_by: None,
1024 commit_sha: Some(validated_sha),
1025 signer_type: Some(SignerType::Workload),
1026 oidc_binding,
1027 },
1028 &signer,
1029 &noop_provider,
1030 )
1031 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
1032
1033 let attestation_json = serde_json::to_string_pretty(&attestation)
1034 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
1035
1036 let pae = dsse_pae("application/vnd.auths+json", attestation_json.as_bytes());
1038 let dsse_sig = auths_crypto::typed_sign(&typed_seed, &pae)
1039 .map_err(|e| ArtifactSigningError::AttestationFailed(format!("DSSE sign: {e}")))?;
1040
1041 Ok(ArtifactSigningResult {
1042 attestation_json,
1043 rid,
1044 digest: artifact_meta.digest.hex,
1045 dsse_signature: Some(dsse_sig),
1046 })
1047}
1048
1049pub fn sign_artifact_raw(
1070 now: DateTime<Utc>,
1071 seed: &SecureSeed,
1072 identity_did: &IdentityDID,
1073 data: &[u8],
1074 expires_in: Option<u64>,
1075 note: Option<String>,
1076 commit_sha: Option<String>,
1077) -> Result<ArtifactSigningResult, ArtifactSigningError> {
1078 let curve = auths_crypto::CurveType::default();
1080 let typed = auths_crypto::TypedSeed::from_curve(curve, *seed.as_bytes());
1081 let pubkey = auths_crypto::typed_public_key(&typed)
1082 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
1083
1084 let device_did = CanonicalDid::from_public_key_did_key(&pubkey, curve);
1085
1086 let digest_hex = hex::encode(Sha256::digest(data));
1087 let artifact_meta = ArtifactMetadata {
1088 artifact_type: "bytes".to_string(),
1089 digest: ArtifactDigest {
1090 algorithm: "sha256".to_string(),
1091 hex: digest_hex,
1092 },
1093 name: None,
1094 size: Some(data.len() as u64),
1095 };
1096
1097 let rid = ResourceId::new(format!("sha256:{}", artifact_meta.digest.hex));
1098 let meta = AttestationMetadata {
1099 timestamp: Some(now),
1100 expires_at: expires_in.map(|s| now + chrono::Duration::seconds(s as i64)),
1101 note,
1102 };
1103
1104 let payload = serde_json::to_value(&artifact_meta)
1105 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
1106
1107 let identity_alias = KeyAlias::new_unchecked("__raw_identity__");
1108 let device_alias = KeyAlias::new_unchecked("__raw_device__");
1109
1110 let mut seeds: HashMap<String, (SecureSeed, auths_crypto::CurveType)> = HashMap::new();
1111 seeds.insert(
1112 identity_alias.as_str().to_string(),
1113 (SecureSeed::new(*seed.as_bytes()), curve),
1114 );
1115 seeds.insert(
1116 device_alias.as_str().to_string(),
1117 (SecureSeed::new(*seed.as_bytes()), curve),
1118 );
1119 let signer = SeedMapSigner { seeds };
1120 let noop_provider = auths_core::PrefilledPassphraseProvider::new("");
1122
1123 let validated_commit_sha = commit_sha
1124 .map(|sha| validate_commit_sha(&sha))
1125 .transpose()?;
1126
1127 let issuer_canonical = CanonicalDid::from(identity_did.clone());
1128 let attestation = create_signed_attestation(
1129 now,
1130 auths_id::attestation::create::AttestationInput {
1131 rid: rid.as_str(),
1132 issuer: &issuer_canonical,
1133 subject: &device_did,
1134 device_public_key: &pubkey,
1135 device_curve: curve,
1136 payload: Some(payload),
1137 meta: &meta,
1138 identity_alias: Some(&identity_alias),
1139 device_alias: Some(&device_alias),
1140 delegated_by: None,
1141 commit_sha: validated_commit_sha,
1142 signer_type: None,
1143 oidc_binding: None,
1144 },
1145 &signer,
1146 &noop_provider,
1147 )
1148 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
1149
1150 let attestation_json = serde_json::to_string_pretty(&attestation)
1151 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
1152
1153 Ok(ArtifactSigningResult {
1154 attestation_json,
1155 rid,
1156 digest: artifact_meta.digest.hex,
1157 dsse_signature: None,
1158 })
1159}
1160
1161pub fn dsse_pae(payload_type: &str, payload: &[u8]) -> Vec<u8> {
1166 let header = format!(
1167 "DSSEv1 {} {} {} ",
1168 payload_type.len(),
1169 payload_type,
1170 payload.len()
1171 );
1172 let mut result = Vec::with_capacity(header.len() + payload.len());
1173 result.extend_from_slice(header.as_bytes());
1174 result.extend_from_slice(payload);
1175 result
1176}