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 crate::commands::artifact::verify::VerifyArtifactArgs {
195 signature: cmd.signature,
196 identity_bundle: cmd.identity_bundle,
197 witness_receipts: cmd.witness_receipts,
198 witness_keys: cmd.witness_keys,
199 witness_threshold: cmd.witness_threshold,
200 verify_commit: false,
201 ephemeral_anchor: crate::commands::artifact::verify::EphemeralAnchor::Required,
202 oidc_policy: None,
203 oidc_policy_did: None,
204 log_evidence: None,
205 log_key: None,
206 expect_signer: None,
207 },
208 )
209 .await
210 }
211 }
212}
213
214impl crate::commands::executable::ExecutableCommand for UnifiedVerifyCommand {
215 fn execute(&self, ctx: &crate::config::CliConfig) -> anyhow::Result<()> {
216 let rt = tokio::runtime::Runtime::new()?;
217 rt.block_on(handle_verify_unified(self.clone(), &ctx.env_config))
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn test_parse_verify_target_git_ref() {
227 assert!(matches!(
228 parse_verify_target("HEAD"),
229 VerifyTarget::GitRef(_)
230 ));
231 assert!(matches!(
232 parse_verify_target("abc1234"),
233 VerifyTarget::GitRef(_)
234 ));
235 assert!(matches!(
236 parse_verify_target("main..HEAD"),
237 VerifyTarget::GitRef(_)
238 ));
239 }
240
241 #[test]
242 fn test_parse_verify_target_stdin() {
243 assert!(matches!(
244 parse_verify_target("-"),
245 VerifyTarget::Attestation(_)
246 ));
247 }
248
249 #[test]
250 fn test_parse_verify_target_nonexistent_defaults_to_git_ref() {
251 let target = parse_verify_target("/nonexistent/attestation.json");
252 assert!(matches!(target, VerifyTarget::GitRef(_)));
253 }
254
255 #[test]
256 fn test_parse_verify_target_file() {
257 use std::fs::File;
258 use tempfile::tempdir;
259 let dir = tempdir().unwrap();
260 let f = dir.path().join("attestation.json");
261 File::create(&f).unwrap();
262 let target = parse_verify_target(f.to_str().unwrap());
263 assert!(matches!(target, VerifyTarget::Attestation(_)));
264 }
265
266 #[test]
267 fn test_parse_verify_target_binary_file_routes_to_artifact() {
268 use std::fs::File;
269 use tempfile::tempdir;
270 let dir = tempdir().unwrap();
271 let artifact = dir.path().join("release.tar.gz");
272 File::create(&artifact).unwrap();
273 let target = parse_verify_target(artifact.to_str().unwrap());
274 assert!(matches!(target, VerifyTarget::ArtifactFile(_)));
275 }
276
277 #[test]
278 fn test_parse_verify_target_json_file_routes_to_attestation() {
279 use std::fs::File;
280 use tempfile::tempdir;
281 let dir = tempdir().unwrap();
282 let attest = dir.path().join("release.auths.json");
283 File::create(&attest).unwrap();
284 let target = parse_verify_target(attest.to_str().unwrap());
285 assert!(matches!(target, VerifyTarget::Attestation(_)));
286 }
287
288 #[test]
289 fn test_parse_verify_target_plain_json_routes_to_attestation() {
290 use std::fs::File;
291 use tempfile::tempdir;
292 let dir = tempdir().unwrap();
293 let f = dir.path().join("attestation.json");
294 File::create(&f).unwrap();
295 let target = parse_verify_target(f.to_str().unwrap());
296 assert!(matches!(target, VerifyTarget::Attestation(_)));
297 }
298}