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)]
138 pub remote: Option<String>,
139
140 #[arg(long)]
144 pub oobi: Option<String>,
145
146 #[arg(long = "require-witnesses")]
149 pub require_witnesses: bool,
150}
151
152pub async fn handle_verify_unified(
159 cmd: UnifiedVerifyCommand,
160 env_config: &auths_sdk::core_config::EnvironmentConfig,
161) -> Result<()> {
162 match parse_verify_target(&cmd.target) {
163 VerifyTarget::GitRef(ref_str) => {
164 let commit_cmd = VerifyCommitCommand {
165 commit: ref_str,
166 witness_receipts: cmd.witness_receipts,
167 witness_threshold: cmd.witness_threshold,
168 witness_keys: cmd.witness_keys,
169 remote: cmd.remote,
170 oobi: cmd.oobi,
171 require_witnesses: cmd.require_witnesses,
172 identity_bundle: cmd.identity_bundle,
175 };
176 handle_verify_commit(commit_cmd, env_config).await
177 }
178 VerifyTarget::Attestation(path_str) => {
179 let verify_cmd = VerifyCommand {
180 attestation: path_str,
181 issuer_pk: cmd.issuer_pk,
182 issuer_did: cmd.issuer_did,
183 trust: None,
184 roots_file: None,
185 witness_receipts: cmd.witness_receipts,
186 witness_threshold: cmd.witness_threshold,
187 witness_keys: cmd.witness_keys,
188 };
189 handle_verify(verify_cmd).await
190 }
191 VerifyTarget::ArtifactFile(artifact_path) => {
192 handle_artifact_verify(
193 &artifact_path,
194 cmd.signature,
195 cmd.identity_bundle,
196 cmd.witness_receipts,
197 &cmd.witness_keys,
198 cmd.witness_threshold,
199 false,
200 )
201 .await
202 }
203 }
204}
205
206impl crate::commands::executable::ExecutableCommand for UnifiedVerifyCommand {
207 fn execute(&self, ctx: &crate::config::CliConfig) -> anyhow::Result<()> {
208 let rt = tokio::runtime::Runtime::new()?;
209 rt.block_on(handle_verify_unified(self.clone(), &ctx.env_config))
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn test_parse_verify_target_git_ref() {
219 assert!(matches!(
220 parse_verify_target("HEAD"),
221 VerifyTarget::GitRef(_)
222 ));
223 assert!(matches!(
224 parse_verify_target("abc1234"),
225 VerifyTarget::GitRef(_)
226 ));
227 assert!(matches!(
228 parse_verify_target("main..HEAD"),
229 VerifyTarget::GitRef(_)
230 ));
231 }
232
233 #[test]
234 fn test_parse_verify_target_stdin() {
235 assert!(matches!(
236 parse_verify_target("-"),
237 VerifyTarget::Attestation(_)
238 ));
239 }
240
241 #[test]
242 fn test_parse_verify_target_nonexistent_defaults_to_git_ref() {
243 let target = parse_verify_target("/nonexistent/attestation.json");
244 assert!(matches!(target, VerifyTarget::GitRef(_)));
245 }
246
247 #[test]
248 fn test_parse_verify_target_file() {
249 use std::fs::File;
250 use tempfile::tempdir;
251 let dir = tempdir().unwrap();
252 let f = dir.path().join("attestation.json");
253 File::create(&f).unwrap();
254 let target = parse_verify_target(f.to_str().unwrap());
255 assert!(matches!(target, VerifyTarget::Attestation(_)));
256 }
257
258 #[test]
259 fn test_parse_verify_target_binary_file_routes_to_artifact() {
260 use std::fs::File;
261 use tempfile::tempdir;
262 let dir = tempdir().unwrap();
263 let artifact = dir.path().join("release.tar.gz");
264 File::create(&artifact).unwrap();
265 let target = parse_verify_target(artifact.to_str().unwrap());
266 assert!(matches!(target, VerifyTarget::ArtifactFile(_)));
267 }
268
269 #[test]
270 fn test_parse_verify_target_json_file_routes_to_attestation() {
271 use std::fs::File;
272 use tempfile::tempdir;
273 let dir = tempdir().unwrap();
274 let attest = dir.path().join("release.auths.json");
275 File::create(&attest).unwrap();
276 let target = parse_verify_target(attest.to_str().unwrap());
277 assert!(matches!(target, VerifyTarget::Attestation(_)));
278 }
279
280 #[test]
281 fn test_parse_verify_target_plain_json_routes_to_attestation() {
282 use std::fs::File;
283 use tempfile::tempdir;
284 let dir = tempdir().unwrap();
285 let f = dir.path().join("attestation.json");
286 File::create(&f).unwrap();
287 let target = parse_verify_target(f.to_str().unwrap());
288 assert!(matches!(target, VerifyTarget::Attestation(_)));
289 }
290}