nixfleet_verify_artifact/
main.rs1use std::path::PathBuf;
5use std::process::ExitCode;
6use std::time::Duration;
7
8use chrono::{DateTime, Utc};
9use clap::{Parser, Subcommand};
10use nixfleet_proto::{RolloutId, TrustConfig};
11use nixfleet_reconciler::evidence::{SignatureStatus, verify_canonical_payload};
12use nixfleet_reconciler::{verify_artifact, verify_rollout_manifest};
13
14#[derive(Parser, Debug)]
15#[command(name = "nixfleet-verify-artifact", version)]
16struct Args {
17 #[command(subcommand)]
18 cmd: Cmd,
19}
20
21#[derive(Subcommand, Debug)]
22enum Cmd {
23 Artifact {
25 #[arg(long)]
26 artifact: PathBuf,
27 #[arg(long)]
28 signature: PathBuf,
29 #[arg(long)]
30 trust_file: PathBuf,
31 #[arg(long)]
32 now: DateTime<Utc>,
33 #[arg(long)]
34 freshness_window_secs: u64,
35 },
36 RolloutManifest {
38 #[arg(long)]
39 manifest: PathBuf,
40 #[arg(long)]
41 signature: PathBuf,
42 #[arg(long)]
43 trust_file: PathBuf,
44 #[arg(long)]
45 now: DateTime<Utc>,
46 #[arg(long)]
47 freshness_window_secs: u64,
48 #[arg(long)]
53 rollout_id: String,
54 },
55 Probe {
57 #[arg(long)]
59 payload: PathBuf,
60 #[arg(long)]
62 signature: PathBuf,
63 #[arg(long)]
65 pubkey: PathBuf,
66 },
67}
68
69fn main() -> ExitCode {
70 match Args::parse().cmd {
71 Cmd::Artifact {
72 artifact,
73 signature,
74 trust_file,
75 now,
76 freshness_window_secs,
77 } => run_artifact(artifact, signature, trust_file, now, freshness_window_secs),
78 Cmd::RolloutManifest {
79 manifest,
80 signature,
81 trust_file,
82 now,
83 freshness_window_secs,
84 rollout_id,
85 } => run_rollout_manifest(
86 manifest,
87 signature,
88 trust_file,
89 now,
90 freshness_window_secs,
91 rollout_id,
92 ),
93 Cmd::Probe {
94 payload,
95 signature,
96 pubkey,
97 } => run_probe(payload, signature, pubkey),
98 }
99}
100
101fn run_rollout_manifest(
102 manifest_path: PathBuf,
103 signature_path: PathBuf,
104 trust_file: PathBuf,
105 now: DateTime<Utc>,
106 freshness_window_secs: u64,
107 expected_rollout_id: String,
108) -> ExitCode {
109 let manifest_bytes = match std::fs::read(&manifest_path) {
110 Ok(v) => v,
111 Err(err) => return arg_error(format!("read manifest {}: {err}", manifest_path.display())),
112 };
113 let signature_bytes = match std::fs::read(&signature_path) {
114 Ok(v) => v,
115 Err(err) => {
116 return arg_error(format!(
117 "read signature {}: {err}",
118 signature_path.display()
119 ));
120 }
121 };
122 let trust_raw = match std::fs::read_to_string(&trust_file) {
123 Ok(v) => v,
124 Err(err) => return arg_error(format!("read trust-file {}: {err}", trust_file.display())),
125 };
126 let trust: TrustConfig = match serde_json::from_str(&trust_raw) {
127 Ok(t) => t,
128 Err(err) => return arg_error(format!("parse trust-file {}: {err}", trust_file.display())),
129 };
130 if trust.schema_version != TrustConfig::CURRENT_SCHEMA_VERSION {
131 return arg_error(format!(
132 "trust-file schemaVersion {} unsupported (accepted: {})",
133 trust.schema_version,
134 TrustConfig::CURRENT_SCHEMA_VERSION
135 ));
136 }
137
138 let manifest = match verify_rollout_manifest(
139 &manifest_bytes,
140 &signature_bytes,
141 &trust.ci_release_key.active_keys_at(now),
142 now,
143 Duration::from_secs(freshness_window_secs),
144 trust.ci_release_key.reject_before,
145 ) {
146 Ok(v) => v.into_inner(),
147 Err(err) => {
148 eprintln!("{err}");
149 return ExitCode::from(1);
150 }
151 };
152
153 let parsed_id = RolloutId::new(&manifest.channel, &manifest.channel_ref);
154 if parsed_id.as_str() != expected_rollout_id {
155 eprintln!(
156 "rolloutId mismatch: expected {expected_rollout_id}, parsed {parsed}",
157 parsed = parsed_id.as_str(),
158 );
159 return ExitCode::from(1);
160 }
161
162 println!(
163 "schemaVersion={} channel={} hostSet={} fleetResolvedHash={} rolloutId={}",
164 manifest.schema_version,
165 manifest.channel,
166 manifest.host_set.len(),
167 manifest.fleet_resolved_hash,
168 parsed_id.as_str(),
169 );
170 ExitCode::SUCCESS
171}
172
173fn run_artifact(
174 artifact: PathBuf,
175 signature: PathBuf,
176 trust_file: PathBuf,
177 now: DateTime<Utc>,
178 freshness_window_secs: u64,
179) -> ExitCode {
180 let artifact_bytes = match std::fs::read(&artifact) {
181 Ok(v) => v,
182 Err(err) => return arg_error(format!("read artifact {}: {err}", artifact.display())),
183 };
184 let signature_bytes = match std::fs::read(&signature) {
185 Ok(v) => v,
186 Err(err) => return arg_error(format!("read signature {}: {err}", signature.display())),
187 };
188 let trust_raw = match std::fs::read_to_string(&trust_file) {
189 Ok(v) => v,
190 Err(err) => return arg_error(format!("read trust-file {}: {err}", trust_file.display())),
191 };
192 let trust: TrustConfig = match serde_json::from_str(&trust_raw) {
193 Ok(t) => t,
194 Err(err) => return arg_error(format!("parse trust-file {}: {err}", trust_file.display())),
195 };
196 if trust.schema_version != TrustConfig::CURRENT_SCHEMA_VERSION {
197 return arg_error(format!(
198 "trust-file schemaVersion {} unsupported (accepted: {})",
199 trust.schema_version,
200 TrustConfig::CURRENT_SCHEMA_VERSION
201 ));
202 }
203
204 match verify_artifact(
205 &artifact_bytes,
206 &signature_bytes,
207 &trust.ci_release_key.active_keys_at(now),
208 now,
209 Duration::from_secs(freshness_window_secs),
210 trust.ci_release_key.reject_before,
211 ) {
212 Ok(verified) => {
213 let fleet = verified.into_inner();
214 println!(
215 "schemaVersion={} hosts={}",
216 fleet.schema_version,
217 fleet.hosts.len()
218 );
219 ExitCode::SUCCESS
220 }
221 Err(err) => {
222 eprintln!("{err}");
223 ExitCode::from(1)
224 }
225 }
226}
227
228fn run_probe(payload: PathBuf, signature: PathBuf, pubkey: PathBuf) -> ExitCode {
229 let payload_raw = match std::fs::read_to_string(&payload) {
230 Ok(v) => v,
231 Err(err) => return arg_error(format!("read payload {}: {err}", payload.display())),
232 };
233 let payload_value: serde_json::Value = match serde_json::from_str(&payload_raw) {
234 Ok(v) => v,
235 Err(err) => return arg_error(format!("parse payload {}: {err}", payload.display())),
236 };
237 let canonical = match serde_jcs::to_vec(&payload_value) {
238 Ok(v) => v,
239 Err(err) => return arg_error(format!("canonicalize payload: {err}")),
240 };
241 let sig_b64 = match std::fs::read_to_string(&signature) {
242 Ok(v) => v.trim().to_string(),
243 Err(err) => return arg_error(format!("read signature {}: {err}", signature.display())),
244 };
245 let pubkey_str = match std::fs::read_to_string(&pubkey) {
246 Ok(v) => v.trim().to_string(),
247 Err(err) => return arg_error(format!("read pubkey {}: {err}", pubkey.display())),
248 };
249
250 let status = verify_canonical_payload(&canonical, Some(&pubkey_str), Some(&sig_b64));
251 println!(
252 "{}",
253 serde_json::to_string(&status).expect("SignatureStatus serialize")
254 );
255 match status {
256 SignatureStatus::Verified => ExitCode::SUCCESS,
257 _ => ExitCode::from(1),
258 }
259}
260
261fn arg_error(msg: String) -> ExitCode {
262 eprintln!("{msg}");
263 ExitCode::from(2)
264}