Skip to main content

mkit_cli/commands/
verify.rs

1//! `mkit verify <rev>` — verify the signature on a commit, remix, or
2//! signed tag.
3//!
4//! ```text
5//! mkit verify <rev> [--trusted] [--trust-roots <path>]
6//! ```
7//!
8//! By default `mkit verify` only proves that the object's own embedded
9//! `signer` public key produced the attached signature — it does NOT
10//! check that key against any allow-list, so a signature from a freshly
11//! generated attacker key verifies exactly the same as one from a key
12//! the caller actually trusts (issue #693). Passing `--trusted` (or
13//! `--trust-roots <path>`) additionally cross-checks `signer` against
14//! the trust-roots registry `mkit trust add/list/remove` manages
15//! (`commands/trust_roots.rs`), failing closed — exit code
16//! [`exit::DATAERR`] — when the signer is not on the list, even if the
17//! cryptographic signature itself is valid.
18//!
19//! `--trust-roots` defaults to the user-scoped
20//! `$XDG_CONFIG_HOME/mkit/trust-roots.toml`; an in-repo path is refused
21//! unless passed explicitly (same hostile-clone defense as
22//! `verify-attest`, see `docs/THREAT-MODEL.md` §5).
23
24use 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 to verify: an object hash (full or short), a branch /
43    /// tag name, or `HEAD`. A tag name resolves to its annotated-tag
44    /// object when one exists.
45    revision: String,
46    /// Also cross-check the signer against the trust-roots registry
47    /// (default path), failing closed on an unlisted signer.
48    #[arg(long)]
49    trusted: bool,
50    /// Path to a trust-roots TOML file. Implies `--trusted`.
51    #[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}