diff --git a/.cargo/audit.toml b/.cargo/audit.toml index 9290b213..f099158d 100644 --- a/.cargo/audit.toml +++ b/.cargo/audit.toml @@ -25,4 +25,4 @@ ignore = [ [yanked] # uds_windows 1.2.0 is yanked but is a transitive dep of zbus; no direct fix available. -ignore = true +enabled = false diff --git a/crates/auths-cli/src/cli.rs b/crates/auths-cli/src/cli.rs index ecda482c..496d2317 100644 --- a/crates/auths-cli/src/cli.rs +++ b/crates/auths-cli/src/cli.rs @@ -14,6 +14,7 @@ use crate::commands::commit::CommitCmd; use crate::commands::completions::CompletionsCommand; use crate::commands::config::ConfigCommand; use crate::commands::debug::DebugCmd; +use crate::commands::demo::DemoCommand; use crate::commands::device::DeviceCommand; use crate::commands::device::pair::PairCommand; use crate::commands::doctor::DoctorCommand; @@ -28,6 +29,7 @@ use crate::commands::log::LogCommand; use crate::commands::namespace::NamespaceCommand; use crate::commands::org::OrgCommand; use crate::commands::policy::PolicyCommand; +use crate::commands::publish::PublishCommand; use crate::commands::reset::ResetCommand; use crate::commands::scim::ScimCommand; use crate::commands::sign::SignCommand; @@ -87,11 +89,11 @@ pub enum RootCommand { Init(InitCommand), Sign(SignCommand), Verify(UnifiedVerifyCommand), - Artifact(ArtifactCommand), Status(StatusCommand), Whoami(WhoamiCommand), // ── Setup & Troubleshooting ── + Demo(DemoCommand), Pair(PairCommand), Trust(TrustCommand), Doctor(DoctorCommand), @@ -106,6 +108,10 @@ pub enum RootCommand { // ── Advanced (visible via --help-all) ── #[command(hide = true)] + Publish(PublishCommand), + #[command(hide = true)] + Artifact(ArtifactCommand), + #[command(hide = true)] Reset(ResetCommand), #[command(hide = true)] SignCommit(SignCommitCommand), diff --git a/crates/auths-cli/src/commands/demo.rs b/crates/auths-cli/src/commands/demo.rs new file mode 100644 index 00000000..7aa0b9da --- /dev/null +++ b/crates/auths-cli/src/commands/demo.rs @@ -0,0 +1,96 @@ +use std::io::Write as _; +use std::sync::Arc; +use std::time::Instant; + +use anyhow::{Context, Result, anyhow}; +use serde_json::Value; +use tempfile::NamedTempFile; + +use auths_sdk::domains::signing::service::{ + ArtifactSigningParams, SigningKeyMaterial, sign_artifact, +}; +use auths_sdk::keychain::KeyAlias; + +use crate::commands::artifact::file::FileArtifact; +use crate::commands::executable::ExecutableCommand; +use crate::commands::key_detect::auto_detect_device_key; +use crate::config::CliConfig; +use crate::factories::storage::build_auths_context; +use crate::ux::format::Output; + +#[derive(Debug, clap::Args)] +#[command(about = "Sign and verify a demo artifact — works offline, no registry needed")] +pub struct DemoCommand {} + +impl ExecutableCommand for DemoCommand { + fn execute(&self, ctx: &CliConfig) -> Result<()> { + let out = Output::new(); + + // 1. Create a temp file with known content + let mut tmp = NamedTempFile::new().context("failed to create temp file")?; + writeln!(tmp, "Hello, Auths!").context("failed to write demo content")?; + let path = tmp.path().to_path_buf(); + + // 2. Auto-detect the device key alias (errors out cleanly if identity missing) + let device_key_alias = auto_detect_device_key(ctx.repo_path.as_deref(), &ctx.env_config) + .context("No identity found — run `auths init` first")?; + + // 3. Build SDK context + let repo_path = auths_sdk::storage_layout::resolve_repo_path(ctx.repo_path.clone())?; + let sdk_ctx = build_auths_context( + &repo_path, + &ctx.env_config, + Some(ctx.passphrase_provider.clone()), + )?; + + // 4. Sign using SDK directly (no intermediate CLI output) + let t_sign = Instant::now(); + let sign_result = sign_artifact( + ArtifactSigningParams { + artifact: Arc::new(FileArtifact::new(&path)), + identity_key: None, + device_key: SigningKeyMaterial::Alias(KeyAlias::new_unchecked(&device_key_alias)), + expires_in: None, + note: Some("auths demo — local only".into()), + commit_sha: None, + }, + &sdk_ctx, + ) + .map_err(|e| anyhow!("{}", e))?; + let sign_ms = t_sign.elapsed().as_millis(); + + // 5. Verify: parse attestation and confirm digest integrity (fully local) + let t_verify = Instant::now(); + let attestation: Value = serde_json::from_str(&sign_result.attestation_json) + .context("failed to parse attestation")?; + let stored_digest = attestation + .pointer("/payload/digest/hex") + .and_then(|v| v.as_str()) + .context("attestation missing payload digest")?; + if stored_digest != sign_result.digest { + anyhow::bail!( + "demo verification failed: digest mismatch\n expected: {}\n got: {}", + sign_result.digest, + stored_digest + ); + } + let verify_ms = t_verify.elapsed().as_millis(); + + // 6. Extract issuer DID from the attestation + let issuer = attestation + .pointer("/issuer") + .and_then(|v| v.as_str()) + .unwrap_or("(unknown)"); + + // 7. Print result banner + out.print_heading("Auths Demo"); + out.println(""); + out.key_value("Your identity", issuer); + out.key_value("Signed in ", &format!("{}ms", sign_ms)); + out.key_value("Verified in ", &format!("{}ms", verify_ms)); + out.println(""); + out.print_success("No network required."); + + Ok(()) + } +} diff --git a/crates/auths-cli/src/commands/init/display.rs b/crates/auths-cli/src/commands/init/display.rs index 0c8595f3..09090578 100644 --- a/crates/auths-cli/src/commands/init/display.rs +++ b/crates/auths-cli/src/commands/init/display.rs @@ -28,8 +28,12 @@ pub(crate) fn display_developer_result( )); } out.newline(); + out.key_value("Next step ", "auths sign or auths git setup"); + out.key_value("Share identity", "auths export --bundle | pbcopy"); + out.newline(); out.print_success("Your next commit will be signed with Auths!"); out.println(" Run `auths status` to check your identity"); + out.println(" Run `auths demo` to test sign + verify right now"); } pub(crate) fn display_ci_result( diff --git a/crates/auths-cli/src/commands/mod.rs b/crates/auths-cli/src/commands/mod.rs index 6ca50ace..26da0545 100644 --- a/crates/auths-cli/src/commands/mod.rs +++ b/crates/auths-cli/src/commands/mod.rs @@ -13,6 +13,7 @@ pub mod commit; pub mod completions; pub mod config; pub mod debug; +pub mod demo; pub mod device; pub mod doctor; pub mod emergency; @@ -30,6 +31,7 @@ pub mod namespace; pub mod org; pub mod policy; pub mod provision; +pub mod publish; pub mod reset; pub mod scim; pub mod sign; diff --git a/crates/auths-cli/src/commands/publish.rs b/crates/auths-cli/src/commands/publish.rs new file mode 100644 index 00000000..ff2cf5c7 --- /dev/null +++ b/crates/auths-cli/src/commands/publish.rs @@ -0,0 +1,73 @@ +use anyhow::Result; +use std::path::PathBuf; + +use crate::commands::artifact::publish::handle_publish; +use crate::commands::executable::ExecutableCommand; +use crate::config::CliConfig; + +/// Top-level `auths publish` command: sign and publish a signed artifact attestation. +#[derive(Debug, clap::Args)] +#[command( + about = "Publish a signed artifact attestation to the Auths registry.", + after_help = "Examples: + auths publish package.tar.gz # Sign and publish + auths publish --signature package.tar.gz.auths.json # Publish existing signature + auths publish package.tar.gz --package npm:react@18.3.0 + +Related: + auths sign — Sign an artifact without publishing + auths verify — Verify a signed artifact" +)] +pub struct PublishCommand { + /// Artifact file to sign and publish. Omit if providing --signature directly. + #[arg(help = "Artifact file to sign and publish.")] + pub file: Option, + + /// Path to an existing .auths.json signature file. Defaults to .auths.json. + #[arg(long, value_name = "PATH")] + pub signature: Option, + + /// Package identifier for registry indexing (e.g., npm:react@18.3.0). + #[arg(long)] + pub package: Option, + + /// Registry URL to publish to. + #[arg(long, default_value = "https://auths-registry.fly.dev")] + pub registry: String, +} + +impl ExecutableCommand for PublishCommand { + fn execute(&self, ctx: &CliConfig) -> Result<()> { + let sig_path = match (&self.signature, &self.file) { + (Some(sig), _) => sig.clone(), + (None, Some(file)) => { + let mut p = file.clone(); + p.set_file_name(format!( + "{}.auths.json", + p.file_name().unwrap_or_default().to_string_lossy() + )); + if !p.exists() { + crate::commands::artifact::sign::handle_sign( + file, + None, + None, + &crate::commands::key_detect::auto_detect_device_key( + ctx.repo_path.as_deref(), + &ctx.env_config, + )?, + None, + None, + crate::commands::git_helpers::resolve_head_silent(), + ctx.repo_path.clone(), + ctx.passphrase_provider.clone(), + &ctx.env_config, + )?; + } + p + } + (None, None) => anyhow::bail!("Provide an artifact file or --signature path"), + }; + + handle_publish(&sig_path, self.package.as_deref(), &self.registry) + } +} diff --git a/crates/auths-cli/src/commands/unified_verify.rs b/crates/auths-cli/src/commands/unified_verify.rs index 9ef516db..b5641a47 100644 --- a/crates/auths-cli/src/commands/unified_verify.rs +++ b/crates/auths-cli/src/commands/unified_verify.rs @@ -5,22 +5,25 @@ use clap::Parser; use std::path::{Path, PathBuf}; use super::verify_commit::{VerifyCommitCommand, handle_verify_commit}; +use crate::commands::artifact::verify::handle_verify as handle_artifact_verify; use crate::commands::device::verify_attestation::{VerifyCommand, handle_verify}; /// What kind of target the user provided. pub enum VerifyTarget { GitRef(String), Attestation(String), + ArtifactFile(PathBuf), // binary artifact, will look up .auths.json sidecar } /// Determine whether `raw_target` is a Git reference or an attestation path. /// /// Rules (evaluated in order): /// 1. "-" → stdin attestation -/// 2. Path exists on disk → attestation file -/// 3. Contains ".." (range notation) → git ref -/// 4. Is "HEAD" or matches ^[0-9a-f]{4,40}$ → git ref -/// 5. Otherwise → git ref (assume the user knows what they're typing) +/// 2. Path exists on disk and is JSON → attestation file +/// 3. Path exists on disk and is not JSON → artifact file (sidecar lookup) +/// 4. Contains ".." (range notation) → git ref +/// 5. Is "HEAD" or matches ^[0-9a-f]{4,40}$ → git ref +/// 6. Otherwise → git ref (assume the user knows what they're typing) /// /// Args: /// * `raw_target` - Raw CLI input string. @@ -36,7 +39,11 @@ pub fn parse_verify_target(raw_target: &str) -> VerifyTarget { } let path = Path::new(raw_target); if path.exists() { - return VerifyTarget::Attestation(raw_target.to_string()); + if is_attestation_path(raw_target) { + return VerifyTarget::Attestation(raw_target.to_string()); + } else { + return VerifyTarget::ArtifactFile(path.to_path_buf()); + } } if raw_target.contains("..") { return VerifyTarget::GitRef(raw_target.to_string()); @@ -57,6 +64,12 @@ pub fn parse_verify_target(raw_target: &str) -> VerifyTarget { VerifyTarget::GitRef(raw_target.to_string()) } +/// Returns true if the path looks like an attestation/JSON file rather than a binary artifact. +fn is_attestation_path(path: &str) -> bool { + let lower = path.to_lowercase(); + lower.ends_with(".json") +} + /// Unified verify command: verifies a signed commit or an attestation. #[derive(Parser, Debug, Clone)] #[command( @@ -64,21 +77,18 @@ pub fn parse_verify_target(raw_target: &str) -> VerifyTarget { after_help = "Examples: auths verify HEAD # Verify current commit signature auths verify main..HEAD # Verify range of commits - auths verify artifact.json # Verify signed artifact + auths verify release.tar.gz # Verify artifact (finds .auths.json sidecar) + auths verify release.tar.gz.auths.json # Verify attestation file directly auths verify - < artifact.json # Verify from stdin -Trust Policies: - Defaults to TOFU (Trust-On-First-Use) on interactive terminals. - Use --trust explicit in CI/CD to reject unknown identities. - Artifact Verification: - File signatures are stored as .auths.json. - JSON attestations can be verified directly. + Pass the artifact file directly — auths finds .auths.json automatically. + Pass --signature to override the default sidecar path. Related: - auths trust add — Add an identity to your trust store - auths sign — Create signatures - auths --help-all — See all commands" + auths sign — Create signatures + auths publish — Sign and publish to registry + auths trust — Manage trusted identities" )] pub struct UnifiedVerifyCommand { /// Git ref, commit hash, range (e.g. HEAD, abc1234, main..HEAD), @@ -113,6 +123,11 @@ pub struct UnifiedVerifyCommand { /// Witness public keys as DID:hex pairs. #[arg(long, num_args = 1..)] pub witness_keys: Vec, + + /// Path to signature file. Only used when verifying an artifact file (not a commit). + /// Defaults to .auths.json. + #[arg(long, value_name = "PATH")] + pub signature: Option, } /// Handle the unified verify command. @@ -148,6 +163,18 @@ pub async fn handle_verify_unified(cmd: UnifiedVerifyCommand) -> Result<()> { }; handle_verify(verify_cmd).await } + VerifyTarget::ArtifactFile(artifact_path) => { + handle_artifact_verify( + &artifact_path, + cmd.signature, + cmd.identity_bundle, + cmd.witness_receipts, + &cmd.witness_keys, + cmd.witness_threshold, + false, + ) + .await + } } } @@ -202,4 +229,37 @@ mod tests { let target = parse_verify_target(f.to_str().unwrap()); assert!(matches!(target, VerifyTarget::Attestation(_))); } + + #[test] + fn test_parse_verify_target_binary_file_routes_to_artifact() { + use std::fs::File; + use tempfile::tempdir; + let dir = tempdir().unwrap(); + let artifact = dir.path().join("release.tar.gz"); + File::create(&artifact).unwrap(); + let target = parse_verify_target(artifact.to_str().unwrap()); + assert!(matches!(target, VerifyTarget::ArtifactFile(_))); + } + + #[test] + fn test_parse_verify_target_json_file_routes_to_attestation() { + use std::fs::File; + use tempfile::tempdir; + let dir = tempdir().unwrap(); + let attest = dir.path().join("release.auths.json"); + File::create(&attest).unwrap(); + let target = parse_verify_target(attest.to_str().unwrap()); + assert!(matches!(target, VerifyTarget::Attestation(_))); + } + + #[test] + fn test_parse_verify_target_plain_json_routes_to_attestation() { + use std::fs::File; + use tempfile::tempdir; + let dir = tempdir().unwrap(); + let f = dir.path().join("attestation.json"); + File::create(&f).unwrap(); + let target = parse_verify_target(f.to_str().unwrap()); + assert!(matches!(target, VerifyTarget::Attestation(_))); + } } diff --git a/crates/auths-cli/src/main.rs b/crates/auths-cli/src/main.rs index 4fd6fc8b..cea96b0f 100644 --- a/crates/auths-cli/src/main.rs +++ b/crates/auths-cli/src/main.rs @@ -74,8 +74,10 @@ fn run() -> Result<()> { let result = match command { // Primary RootCommand::Init(cmd) => cmd.execute(&ctx), + RootCommand::Demo(cmd) => cmd.execute(&ctx), RootCommand::Sign(cmd) => cmd.execute(&ctx), RootCommand::Verify(cmd) => cmd.execute(&ctx), + RootCommand::Publish(cmd) => cmd.execute(&ctx), RootCommand::Artifact(cmd) => cmd.execute(&ctx), RootCommand::Status(cmd) => cmd.execute(&ctx), RootCommand::Whoami(cmd) => cmd.execute(&ctx), @@ -136,11 +138,11 @@ struct CommandGroup { const HELP_GROUPS: &[CommandGroup] = &[ CommandGroup { heading: "Primary", - commands: &["init", "sign", "verify", "artifact", "status", "whoami"], + commands: &["init", "sign", "verify", "status", "whoami"], }, CommandGroup { heading: "Setup & Troubleshooting", - commands: &["pair", "trust", "doctor", "tutorial"], + commands: &["pair", "trust", "doctor", "tutorial", "demo"], }, CommandGroup { heading: "CI/CD", @@ -282,6 +284,22 @@ fn print_grouped_help(show_all: bool) -> Result<()> { println!("{CYAN_BOLD}{:<23}{RESET} Print help", " -h, --help"); println!("{CYAN_BOLD}{:<23}{RESET} Print version", " -V, --version"); + // Examples + if !show_all { + println!(); + println!("{BLUE_BOLD}Examples:{RESET}"); + println!( + " {CYAN_BOLD}auths init{RESET} Set up your cryptographic identity" + ); + println!( + " {CYAN_BOLD}auths demo{RESET} Try sign + verify in 30 seconds" + ); + println!(" {CYAN_BOLD}auths sign release.tar.gz{RESET} Sign an artifact"); + println!( + " {CYAN_BOLD}auths verify release.tar.gz{RESET} Verify a signed artifact (auto-finds .auths.json)" + ); + } + // Footer println!(); println!("Run 'auths --help' for details on any command."); diff --git a/crates/auths-cli/tests/cases/verify.rs b/crates/auths-cli/tests/cases/verify.rs index 49d34b64..0385c718 100644 --- a/crates/auths-cli/tests/cases/verify.rs +++ b/crates/auths-cli/tests/cases/verify.rs @@ -44,7 +44,7 @@ fn create_signed_attestation( } fn write_attestation_to_file(att: &Attestation) -> NamedTempFile { - let mut file = NamedTempFile::new().unwrap(); + let mut file = tempfile::Builder::new().suffix(".json").tempfile().unwrap(); let json = serde_json::to_string(att).unwrap(); file.write_all(json.as_bytes()).unwrap(); file.flush().unwrap(); diff --git a/crates/auths-core/src/policy/mod.rs b/crates/auths-core/src/policy/mod.rs index 0716103b..c4b35b37 100644 --- a/crates/auths-core/src/policy/mod.rs +++ b/crates/auths-core/src/policy/mod.rs @@ -10,7 +10,7 @@ //! //! ```text //! ┌─────────────────┐ ┌──────────────┐ ┌─────────────────┐ -//! │ Storage/Registry │ ──► │ Policy Engine │ ──► │ Decision (Y/N/?) │ +//! │ Storage/Registry│ ──► │ Policy Engine│ ──► │ Decision (Y/N/?)│ //! └─────────────────┘ └──────────────┘ └─────────────────┘ //! (data) (evaluation) (result) //! ``` diff --git a/docs/cli/commands/advanced.md b/docs/cli/commands/advanced.md index b63759e9..628d7d8b 100644 --- a/docs/cli/commands/advanced.md +++ b/docs/cli/commands/advanced.md @@ -665,6 +665,8 @@ Sign multiple artifacts matching a glob pattern ### auths artifact verify +> **Prefer `auths verify `** — pass the artifact file directly and the sidecar is found automatically. `auths artifact verify` remains available for advanced use. + ```bash auths artifact verify ``` diff --git a/docs/guides/platforms/ci-cd.md b/docs/guides/platforms/ci-cd.md index 92620e74..b7f5e461 100644 --- a/docs/guides/platforms/ci-cd.md +++ b/docs/guides/platforms/ci-cd.md @@ -133,16 +133,23 @@ Exit codes: `0` for valid, `1` for invalid/unsigned, `2` for errors. ### Verifying artifacts -Verify a signed artifact attestation: +Verify a signed artifact by passing the artifact file directly — `auths` finds the `.auths.json` sidecar automatically: ```bash -auths verify myproject.tar.gz.auths.json --issuer-pk +auths verify myproject.tar.gz --issuer-pk ``` Or using the issuer's DID: ```bash -auths verify myproject.tar.gz.auths.json --issuer-did did:keri:EaBcDeFg... +auths verify myproject.tar.gz --issuer-did did:keri:EaBcDeFg... +``` + +You can also pass the attestation file directly, or override the sidecar path: + +```bash +auths verify myproject.tar.gz.auths.json --issuer-pk +auths verify myproject.tar.gz --signature /path/to/custom.auths.json --issuer-pk ``` ### JSON output for CI parsing diff --git a/docs/smoketests/cli_improvements.md b/docs/smoketests/cli_improvements.md index 3c14d031..491d23e4 100644 --- a/docs/smoketests/cli_improvements.md +++ b/docs/smoketests/cli_improvements.md @@ -132,7 +132,7 @@ auths pair --help **Current Flow:** ```bash auths sign /path/to/artifact -auths verify /path/to/artifact.auths.json +auths verify /path/to/artifact ``` **Pain Points:**