mkit_cli/commands/
verify.rs1use std::io::Write;
25use std::path::PathBuf;
26
27use clap::Parser;
28use mkit_core::object::Object;
29use mkit_core::sign::{verify_commit, verify_remix, verify_tag};
30use mkit_core::store::ObjectStore;
31
32use super::trust_roots;
33use crate::clap_shim;
34use crate::exit;
35
36#[derive(Debug, Parser)]
37#[command(
38 name = "mkit verify",
39 about = "Verify the signature on a commit, remix, or signed tag."
40)]
41struct VerifyOpts {
42 revision: String,
46 #[arg(long)]
49 trusted: bool,
50 #[arg(long, value_name = "PATH")]
52 trust_roots: Option<String>,
53}
54
55#[must_use]
56pub fn run(args: &[String]) -> u8 {
57 let opts = match clap_shim::parse::<VerifyOpts>("mkit verify", args) {
58 Ok(o) => o,
59 Err(code) => return code,
60 };
61 let cwd = match std::env::current_dir() {
62 Ok(p) => p,
63 Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
64 };
65 let layout = match super::resolve_layout(&cwd) {
66 Ok(layout) => layout,
67 Err(code) => return code,
68 };
69 let store = match ObjectStore::open(&layout) {
70 Ok(s) => s,
71 Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
72 };
73 let h = match super::revspec::resolve_revision(&store, &layout, &opts.revision) {
74 Ok(h) => h,
75 Err(e) => return emit_err(&format!("{e}"), exit::DATAERR),
76 };
77 let obj = match store.read_object(&h) {
78 Ok(o) => o,
79 Err(e) => return emit_err(&format!("read: {e}"), exit::NOINPUT),
80 };
81 let signer: [u8; 32] = match &obj {
82 Object::Commit(c) => c.signer,
83 Object::Remix(r) => r.signer,
84 Object::Tag(t) => t.signer,
85 _ => {
86 return emit_err(
87 "object is not a commit, remix, or signed tag",
88 exit::DATAERR,
89 );
90 }
91 };
92 let res = match &obj {
93 Object::Commit(c) => verify_commit(c),
94 Object::Remix(r) => verify_remix(r),
95 Object::Tag(t) => verify_tag(t),
96 _ => unreachable!("checked above"),
97 };
98 let mut stdout = std::io::stdout().lock();
99 if let Err(e) = res {
100 let _ = writeln!(stdout, "bad: {e}");
101 return exit::DATAERR;
102 }
103
104 let want_trust_check = opts.trusted || opts.trust_roots.is_some();
105 if want_trust_check {
106 let trust_path = opts
107 .trust_roots
108 .as_deref()
109 .map_or_else(trust_roots::default_trust_roots_path, PathBuf::from);
110 if let Err(code) = trust_roots::warn_if_unsafe_trust_roots(
111 &trust_path,
112 layout.common_dir(),
113 opts.trust_roots.is_some(),
114 ) {
115 return code;
116 }
117 trust_roots::note_if_missing(&trust_path);
118 let entries = match trust_roots::load_entries(&trust_path) {
119 Ok(e) => e,
120 Err((msg, code)) => return emit_err(&msg, code),
121 };
122 if let Some(keyid) = trust_roots::find_ed25519_signer(&entries, &signer) {
123 let _ = writeln!(
124 stdout,
125 "ok: signature valid, signer trusted ({})",
126 trust_roots::short_keyid(keyid)
127 );
128 return exit::OK;
129 }
130 let _ = writeln!(
131 stdout,
132 "bad: signature valid, but signer {} is not in the trust-roots registry ({})",
133 mkit_core::hash::to_hex_bytes(&signer),
134 trust_path.display()
135 );
136 return exit::DATAERR;
137 }
138
139 let _ = writeln!(stdout, "ok: signature valid");
140 exit::OK
141}
142
143use super::error as emit_err;
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 fn parse_args(args: &[String]) -> Result<VerifyOpts, clap::Error> {
150 let mut full: Vec<String> = vec!["mkit verify".into()];
151 full.extend_from_slice(args);
152 VerifyOpts::try_parse_from(full)
153 }
154
155 #[test]
156 fn parse_args_defaults() {
157 let p = parse_args(&["HEAD".into()]).unwrap();
158 assert_eq!(p.revision, "HEAD");
159 assert!(!p.trusted);
160 assert!(p.trust_roots.is_none());
161 }
162
163 #[test]
164 fn parse_args_accepts_trusted_and_trust_roots() {
165 let p = parse_args(&[
166 "HEAD".into(),
167 "--trusted".into(),
168 "--trust-roots".into(),
169 "/tmp/tr.toml".into(),
170 ])
171 .unwrap();
172 assert!(p.trusted);
173 assert_eq!(p.trust_roots.as_deref(), Some("/tmp/tr.toml"));
174 }
175}