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
88Related:
89 auths sign — Create signatures
90 auths publish — Sign and publish to registry
91 auths trust — Manage trusted identities"
92)]
93pub struct UnifiedVerifyCommand {
94 #[arg(default_value = "HEAD")]
97 pub target: String,
98
99 #[arg(long, value_parser)]
101 pub identity_bundle: Option<PathBuf>,
102
103 #[arg(long = "signer-key")]
105 pub issuer_pk: Option<String>,
106
107 #[arg(long = "signer", visible_alias = "issuer-did")]
109 pub issuer_did: Option<String>,
110
111 #[arg(long = "witness-signatures")]
113 pub witness_receipts: Option<PathBuf>,
114
115 #[arg(long = "witnesses-required", default_value = "1")]
117 pub witness_threshold: usize,
118
119 #[arg(long, num_args = 1..)]
121 pub witness_keys: Vec<String>,
122
123 #[arg(long, value_name = "PATH")]
126 pub signature: Option<PathBuf>,
127
128 #[arg(long)]
133 pub remote: Option<String>,
134
135 #[arg(long)]
139 pub oobi: Option<String>,
140
141 #[arg(long = "require-witnesses")]
144 pub require_witnesses: bool,
145}
146
147pub async fn handle_verify_unified(
154 cmd: UnifiedVerifyCommand,
155 env_config: &auths_sdk::core_config::EnvironmentConfig,
156) -> Result<()> {
157 match parse_verify_target(&cmd.target) {
158 VerifyTarget::GitRef(ref_str) => {
159 let commit_cmd = VerifyCommitCommand {
160 commit: ref_str,
161 witness_receipts: cmd.witness_receipts,
162 witness_threshold: cmd.witness_threshold,
163 witness_keys: cmd.witness_keys,
164 remote: cmd.remote,
165 oobi: cmd.oobi,
166 require_witnesses: cmd.require_witnesses,
167 identity_bundle: cmd.identity_bundle,
170 };
171 handle_verify_commit(commit_cmd, env_config).await
172 }
173 VerifyTarget::Attestation(path_str) => {
174 let verify_cmd = VerifyCommand {
175 attestation: path_str,
176 issuer_pk: cmd.issuer_pk,
177 issuer_did: cmd.issuer_did,
178 trust: None,
179 roots_file: None,
180 witness_receipts: cmd.witness_receipts,
181 witness_threshold: cmd.witness_threshold,
182 witness_keys: cmd.witness_keys,
183 };
184 handle_verify(verify_cmd).await
185 }
186 VerifyTarget::ArtifactFile(artifact_path) => {
187 handle_artifact_verify(
188 &artifact_path,
189 cmd.signature,
190 cmd.identity_bundle,
191 cmd.witness_receipts,
192 &cmd.witness_keys,
193 cmd.witness_threshold,
194 false,
195 )
196 .await
197 }
198 }
199}
200
201impl crate::commands::executable::ExecutableCommand for UnifiedVerifyCommand {
202 fn execute(&self, ctx: &crate::config::CliConfig) -> anyhow::Result<()> {
203 let rt = tokio::runtime::Runtime::new()?;
204 rt.block_on(handle_verify_unified(self.clone(), &ctx.env_config))
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn test_parse_verify_target_git_ref() {
214 assert!(matches!(
215 parse_verify_target("HEAD"),
216 VerifyTarget::GitRef(_)
217 ));
218 assert!(matches!(
219 parse_verify_target("abc1234"),
220 VerifyTarget::GitRef(_)
221 ));
222 assert!(matches!(
223 parse_verify_target("main..HEAD"),
224 VerifyTarget::GitRef(_)
225 ));
226 }
227
228 #[test]
229 fn test_parse_verify_target_stdin() {
230 assert!(matches!(
231 parse_verify_target("-"),
232 VerifyTarget::Attestation(_)
233 ));
234 }
235
236 #[test]
237 fn test_parse_verify_target_nonexistent_defaults_to_git_ref() {
238 let target = parse_verify_target("/nonexistent/attestation.json");
239 assert!(matches!(target, VerifyTarget::GitRef(_)));
240 }
241
242 #[test]
243 fn test_parse_verify_target_file() {
244 use std::fs::File;
245 use tempfile::tempdir;
246 let dir = tempdir().unwrap();
247 let f = dir.path().join("attestation.json");
248 File::create(&f).unwrap();
249 let target = parse_verify_target(f.to_str().unwrap());
250 assert!(matches!(target, VerifyTarget::Attestation(_)));
251 }
252
253 #[test]
254 fn test_parse_verify_target_binary_file_routes_to_artifact() {
255 use std::fs::File;
256 use tempfile::tempdir;
257 let dir = tempdir().unwrap();
258 let artifact = dir.path().join("release.tar.gz");
259 File::create(&artifact).unwrap();
260 let target = parse_verify_target(artifact.to_str().unwrap());
261 assert!(matches!(target, VerifyTarget::ArtifactFile(_)));
262 }
263
264 #[test]
265 fn test_parse_verify_target_json_file_routes_to_attestation() {
266 use std::fs::File;
267 use tempfile::tempdir;
268 let dir = tempdir().unwrap();
269 let attest = dir.path().join("release.auths.json");
270 File::create(&attest).unwrap();
271 let target = parse_verify_target(attest.to_str().unwrap());
272 assert!(matches!(target, VerifyTarget::Attestation(_)));
273 }
274
275 #[test]
276 fn test_parse_verify_target_plain_json_routes_to_attestation() {
277 use std::fs::File;
278 use tempfile::tempdir;
279 let dir = tempdir().unwrap();
280 let f = dir.path().join("attestation.json");
281 File::create(&f).unwrap();
282 let target = parse_verify_target(f.to_str().unwrap());
283 assert!(matches!(target, VerifyTarget::Attestation(_)));
284 }
285}