auths_cli/commands/
auth.rs1use anyhow::{Context, Result, anyhow};
2use clap::{Parser, Subcommand};
3
4use auths_sdk::storage_layout::layout;
5
6use crate::factories::storage::build_auths_context;
7
8use crate::commands::executable::ExecutableCommand;
9use crate::config::CliConfig;
10use crate::ux::format::{JsonResponse, is_json_mode};
11
12#[derive(Parser, Debug, Clone)]
14#[command(
15 about = "Authenticate with external services using your auths identity",
16 after_help = "Examples:
17 auths auth challenge --nonce abc123def456 --domain example.com
18 # Sign an authentication challenge
19 auths auth challenge --nonce abc123def456
20 # Sign challenge for default domain (auths.dev)
21 auths auth verify --nonce abc123def456 --did did:keri:E... --signature <hex>
22 # Verify a challenge response offline, against the
23 # registry's current key for that DID
24
25Flow:
26 1. Verifier sends a nonce
27 2. Responder runs: auths auth challenge --nonce <nonce> --domain <domain>
28 3. Verifier runs: auths auth verify --nonce <nonce> --domain <domain> \\
29 --did <did> --signature <hex>
30 (offline — the signature is checked against the registry's in-force
31 key, never a key the responder supplied)
32
33Related:
34 auths id — Manage your identity
35 auths sign — Sign files and commits
36 auths verify — Verify signatures"
37)]
38pub struct AuthCommand {
39 #[clap(subcommand)]
40 pub subcommand: AuthSubcommand,
41}
42
43#[derive(Subcommand, Debug, Clone)]
45pub enum AuthSubcommand {
46 Challenge {
48 #[arg(long, allow_hyphen_values = true)]
50 nonce: String,
51
52 #[arg(long, default_value = "auths.dev")]
54 domain: String,
55 },
56
57 Verify {
59 #[arg(long, allow_hyphen_values = true)]
61 nonce: String,
62
63 #[arg(long, default_value = "auths.dev")]
65 domain: String,
66
67 #[arg(long)]
71 did: String,
72
73 #[arg(long)]
75 signature: String,
76 },
77}
78
79fn handle_auth_challenge(nonce: &str, domain: &str, ctx: &CliConfig) -> Result<()> {
80 let repo_path = layout::resolve_repo_path(ctx.repo_path.clone())?;
81 let passphrase_provider = ctx.passphrase_provider.clone();
82
83 let auths_ctx = build_auths_context(
84 &repo_path,
85 &ctx.env_config,
86 Some(ctx.passphrase_provider.clone()),
87 )?;
88 let managed = auths_ctx
89 .identity_storage
90 .load_identity()
91 .context("No identity found. Run `auths init` first.")?;
92
93 let controller_did = &managed.controller_did;
94
95 let key_alias_str =
96 super::key_detect::auto_detect_device_key(ctx.repo_path.as_deref(), &ctx.env_config)?;
97 let key_alias = auths_sdk::keychain::KeyAlias::new(&key_alias_str)
98 .map_err(|e| anyhow!("Invalid key alias: {e}"))?;
99
100 let message = auths_sdk::workflows::auth::build_auth_challenge_message(nonce, domain)
101 .context("Failed to build auth challenge payload")?;
102
103 let (signature_bytes, public_key_bytes, _curve) = auths_sdk::keychain::sign_with_key(
104 auths_ctx.key_storage.as_ref(),
105 &key_alias,
106 passphrase_provider.as_ref(),
107 message.as_bytes(),
108 )
109 .with_context(|| format!("Failed to sign auth challenge with key '{}'", key_alias))?;
110
111 let result = auths_sdk::workflows::auth::SignedAuthChallenge {
112 signature_hex: hex::encode(&signature_bytes),
113 public_key_hex: hex::encode(&public_key_bytes),
114 did: controller_did.to_string(),
115 };
116
117 if is_json_mode() {
118 JsonResponse::success(
119 "auth challenge",
120 &serde_json::json!({
121 "signature": result.signature_hex,
122 "public_key": result.public_key_hex,
123 "did": result.did,
124 }),
125 )
126 .print()
127 .map_err(anyhow::Error::from)
128 } else {
129 println!("Signature: {}", result.signature_hex);
130 println!("Public Key: {}", result.public_key_hex);
131 println!("DID: {}", result.did);
132 Ok(())
133 }
134}
135
136fn handle_auth_verify(nonce: &str, domain: &str, did: &str, signature_hex: &str) -> Result<()> {
144 let signature = hex::decode(signature_hex)
145 .context("--signature must be the hex string printed by `auths auth challenge`")?;
146
147 let auths_home = auths_sdk::paths::auths_home().map_err(|e| anyhow!(e))?;
148 let registry = auths_sdk::storage::GitRegistryBackend::from_config_unchecked(
149 auths_sdk::storage::RegistryConfig::single_tenant(&auths_home),
150 );
151
152 let verified = auths_sdk::workflows::auth::verify_auth_challenge(
153 ®istry, did, nonce, domain, &signature,
154 )
155 .context("Auth challenge verification failed")?;
156
157 if is_json_mode() {
158 JsonResponse::success(
159 "auth verify",
160 &serde_json::json!({
161 "verified": true,
162 "did": verified.did,
163 "public_key": verified.public_key_hex,
164 "curve": verified.curve.to_string(),
165 "nonce": nonce,
166 "domain": domain,
167 }),
168 )
169 .print()
170 .map_err(anyhow::Error::from)
171 } else {
172 println!("✓ Verified — the signature checks out under the registry's current key");
173 println!("DID: {}", verified.did);
174 println!("Public Key: {}", verified.public_key_hex);
175 println!("Curve: {}", verified.curve);
176 println!("Domain: {domain}");
177 Ok(())
178 }
179}
180
181impl ExecutableCommand for AuthCommand {
182 fn execute(&self, ctx: &CliConfig) -> Result<()> {
183 match &self.subcommand {
184 AuthSubcommand::Challenge { nonce, domain } => {
185 handle_auth_challenge(nonce, domain, ctx)
186 }
187 AuthSubcommand::Verify {
188 nonce,
189 domain,
190 did,
191 signature,
192 } => handle_auth_verify(nonce, domain, did, signature),
193 }
194 }
195}