-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathunified_verify.rs
More file actions
299 lines (270 loc) · 10.8 KB
/
Copy pathunified_verify.rs
File metadata and controls
299 lines (270 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
//! Unified verify command: verifies a git commit OR an attestation file.
use anyhow::Result;
use clap::Parser;
use std::path::{Path, PathBuf};
use super::verify_commit::{VerifyCommitCommand, handle_verify_commit};
use crate::commands::artifact::verify::handle_verify as handle_artifact_verify;
use crate::commands::device::verify_attestation::{VerifyCommand, handle_verify};
/// What kind of target the user provided.
pub enum VerifyTarget {
GitRef(String),
Attestation(String),
ArtifactFile(PathBuf), // binary artifact, will look up .auths.json sidecar
}
/// Determine whether `raw_target` is a Git reference or an attestation path.
///
/// Rules (evaluated in order):
/// 1. "-" → stdin attestation
/// 2. Path exists on disk and is JSON → attestation file
/// 3. Path exists on disk and is not JSON → artifact file (sidecar lookup)
/// 4. Contains ".." (range notation) → git ref
/// 5. Is "HEAD" or matches ^[0-9a-f]{4,40}$ → git ref
/// 6. Otherwise → git ref (assume the user knows what they're typing)
///
/// Args:
/// * `raw_target` - Raw CLI input string.
///
/// Usage:
/// ```ignore
/// let t = parse_verify_target("HEAD");
/// assert!(matches!(t, VerifyTarget::GitRef(_)));
/// ```
pub fn parse_verify_target(raw_target: &str) -> VerifyTarget {
if raw_target == "-" {
return VerifyTarget::Attestation(raw_target.to_string());
}
let path = Path::new(raw_target);
if path.exists() {
if is_attestation_path(raw_target) {
return VerifyTarget::Attestation(raw_target.to_string());
} else {
return VerifyTarget::ArtifactFile(path.to_path_buf());
}
}
if raw_target.contains("..") {
return VerifyTarget::GitRef(raw_target.to_string());
}
if raw_target.eq_ignore_ascii_case("HEAD") {
return VerifyTarget::GitRef(raw_target.to_string());
}
// 4-40 hex chars → commit hash
let is_hex = raw_target.len() >= 4
&& raw_target.len() <= 40
&& raw_target.chars().all(|c| c.is_ascii_hexdigit());
if is_hex {
return VerifyTarget::GitRef(raw_target.to_string());
}
// Fallback: treat as git ref. The execution layer (handle_verify_commit) will
// return a clear error if the ref doesn't resolve in the git repo, so no
// silent data loss occurs from a typoed filename being misclassified.
VerifyTarget::GitRef(raw_target.to_string())
}
/// Returns true if the path looks like an attestation/JSON file rather than a binary artifact.
fn is_attestation_path(path: &str) -> bool {
let lower = path.to_lowercase();
lower.ends_with(".json")
}
/// Unified verify command: verifies a signed commit or an attestation.
#[derive(Parser, Debug, Clone)]
#[command(
about = "Verify a signed commit or attestation.",
after_help = "Examples:
auths verify HEAD # Verify current commit signature
auths verify main..HEAD # Verify range of commits
auths verify release.tar.gz # Verify artifact (finds .auths.json sidecar)
auths verify release.tar.gz.auths.json # Verify attestation file directly
auths verify - < artifact.json # Verify from stdin
Artifact Verification:
Pass the artifact file directly — auths finds <file>.auths.json automatically.
Pass --signature to override the default sidecar path.
Exit codes:
0 verified
1 verification failed (signature, trust, or untrusted/unresolvable signer)
2 could not attempt (I/O error, malformed input, missing repository)
Related:
auths sign — Create signatures
auths publish — Sign and publish to registry
auths trust — Manage trusted identities"
)]
pub struct UnifiedVerifyCommand {
/// Git ref, commit hash, range (e.g. HEAD, abc1234, main..HEAD),
/// or path to an attestation JSON file / "-" for stdin.
#[arg(default_value = "HEAD")]
pub target: String,
/// Path to identity bundle JSON (for CI/CD stateless commit verification).
#[arg(long, value_parser)]
pub identity_bundle: Option<PathBuf>,
/// Signer public key in hex format (attestation verification).
#[arg(long = "signer-key")]
pub issuer_pk: Option<String>,
/// Signer identity ID for attestation trust-based key resolution.
#[arg(long = "signer", visible_alias = "issuer-did")]
pub issuer_did: Option<String>,
/// Path to witness signatures JSON file.
#[arg(long = "witness-signatures")]
pub witness_receipts: Option<PathBuf>,
/// Number of witnesses required.
#[arg(long = "witnesses-required", default_value = "1")]
pub witness_threshold: usize,
/// Witness public keys as DID:hex pairs.
#[arg(long, num_args = 1..)]
pub witness_keys: Vec<String>,
/// Path to signature file. Only used when verifying an artifact file (not a commit).
/// Defaults to <FILE>.auths.json.
#[arg(long, value_name = "PATH")]
pub signature: Option<PathBuf>,
/// Fetch a signer's KEL from this git remote when it is absent locally
/// (opt-in). The local registry stays the trusted floor — a remote can only
/// advance the key-state, never roll it back. Without it, resolution is
/// local-only (no network).
#[arg(long)]
pub remote: Option<String>,
/// Fetch signer KELs over HTTP from this OOBI base URL (SSRF-hardened:
/// HTTPS-only, no redirects, private/loopback hosts blocked). Takes
/// precedence over `--remote`.
#[arg(long)]
pub oobi: Option<String>,
/// Fail verification when the signer's root KEL has not reached witness
/// quorum (fail-closed). Default: warn and continue.
#[arg(long = "require-witnesses")]
pub require_witnesses: bool,
}
/// Handle the unified verify command.
///
/// Routes to commit verification or attestation verification based on target type.
///
/// Args:
/// * `cmd` - Parsed UnifiedVerifyCommand.
pub async fn handle_verify_unified(
cmd: UnifiedVerifyCommand,
env_config: &auths_sdk::core_config::EnvironmentConfig,
) -> Result<()> {
match parse_verify_target(&cmd.target) {
VerifyTarget::GitRef(ref_str) => {
let commit_cmd = VerifyCommitCommand {
commit: ref_str,
witness_receipts: cmd.witness_receipts,
witness_threshold: cmd.witness_threshold,
witness_keys: cmd.witness_keys,
remote: cmd.remote,
oobi: cmd.oobi,
require_witnesses: cmd.require_witnesses,
// Honor --identity-bundle for commit/git-ref targets (previously dropped
// here, silently verifying against the wrong/default trust root).
identity_bundle: cmd.identity_bundle,
};
handle_verify_commit(commit_cmd, env_config).await
}
VerifyTarget::Attestation(path_str) => {
let verify_cmd = VerifyCommand {
attestation: path_str,
issuer_pk: cmd.issuer_pk,
issuer_did: cmd.issuer_did,
trust: None,
roots_file: None,
witness_receipts: cmd.witness_receipts,
witness_threshold: cmd.witness_threshold,
witness_keys: cmd.witness_keys,
};
handle_verify(verify_cmd).await
}
VerifyTarget::ArtifactFile(artifact_path) => {
handle_artifact_verify(
&artifact_path,
crate::commands::artifact::verify::VerifyArtifactArgs {
signature: cmd.signature,
identity_bundle: cmd.identity_bundle,
witness_receipts: cmd.witness_receipts,
witness_keys: cmd.witness_keys,
witness_threshold: cmd.witness_threshold,
verify_commit: false,
ephemeral_anchor: crate::commands::artifact::verify::EphemeralAnchor::Required,
oidc_policy: None,
oidc_policy_did: None,
log_evidence: None,
log_key: None,
expect_signer: None,
require_rooted_signer: false,
},
)
.await
}
}
}
impl crate::commands::executable::ExecutableCommand for UnifiedVerifyCommand {
fn execute(&self, ctx: &crate::config::CliConfig) -> anyhow::Result<()> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(handle_verify_unified(self.clone(), &ctx.env_config))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_verify_target_git_ref() {
assert!(matches!(
parse_verify_target("HEAD"),
VerifyTarget::GitRef(_)
));
assert!(matches!(
parse_verify_target("abc1234"),
VerifyTarget::GitRef(_)
));
assert!(matches!(
parse_verify_target("main..HEAD"),
VerifyTarget::GitRef(_)
));
}
#[test]
fn test_parse_verify_target_stdin() {
assert!(matches!(
parse_verify_target("-"),
VerifyTarget::Attestation(_)
));
}
#[test]
fn test_parse_verify_target_nonexistent_defaults_to_git_ref() {
let target = parse_verify_target("/nonexistent/attestation.json");
assert!(matches!(target, VerifyTarget::GitRef(_)));
}
#[test]
fn test_parse_verify_target_file() {
use std::fs::File;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let f = dir.path().join("attestation.json");
File::create(&f).unwrap();
let target = parse_verify_target(f.to_str().unwrap());
assert!(matches!(target, VerifyTarget::Attestation(_)));
}
#[test]
fn test_parse_verify_target_binary_file_routes_to_artifact() {
use std::fs::File;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let artifact = dir.path().join("release.tar.gz");
File::create(&artifact).unwrap();
let target = parse_verify_target(artifact.to_str().unwrap());
assert!(matches!(target, VerifyTarget::ArtifactFile(_)));
}
#[test]
fn test_parse_verify_target_json_file_routes_to_attestation() {
use std::fs::File;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let attest = dir.path().join("release.auths.json");
File::create(&attest).unwrap();
let target = parse_verify_target(attest.to_str().unwrap());
assert!(matches!(target, VerifyTarget::Attestation(_)));
}
#[test]
fn test_parse_verify_target_plain_json_routes_to_attestation() {
use std::fs::File;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let f = dir.path().join("attestation.json");
File::create(&f).unwrap();
let target = parse_verify_target(f.to_str().unwrap());
assert!(matches!(target, VerifyTarget::Attestation(_)));
}
}