auths_cli/commands/
unified_verify.rs1use anyhow::Result;
4use clap::Parser;
5use std::path::{Path, PathBuf};
6
7use super::verify_commit::{VerifyCommitCommand, handle_verify_commit};
8use crate::commands::artifact::verify::handle_verify as handle_artifact_verify;
9use crate::commands::device::verify_attestation::{VerifyCommand, handle_verify};
10
11pub enum VerifyTarget {
13 GitRef(String),
14 Attestation(String),
15 ArtifactFile(PathBuf), }
17
18pub fn parse_verify_target(raw_target: &str) -> VerifyTarget {
37 if raw_target == "-" {
38 return VerifyTarget::Attestation(raw_target.to_string());
39 }
40 let path = Path::new(raw_target);
41 if path.exists() {
42 if is_attestation_path(raw_target) {
43 return VerifyTarget::Attestation(raw_target.to_string());
44 } else {
45 return VerifyTarget::ArtifactFile(path.to_path_buf());
46 }
47 }
48 if raw_target.contains("..") {
49 return VerifyTarget::GitRef(raw_target.to_string());
50 }
51 if raw_target.eq_ignore_ascii_case("HEAD") {
52 return VerifyTarget::GitRef(raw_target.to_string());
53 }
54 let is_hex = raw_target.len() >= 4
56 && raw_target.len() <= 40
57 && raw_target.chars().all(|c| c.is_ascii_hexdigit());
58 if is_hex {
59 return VerifyTarget::GitRef(raw_target.to_string());
60 }
61 VerifyTarget::GitRef(raw_target.to_string())
65}
66
67fn is_attestation_path(path: &str) -> bool {
69 let lower = path.to_lowercase();
70 lower.ends_with(".json")
71}
72
73#[derive(Parser, Debug, Clone)]
75#[command(
76 about = "Verify a signed commit or attestation.",
77 after_help = "Examples:
78 auths verify HEAD # Verify current commit signature
79 auths verify main..HEAD # Verify range of commits
80 auths verify release.tar.gz # Verify artifact (finds .auths.json sidecar)
81 auths verify release.tar.gz.auths.json # Verify attestation file directly
82 auths verify - < artifact.json # Verify from stdin
83
84Artifact Verification:
85 Pass the artifact file directly — auths finds <file>.auths.json automatically.
86 Pass --signature to override the default sidecar path.
87
88Exit codes:
89 0 verified
90 1 verification failed (signature, trust, or untrusted/unresolvable signer)
91 2 could not attempt (I/O error, malformed input, missing repository)
92
93Related:
94 auths sign — Create signatures
95 auths publish — Sign and publish to registry
96 auths trust — Manage trusted identities"
97)]
98pub struct UnifiedVerifyCommand {
99 #[arg(default_value = "HEAD")]
102 pub target: String,
103
104 #[arg(long, value_parser)]
106 pub identity_bundle: Option<PathBuf>,
107
108 #[arg(long = "signer-key")]
110 pub issuer_pk: Option<String>,
111
112 #[arg(long = "signer", visible_alias = "issuer-did")]
114 pub issuer_did: Option<String>,
115
116 #[arg(long = "witness-signatures")]
118 pub witness_receipts: Option<PathBuf>,
119
120 #[arg(long = "witnesses-required", default_value = "1")]
122 pub witness_threshold: usize,
123
124 #[arg(long, num_args = 1..)]
126 pub witness_keys: Vec<String>,
127
128 #[arg(long, value_name = "PATH")]
131 pub signature: Option<PathBuf>,
132
133 #[arg(long = "require-witnesses")]
136 pub require_witnesses: bool,
137}
138
139pub async fn handle_verify_unified(
146 cmd: UnifiedVerifyCommand,
147 env_config: &auths_sdk::core_config::EnvironmentConfig,
148) -> Result<()> {
149 match parse_verify_target(&cmd.target) {
150 VerifyTarget::GitRef(ref_str) => {
151 let commit_cmd = VerifyCommitCommand {
152 commit: ref_str,
153 witness_receipts: cmd.witness_receipts,
154 witness_threshold: cmd.witness_threshold,
155 witness_keys: cmd.witness_keys,
156 require_witnesses: cmd.require_witnesses,
157 identity_bundle: cmd.identity_bundle,
160 };
161 handle_verify_commit(commit_cmd, env_config).await
162 }
163 VerifyTarget::Attestation(path_str) => {
164 let verify_cmd = VerifyCommand {
165 attestation: path_str,
166 issuer_pk: cmd.issuer_pk,
167 issuer_did: cmd.issuer_did,
168 trust: None,
169 roots_file: None,
170 witness_receipts: cmd.witness_receipts,
171 witness_threshold: cmd.witness_threshold,
172 witness_keys: cmd.witness_keys,
173 };
174 handle_verify(verify_cmd).await
175 }
176 VerifyTarget::ArtifactFile(artifact_path) => {
177 handle_artifact_verify(
178 &artifact_path,
179 crate::commands::artifact::verify::VerifyArtifactArgs {
180 signature: cmd.signature,
181 identity_bundle: cmd.identity_bundle,
182 witness_receipts: cmd.witness_receipts,
183 witness_keys: cmd.witness_keys,
184 witness_threshold: cmd.witness_threshold,
185 verify_commit: false,
186 ephemeral_anchor: crate::commands::artifact::verify::EphemeralAnchor::Required,
187 oidc_policy: None,
188 oidc_policy_did: None,
189 log_evidence: None,
190 log_key: None,
191 expect_signer: None,
192 require_rooted_signer: false,
193 },
194 )
195 .await
196 }
197 }
198}
199
200impl crate::commands::executable::ExecutableCommand for UnifiedVerifyCommand {
201 fn execute(&self, ctx: &crate::config::CliConfig) -> anyhow::Result<()> {
202 let rt = tokio::runtime::Runtime::new()?;
203 rt.block_on(handle_verify_unified(self.clone(), &ctx.env_config))
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn test_parse_verify_target_git_ref() {
213 assert!(matches!(
214 parse_verify_target("HEAD"),
215 VerifyTarget::GitRef(_)
216 ));
217 assert!(matches!(
218 parse_verify_target("abc1234"),
219 VerifyTarget::GitRef(_)
220 ));
221 assert!(matches!(
222 parse_verify_target("main..HEAD"),
223 VerifyTarget::GitRef(_)
224 ));
225 }
226
227 #[test]
228 fn test_parse_verify_target_stdin() {
229 assert!(matches!(
230 parse_verify_target("-"),
231 VerifyTarget::Attestation(_)
232 ));
233 }
234
235 #[test]
236 fn test_parse_verify_target_nonexistent_defaults_to_git_ref() {
237 let target = parse_verify_target("/nonexistent/attestation.json");
238 assert!(matches!(target, VerifyTarget::GitRef(_)));
239 }
240
241 #[test]
242 fn test_parse_verify_target_file() {
243 use std::fs::File;
244 use tempfile::tempdir;
245 let dir = tempdir().unwrap();
246 let f = dir.path().join("attestation.json");
247 File::create(&f).unwrap();
248 let target = parse_verify_target(f.to_str().unwrap());
249 assert!(matches!(target, VerifyTarget::Attestation(_)));
250 }
251
252 #[test]
253 fn test_parse_verify_target_binary_file_routes_to_artifact() {
254 use std::fs::File;
255 use tempfile::tempdir;
256 let dir = tempdir().unwrap();
257 let artifact = dir.path().join("release.tar.gz");
258 File::create(&artifact).unwrap();
259 let target = parse_verify_target(artifact.to_str().unwrap());
260 assert!(matches!(target, VerifyTarget::ArtifactFile(_)));
261 }
262
263 #[test]
264 fn test_parse_verify_target_json_file_routes_to_attestation() {
265 use std::fs::File;
266 use tempfile::tempdir;
267 let dir = tempdir().unwrap();
268 let attest = dir.path().join("release.auths.json");
269 File::create(&attest).unwrap();
270 let target = parse_verify_target(attest.to_str().unwrap());
271 assert!(matches!(target, VerifyTarget::Attestation(_)));
272 }
273
274 #[test]
275 fn test_parse_verify_target_plain_json_routes_to_attestation() {
276 use std::fs::File;
277 use tempfile::tempdir;
278 let dir = tempdir().unwrap();
279 let f = dir.path().join("attestation.json");
280 File::create(&f).unwrap();
281 let target = parse_verify_target(f.to_str().unwrap());
282 assert!(matches!(target, VerifyTarget::Attestation(_)));
283 }
284}