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::{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
283impl auths_core::error::AuthsErrorInfo for ArtifactSigningError {
284 fn error_code(&self) -> &'static str {
285 match self {
286 Self::IdentityNotFound => "AUTHS-E5850",
287 Self::KeyResolutionFailed(_) => "AUTHS-E5851",
288 Self::KeyDecryptionFailed(_) => "AUTHS-E5852",
289 Self::DigestFailed(_) => "AUTHS-E5853",
290 Self::AttestationFailed(_) => "AUTHS-E5854",
291 Self::ResignFailed(_) => "AUTHS-E5855",
292 Self::InvalidCommitSha(_) => "AUTHS-E5856",
293 }
294 }
295
296 fn suggestion(&self) -> Option<&'static str> {
297 match self {
298 Self::IdentityNotFound => {
299 Some("Run `auths init` to create an identity, or `auths key import` to restore one")
300 }
301 Self::KeyResolutionFailed(_) => {
302 Some("Run `auths status` to see available device aliases")
303 }
304 Self::KeyDecryptionFailed(_) => Some("Check your passphrase and try again"),
305 Self::DigestFailed(_) => Some("Verify the file exists and is readable"),
306 Self::AttestationFailed(_) => Some("Check identity storage with `auths status`"),
307 Self::ResignFailed(_) => {
308 Some("Verify your device key is accessible with `auths status`")
309 }
310 Self::InvalidCommitSha(_) => {
311 Some("Provide a full SHA-1 (40 hex chars) or SHA-256 (64 hex chars) commit hash")
312 }
313 }
314 }
315}
316
317struct SeedMapSigner {
323 seeds: HashMap<String, (SecureSeed, auths_crypto::CurveType)>,
324}
325
326impl SecureSigner for SeedMapSigner {
327 fn sign_with_alias(
328 &self,
329 alias: &auths_core::storage::keychain::KeyAlias,
330 _passphrase_provider: &dyn PassphraseProvider,
331 message: &[u8],
332 ) -> Result<Vec<u8>, auths_core::AgentError> {
333 let (seed, curve) = self
334 .seeds
335 .get(alias.as_str())
336 .ok_or(auths_core::AgentError::KeyNotFound)?;
337 let typed = match curve {
338 auths_crypto::CurveType::Ed25519 => auths_crypto::TypedSeed::Ed25519(*seed.as_bytes()),
339 auths_crypto::CurveType::P256 => auths_crypto::TypedSeed::P256(*seed.as_bytes()),
340 };
341 auths_crypto::typed_sign(&typed, message)
342 .map_err(|e| auths_core::AgentError::CryptoError(e.to_string()))
343 }
344
345 fn sign_for_identity(
346 &self,
347 _identity_did: &IdentityDID,
348 _passphrase_provider: &dyn PassphraseProvider,
349 _message: &[u8],
350 ) -> Result<Vec<u8>, auths_core::AgentError> {
351 Err(auths_core::AgentError::KeyNotFound)
352 }
353}
354
355struct ResolvedKey {
356 alias: KeyAlias,
357 seed: Option<SecureSeed>,
359 public_key_bytes: Vec<u8>,
360 curve: auths_crypto::CurveType,
361 is_hardware: bool,
363}
364
365fn resolve_optional_key(
366 material: Option<&SigningKeyMaterial>,
367 synthetic_alias: &'static str,
368 keychain: &(dyn KeyStorage + Send + Sync),
369 passphrase_provider: &dyn PassphraseProvider,
370 passphrase_prompt: &str,
371) -> Result<Option<ResolvedKey>, ArtifactSigningError> {
372 match material {
373 None => Ok(None),
374 Some(SigningKeyMaterial::Alias(alias)) => {
375 if keychain.is_hardware_backend() {
379 let (pubkey, curve) = auths_core::storage::keychain::extract_public_key_bytes(
380 keychain,
381 alias,
382 passphrase_provider,
383 )
384 .map_err(|e| ArtifactSigningError::KeyResolutionFailed(e.to_string()))?;
385 return Ok(Some(ResolvedKey {
386 alias: alias.clone(),
387 seed: None,
388 public_key_bytes: pubkey,
389 curve,
390 is_hardware: true,
391 }));
392 }
393
394 let (_, _role, encrypted) = keychain
395 .load_key(alias)
396 .map_err(|e| ArtifactSigningError::KeyResolutionFailed(e.to_string()))?;
397 let passphrase = passphrase_provider
398 .get_passphrase(passphrase_prompt)
399 .map_err(|e| ArtifactSigningError::KeyDecryptionFailed(e.to_string()))?;
400 debug_assert!(
402 !keychain.is_hardware_backend(),
403 "SE keys must never reach decrypt_keypair"
404 );
405 let pkcs8 = core_signer::decrypt_keypair(&encrypted, &passphrase)
406 .map_err(|e| ArtifactSigningError::KeyDecryptionFailed(e.to_string()))?;
407 let (seed, pubkey, curve) = core_signer::load_seed_and_pubkey(&pkcs8)
408 .map_err(|e| ArtifactSigningError::KeyDecryptionFailed(e.to_string()))?;
409 Ok(Some(ResolvedKey {
410 alias: alias.clone(),
411 seed: Some(seed),
412 public_key_bytes: pubkey.to_vec(),
413 curve,
414 is_hardware: false,
415 }))
416 }
417 Some(SigningKeyMaterial::Direct(seed)) => {
418 let typed = auths_crypto::TypedSeed::Ed25519(*seed.as_bytes());
419 let pubkey = auths_crypto::typed_public_key(&typed)
420 .map_err(|e| ArtifactSigningError::KeyDecryptionFailed(e.to_string()))?;
421 Ok(Some(ResolvedKey {
422 alias: KeyAlias::new_unchecked(synthetic_alias),
423 seed: Some(SecureSeed::new(*seed.as_bytes())),
424 public_key_bytes: pubkey,
425 curve: auths_crypto::CurveType::Ed25519,
426 is_hardware: false,
427 }))
428 }
429 }
430}
431
432fn resolve_required_key(
433 material: &SigningKeyMaterial,
434 synthetic_alias: &'static str,
435 keychain: &(dyn KeyStorage + Send + Sync),
436 passphrase_provider: &dyn PassphraseProvider,
437 passphrase_prompt: &str,
438) -> Result<ResolvedKey, ArtifactSigningError> {
439 resolve_optional_key(
440 Some(material),
441 synthetic_alias,
442 keychain,
443 passphrase_provider,
444 passphrase_prompt,
445 )
446 .map(|opt| {
447 opt.ok_or(ArtifactSigningError::KeyDecryptionFailed(
448 "expected key material but got None".into(),
449 ))
450 })?
451}
452
453pub fn validate_commit_sha(sha: &str) -> Result<String, ArtifactSigningError> {
463 let normalized = sha.to_ascii_lowercase();
464 let len = normalized.len();
465 if (len != 40 && len != 64) || !normalized.bytes().all(|b| b.is_ascii_hexdigit()) {
466 return Err(ArtifactSigningError::InvalidCommitSha(sha.to_string()));
467 }
468 Ok(normalized)
469}
470
471pub fn sign_artifact(
494 params: ArtifactSigningParams,
495 ctx: &AuthsContext,
496) -> Result<ArtifactSigningResult, ArtifactSigningError> {
497 let managed = ctx
498 .identity_storage
499 .load_identity()
500 .map_err(|_| ArtifactSigningError::IdentityNotFound)?;
501
502 let keychain = ctx.key_storage.as_ref();
503 let passphrase_provider = ctx.passphrase_provider.as_ref();
504
505 let identity_resolved = resolve_optional_key(
506 params.identity_key.as_ref(),
507 "__artifact_identity__",
508 keychain,
509 passphrase_provider,
510 "Enter passphrase for identity key:",
511 )?;
512
513 let device_resolved = resolve_required_key(
514 ¶ms.device_key,
515 "__artifact_device__",
516 keychain,
517 passphrase_provider,
518 "Enter passphrase for device key:",
519 )?;
520
521 let mut seeds: HashMap<String, (SecureSeed, auths_crypto::CurveType)> = HashMap::new();
522 let identity_alias: Option<KeyAlias> = identity_resolved.map(|r| {
523 let alias = r.alias.clone();
524 let curve = r.curve;
525 if let Some(seed) = r.seed {
526 seeds.insert(r.alias.into_inner(), (seed, curve));
527 }
528 alias
529 });
530 let device_alias = device_resolved.alias.clone();
531 let device_is_hardware = device_resolved.is_hardware;
532 let device_curve = device_resolved.curve;
533 if let Some(seed) = device_resolved.seed {
534 seeds.insert(device_resolved.alias.into_inner(), (seed, device_curve));
535 }
536 let device_pk_bytes = device_resolved.public_key_bytes;
537
538 let device_did = CanonicalDid::from_public_key_did_key(&device_pk_bytes, device_resolved.curve);
539
540 let artifact_meta = params
541 .artifact
542 .metadata()
543 .map_err(|e| ArtifactSigningError::DigestFailed(e.to_string()))?;
544
545 let rid = ResourceId::new(format!("sha256:{}", artifact_meta.digest.hex));
546 let now = ctx.clock.now();
547 let meta = AttestationMetadata {
548 timestamp: Some(now),
549 expires_at: params
550 .expires_in
551 .map(|s| now + chrono::Duration::seconds(s as i64)),
552 note: params.note,
553 };
554
555 let payload = serde_json::to_value(&artifact_meta)
556 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
557
558 let validated_commit_sha = params
559 .commit_sha
560 .map(|sha| validate_commit_sha(&sha))
561 .transpose()?;
562
563 let attestation_json = create_and_sign_attestation(
564 ctx,
565 seeds,
566 device_is_hardware,
567 now,
568 &rid,
569 &managed.controller_did,
570 &device_did,
571 &device_pk_bytes,
572 device_curve,
573 payload,
574 &meta,
575 identity_alias.as_ref(),
576 &device_alias,
577 validated_commit_sha,
578 )?;
579
580 if let Some(ref alias) = identity_alias {
581 anchor_artifact_attestation(ctx, &managed.controller_did, alias, &attestation_json, now)?;
582 }
583
584 Ok(ArtifactSigningResult {
585 attestation_json,
586 rid,
587 digest: artifact_meta.digest.hex,
588 dsse_signature: None,
589 })
590}
591
592fn anchor_artifact_attestation(
593 ctx: &AuthsContext,
594 controller_did: &auths_core::storage::keychain::IdentityDID,
595 alias: &KeyAlias,
596 attestation_json: &str,
597 now: DateTime<Utc>,
598) -> Result<(), ArtifactSigningError> {
599 let prefix = auths_id::keri::parse_did_keri(controller_did.as_str())
600 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
601 let att: auths_verifier::core::Attestation = serde_json::from_str(attestation_json)
602 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
603 let storage_signer = auths_core::signing::StorageSigner::new(Arc::clone(&ctx.key_storage));
604 let mut batch = auths_id::storage::registry::backend::AtomicWriteBatch::new();
605 batch.stage_attestation(att.clone());
606 auths_id::keri::anchor_and_persist_via_backend(
607 ctx.registry.as_ref(),
608 &storage_signer,
609 alias,
610 ctx.passphrase_provider.as_ref(),
611 &prefix,
612 &att,
613 &mut batch,
614 &ctx.witness_params(),
615 now,
616 )
617 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
618 Ok(())
619}
620
621#[allow(clippy::too_many_arguments)]
623fn create_and_sign_attestation(
624 ctx: &AuthsContext,
625 seeds: HashMap<String, (SecureSeed, auths_crypto::CurveType)>,
626 device_is_hardware: bool,
627 now: DateTime<Utc>,
628 rid: &ResourceId,
629 controller_did: &IdentityDID,
630 subject: &CanonicalDid,
631 device_pk_bytes: &[u8],
632 device_curve: auths_crypto::CurveType,
633 payload: serde_json::Value,
634 meta: &AttestationMetadata,
635 identity_alias: Option<&KeyAlias>,
636 device_alias: &KeyAlias,
637 commit_sha: Option<String>,
638) -> Result<String, ArtifactSigningError> {
639 let seed_signer = SeedMapSigner { seeds };
640 let storage_signer = auths_core::signing::StorageSigner::new(Arc::clone(&ctx.key_storage));
641 let signer: &dyn SecureSigner = if device_is_hardware {
642 &storage_signer
643 } else {
644 &seed_signer
645 };
646 let noop_provider = auths_core::PrefilledPassphraseProvider::new("");
647
648 let mut attestation = create_signed_attestation(
649 now,
650 auths_id::attestation::create::AttestationInput {
651 rid: rid.as_str(),
652 identity_did: controller_did,
653 subject,
654 device_public_key: device_pk_bytes,
655 device_curve,
656 payload: Some(payload),
657 meta,
658 identity_alias,
659 device_alias: Some(device_alias),
660 delegated_by: None,
661 commit_sha,
662 signer_type: None,
663 },
664 signer,
665 &noop_provider,
666 )
667 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
668
669 resign_attestation(
670 &mut attestation,
671 signer,
672 &noop_provider,
673 identity_alias,
674 device_alias,
675 )
676 .map_err(|e| ArtifactSigningError::ResignFailed(e.to_string()))?;
677
678 serde_json::to_string_pretty(&attestation)
679 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))
680}
681
682pub fn sign_artifact_ephemeral(
708 now: DateTime<Utc>,
709 data: &[u8],
710 artifact_name: Option<String>,
711 commit_sha: String,
712 expires_in: Option<u64>,
713 note: Option<String>,
714 ci_env: Option<serde_json::Value>,
715) -> Result<ArtifactSigningResult, ArtifactSigningError> {
716 let mut seed_bytes = Zeroizing::new([0u8; 32]);
718 ring::rand::SecureRandom::fill(&ring::rand::SystemRandom::new(), seed_bytes.as_mut())
719 .map_err(|_| ArtifactSigningError::AttestationFailed("RNG failure".into()))?;
720
721 let typed_seed = auths_crypto::TypedSeed::P256(*seed_bytes);
723 let pubkey_vec = auths_crypto::typed_public_key(&typed_seed)
724 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
725
726 let device_did =
727 CanonicalDid::from_public_key_did_key(&pubkey_vec, auths_crypto::CurveType::P256);
728 #[allow(clippy::disallowed_methods)]
729 let identity_did = IdentityDID::new_unchecked(device_did.as_str());
730
731 let digest_hex = hex::encode(Sha256::digest(data));
733 let artifact_meta = ArtifactMetadata {
734 artifact_type: "file".to_string(),
735 digest: ArtifactDigest {
736 algorithm: "sha256".to_string(),
737 hex: digest_hex,
738 },
739 name: artifact_name,
740 size: Some(data.len() as u64),
741 };
742
743 let mut payload_value = serde_json::to_value(&artifact_meta)
744 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
745
746 if let Some(env) = ci_env
747 && let serde_json::Value::Object(ref mut map) = payload_value
748 {
749 map.insert("ci_environment".to_string(), env);
750 }
751
752 let rid = ResourceId::new(format!("sha256:{}", artifact_meta.digest.hex));
753 let meta = AttestationMetadata {
754 timestamp: Some(now),
755 expires_at: expires_in.map(|s| now + chrono::Duration::seconds(s as i64)),
756 note,
757 };
758
759 let validated_sha = validate_commit_sha(&commit_sha)?;
761
762 let identity_alias = KeyAlias::new_unchecked("__ephemeral_identity__");
764 let device_alias = KeyAlias::new_unchecked("__ephemeral_device__");
765
766 let mut seeds: HashMap<String, (SecureSeed, auths_crypto::CurveType)> = HashMap::new();
767 seeds.insert(
768 identity_alias.as_str().to_string(),
769 (SecureSeed::new(*seed_bytes), auths_crypto::CurveType::P256),
770 );
771 seeds.insert(
772 device_alias.as_str().to_string(),
773 (SecureSeed::new(*seed_bytes), auths_crypto::CurveType::P256),
774 );
775 let signer = SeedMapSigner { seeds };
776 let noop_provider = auths_core::PrefilledPassphraseProvider::new("");
777
778 let attestation = create_signed_attestation(
780 now,
781 auths_id::attestation::create::AttestationInput {
782 rid: rid.as_str(),
783 identity_did: &identity_did,
784 subject: &device_did,
785 device_public_key: &pubkey_vec,
786 device_curve: auths_crypto::CurveType::P256,
787 payload: Some(payload_value),
788 meta: &meta,
789 identity_alias: Some(&identity_alias),
790 device_alias: Some(&device_alias),
791 delegated_by: None,
792 commit_sha: Some(validated_sha),
793 signer_type: Some(SignerType::Workload),
794 },
795 &signer,
796 &noop_provider,
797 )
798 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
799
800 let attestation_json = serde_json::to_string_pretty(&attestation)
801 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
802
803 let pae = dsse_pae("application/vnd.auths+json", attestation_json.as_bytes());
805 let dsse_sig = auths_crypto::typed_sign(&typed_seed, &pae)
806 .map_err(|e| ArtifactSigningError::AttestationFailed(format!("DSSE sign: {e}")))?;
807
808 Ok(ArtifactSigningResult {
809 attestation_json,
810 rid,
811 digest: artifact_meta.digest.hex,
812 dsse_signature: Some(dsse_sig),
813 })
814}
815
816pub fn sign_artifact_raw(
837 now: DateTime<Utc>,
838 seed: &SecureSeed,
839 identity_did: &IdentityDID,
840 data: &[u8],
841 expires_in: Option<u64>,
842 note: Option<String>,
843 commit_sha: Option<String>,
844) -> Result<ArtifactSigningResult, ArtifactSigningError> {
845 let curve = auths_crypto::CurveType::default();
847 let typed = match curve {
848 auths_crypto::CurveType::Ed25519 => auths_crypto::TypedSeed::Ed25519(*seed.as_bytes()),
849 auths_crypto::CurveType::P256 => auths_crypto::TypedSeed::P256(*seed.as_bytes()),
850 };
851 let pubkey = auths_crypto::typed_public_key(&typed)
852 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
853
854 let device_did = CanonicalDid::from_public_key_did_key(&pubkey, curve);
855
856 let digest_hex = hex::encode(Sha256::digest(data));
857 let artifact_meta = ArtifactMetadata {
858 artifact_type: "bytes".to_string(),
859 digest: ArtifactDigest {
860 algorithm: "sha256".to_string(),
861 hex: digest_hex,
862 },
863 name: None,
864 size: Some(data.len() as u64),
865 };
866
867 let rid = ResourceId::new(format!("sha256:{}", artifact_meta.digest.hex));
868 let meta = AttestationMetadata {
869 timestamp: Some(now),
870 expires_at: expires_in.map(|s| now + chrono::Duration::seconds(s as i64)),
871 note,
872 };
873
874 let payload = serde_json::to_value(&artifact_meta)
875 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
876
877 let identity_alias = KeyAlias::new_unchecked("__raw_identity__");
878 let device_alias = KeyAlias::new_unchecked("__raw_device__");
879
880 let mut seeds: HashMap<String, (SecureSeed, auths_crypto::CurveType)> = HashMap::new();
881 seeds.insert(
882 identity_alias.as_str().to_string(),
883 (SecureSeed::new(*seed.as_bytes()), curve),
884 );
885 seeds.insert(
886 device_alias.as_str().to_string(),
887 (SecureSeed::new(*seed.as_bytes()), curve),
888 );
889 let signer = SeedMapSigner { seeds };
890 let noop_provider = auths_core::PrefilledPassphraseProvider::new("");
892
893 let validated_commit_sha = commit_sha
894 .map(|sha| validate_commit_sha(&sha))
895 .transpose()?;
896
897 let attestation = create_signed_attestation(
898 now,
899 auths_id::attestation::create::AttestationInput {
900 rid: rid.as_str(),
901 identity_did,
902 subject: &device_did,
903 device_public_key: &pubkey,
904 device_curve: curve,
905 payload: Some(payload),
906 meta: &meta,
907 identity_alias: Some(&identity_alias),
908 device_alias: Some(&device_alias),
909 delegated_by: None,
910 commit_sha: validated_commit_sha,
911 signer_type: None,
912 },
913 &signer,
914 &noop_provider,
915 )
916 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
917
918 let attestation_json = serde_json::to_string_pretty(&attestation)
919 .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?;
920
921 Ok(ArtifactSigningResult {
922 attestation_json,
923 rid,
924 digest: artifact_meta.digest.hex,
925 dsse_signature: None,
926 })
927}
928
929pub fn dsse_pae(payload_type: &str, payload: &[u8]) -> Vec<u8> {
934 let header = format!(
935 "DSSEv1 {} {} {} ",
936 payload_type.len(),
937 payload_type,
938 payload.len()
939 );
940 let mut result = Vec::with_capacity(header.len() + payload.len());
941 result.extend_from_slice(header.as_bytes());
942 result.extend_from_slice(payload);
943 result
944}