Skip to main content

mkit_cli/commands/
keygen.rs

1//! `mkit keygen` — generate a fresh signing key for one of the three
2//! attestation algorithms.
3//!
4//! ```text
5//! mkit keygen [--algorithm ed25519|secp256k1|p256] [--force] [--print-pubkey]
6//! ```
7//!
8//! Behaviour:
9//!
10//! * `--algorithm` defaults to `ed25519` (backward-compat with the
11//!   original single-algorithm command). `ed25519` writes to
12//!   `.mkit/keys/default.key`; `secp256k1` / `p256` write to the path
13//!   configured via `attest.<algo>_key_path` (default
14//!   `.mkit/keys/<algo>.key`).
15//! * `--force` overwrites an existing key file; without it, refuse with
16//!   a clear error.
17//! * `--print-pubkey` emits the canonical keyid on stdout so downstream
18//!   tooling can populate trust-roots entries without needing to parse
19//!   key files:
20//!     * `ed25519:<64-hex>`
21//!     * `secp256k1:<66-hex>` (33-byte compressed SEC1)
22//!     * `p256:<66-hex>`     (33-byte compressed SEC1)
23//!
24//! Key-file layout mirrors what the repo-key signer factory loads:
25//! a raw 32-byte secret, mode `0600` on Unix. The mode is set on the
26//! open file handle (not via a post-write `chmod`/`rename`) so the
27//! secret is never briefly world-readable in a TOCTOU window between
28//! creating the file and tightening its permissions.
29
30use std::io::Write;
31use std::path::Path;
32
33use clap::Parser;
34use mkit_attest::Algorithm;
35use mkit_core::sign::{KeyPair, load_raw_32, save_key, save_raw_32};
36use zeroize::Zeroizing;
37
38use crate::clap_shim;
39use crate::commands::attest_factory;
40use crate::exit;
41use crate::format;
42
43#[derive(Debug, Parser)]
44#[command(name = "mkit keygen", about = "Generate a fresh signing key.")]
45struct KeygenOpts {
46    /// Algorithm: `ed25519` (default), `secp256k1`, or `p256`.
47    #[arg(long)]
48    algorithm: Option<String>,
49    /// Overwrite an existing key file at the target path.
50    #[arg(long)]
51    force: bool,
52    /// Emit the canonical keyid on stdout for trust-roots entries.
53    #[arg(long)]
54    print_pubkey: bool,
55}
56
57#[must_use]
58pub fn run(args: &[String]) -> u8 {
59    let parsed = match clap_shim::parse::<KeygenOpts>("mkit keygen", args) {
60        Ok(o) => o,
61        Err(code) => return code,
62    };
63
64    let cwd = match std::env::current_dir() {
65        Ok(p) => p,
66        Err(e) => return emit_err(&format!("cannot read cwd: {e}"), exit::NOINPUT),
67    };
68
69    let alg_str = parsed
70        .algorithm
71        .clone()
72        .unwrap_or_else(|| "ed25519".to_owned());
73    let Ok(algorithm) = attest_factory::parse_algorithm(&alg_str) else {
74        return emit_err(
75            &format!("unknown algorithm '{alg_str}' — expected one of: ed25519, secp256k1, p256"),
76            exit::USAGE,
77        );
78    };
79
80    // Resolve the target key path from config. Each algorithm reads
81    // its own config knob so `mkit keygen`, `mkit commit`, and
82    // `mkit attest` agree on where the key lives — a user with
83    // `signing_key = /home/u/.mkit/global.key` in their user-scoped
84    // config gets that path written/read consistently.
85    let layout = match super::resolve_layout(&cwd) {
86        Ok(layout) => layout,
87        Err(code) => return code,
88    };
89    let cfg = match crate::config::read_or_default(&layout) {
90        Ok(c) => c,
91        Err(e) => return emit_err(&format!("config: {e}"), exit::CONFIG_ERROR),
92    };
93    let rel_path: &str = match algorithm {
94        Algorithm::Ed25519 => {
95            if cfg.signing_key.is_empty() {
96                crate::config::DEFAULT_SIGNING_KEY
97            } else {
98                cfg.signing_key.as_str()
99            }
100        }
101        Algorithm::Secp256k1 => cfg.attest.secp256k1_key_path_or_default(),
102        Algorithm::P256 => cfg.attest.p256_key_path_or_default(),
103        #[cfg(feature = "bls-threshold")]
104        Algorithm::Bls12381Threshold => {
105            return emit_err(
106                "BLS threshold keygen is not supported here; use `mkit key generate` (issue #160)",
107                exit::UNAVAILABLE,
108            );
109        }
110    };
111    let key_path = match crate::config::resolve_key_path(&layout, rel_path) {
112        Ok(p) => p,
113        Err(e) => return emit_err(&format!("{e}"), exit::CONFIG_ERROR),
114    };
115
116    match algorithm {
117        Algorithm::Ed25519 => run_ed25519(&key_path, parsed.force, parsed.print_pubkey),
118        Algorithm::Secp256k1 => run_secp256k1(&key_path, parsed.force, parsed.print_pubkey),
119        Algorithm::P256 => run_p256(&key_path, parsed.force, parsed.print_pubkey),
120        #[cfg(feature = "bls-threshold")]
121        Algorithm::Bls12381Threshold => emit_err(
122            "BLS threshold keygen is not supported here; use `mkit key generate` (issue #160)",
123            exit::UNAVAILABLE,
124        ),
125    }
126}
127
128fn run_ed25519(key_path: &Path, force: bool, print_pubkey: bool) -> u8 {
129    let exists = key_path.exists();
130    // When `--print-pubkey` is set and the key already exists, load it
131    // and print — acts as an idempotent "show me the pubkey" path that
132    // downstream tooling can script against.
133    if exists && print_pubkey && !force {
134        let kp = match mkit_core::sign::load_key(key_path) {
135            Ok(kp) => kp,
136            Err(e) => return emit_err(&format!("load key: {e}"), exit::GENERAL_ERROR),
137        };
138        print_ed25519_pubkey(&kp);
139        return exit::OK;
140    }
141    if exists && !force {
142        return emit_err(
143            &format!(
144                "signing key already exists: {} (pass --force to overwrite)",
145                key_path.display()
146            ),
147            exit::GENERAL_ERROR,
148        );
149    }
150    let kp = match KeyPair::generate() {
151        Ok(kp) => kp,
152        Err(e) => return emit_err(&format!("rng failed: {e}"), exit::GENERAL_ERROR),
153    };
154    if let Err(e) = save_key(key_path, &kp) {
155        return emit_err(&format!("save key: {e}"), exit::CANTCREAT);
156    }
157    let pk_hex = hex32(&kp.public.0);
158    {
159        let mut stderr = std::io::stderr().lock();
160        let _ = writeln!(stderr, "generated signing key at {}", key_path.display());
161        let _ = writeln!(stderr, "public:  ed25519:{pk_hex}");
162        let _ = writeln!(
163            stderr,
164            "identity: {}",
165            format::short_identity(&mkit_core::Identity::ed25519(kp.public.0))
166        );
167    }
168    if print_pubkey {
169        // The key string IS the data when --print-pubkey is set.
170        let mut stdout = std::io::stdout().lock();
171        let _ = writeln!(stdout, "ed25519:{pk_hex}");
172    }
173    exit::OK
174}
175
176fn run_secp256k1(key_path: &Path, force: bool, print_pubkey: bool) -> u8 {
177    // Idempotent read path for --print-pubkey.
178    if key_path.exists() && print_pubkey && !force {
179        let secret = match load_raw_32(key_path) {
180            Ok(s) => s,
181            Err(e) => return emit_err(&format!("load key: {e}"), exit::GENERAL_ERROR),
182        };
183        // Borrow through `from_seed_zeroizing` so no plain `[u8; 32]`
184        // is materialised on this frame — the constructor copies
185        // through a scratch buffer that it scrubs itself.
186        let signer = match mkit_attest::signer_k256::Secp256k1Signer::from_seed_zeroizing(&secret) {
187            Ok(s) => s,
188            Err(e) => return emit_err(&format!("invalid secp256k1 key: {e}"), exit::GENERAL_ERROR),
189        };
190        let pk = signer.public_key_sec1();
191        let mut stdout = std::io::stdout().lock();
192        let _ = writeln!(stdout, "secp256k1:{}", hex_lower(&pk));
193        return exit::OK;
194    }
195    if key_path.exists() && !force {
196        return emit_err(
197            &format!(
198                "signing key already exists: {} (pass --force to overwrite)",
199                key_path.display()
200            ),
201            exit::GENERAL_ERROR,
202        );
203    }
204
205    // Generate a valid secp256k1 scalar. Sampling uniformly from a 32-byte
206    // space: the probability of hitting zero or >= n on a single draw is
207    // ~2^-128 for the >= n case and 2^-256 for zero; a small retry loop
208    // just lets `Secp256k1Signer::new` be the authoritative validator.
209    let (signer, secret) = match generate_secp256k1_signer() {
210        Ok(x) => x,
211        Err(e) => return emit_err(&e, exit::GENERAL_ERROR),
212    };
213    if let Err(e) = save_raw_32(key_path, &secret) {
214        return emit_err(&format!("save key: {e}"), exit::CANTCREAT);
215    }
216    drop(secret);
217
218    let pk = signer.public_key_sec1();
219    {
220        let mut stderr = std::io::stderr().lock();
221        let _ = writeln!(stderr, "generated signing key at {}", key_path.display());
222        let _ = writeln!(stderr, "public:  secp256k1:{}", hex_lower(&pk));
223    }
224    if print_pubkey {
225        let mut stdout = std::io::stdout().lock();
226        let _ = writeln!(stdout, "secp256k1:{}", hex_lower(&pk));
227    }
228    exit::OK
229}
230
231fn run_p256(key_path: &Path, force: bool, print_pubkey: bool) -> u8 {
232    if key_path.exists() && print_pubkey && !force {
233        let secret = match load_raw_32(key_path) {
234            Ok(s) => s,
235            Err(e) => return emit_err(&format!("load key: {e}"), exit::GENERAL_ERROR),
236        };
237        // Borrow-through pattern matches the secp256k1 arm above.
238        let signer = match mkit_attest::signer_p256::P256Signer::from_seed_zeroizing(&secret) {
239            Ok(s) => s,
240            Err(e) => return emit_err(&format!("invalid p256 key: {e}"), exit::GENERAL_ERROR),
241        };
242        let pk = signer.public_key_sec1();
243        let mut stdout = std::io::stdout().lock();
244        let _ = writeln!(stdout, "p256:{}", hex_lower(&pk));
245        return exit::OK;
246    }
247    if key_path.exists() && !force {
248        return emit_err(
249            &format!(
250                "signing key already exists: {} (pass --force to overwrite)",
251                key_path.display()
252            ),
253            exit::GENERAL_ERROR,
254        );
255    }
256
257    let (signer, secret) = match generate_p256_signer() {
258        Ok(x) => x,
259        Err(e) => return emit_err(&e, exit::GENERAL_ERROR),
260    };
261    if let Err(e) = save_raw_32(key_path, &secret) {
262        return emit_err(&format!("save key: {e}"), exit::CANTCREAT);
263    }
264    drop(secret);
265
266    let pk = signer.public_key_sec1();
267    {
268        let mut stderr = std::io::stderr().lock();
269        let _ = writeln!(stderr, "generated signing key at {}", key_path.display());
270        let _ = writeln!(stderr, "public:  p256:{}", hex_lower(&pk));
271    }
272    if print_pubkey {
273        let mut stdout = std::io::stdout().lock();
274        let _ = writeln!(stdout, "p256:{}", hex_lower(&pk));
275    }
276    exit::OK
277}
278
279/// Draw a 32-byte secret until the curve's `SigningKey::from_bytes`
280/// accepts it (rejects zero and values >= n). The retry loop is
281/// effectively one-shot; 256 iterations is an upper bound that would
282/// require astronomical RNG bias to reach.
283///
284/// The returned secret lives inside a [`Zeroizing`] wrapper so it is
285/// scrubbed when the keygen command finishes — the only persistent
286/// copy is the one written to `path` at mode 0600. Note: the signer
287/// constructor takes the secret by value (Copy), so we must pass a
288/// fresh copy in; the wrapper here scrubs the local buffer after.
289fn generate_secp256k1_signer() -> Result<
290    (
291        mkit_attest::signer_k256::Secp256k1Signer,
292        Zeroizing<[u8; 32]>,
293    ),
294    String,
295> {
296    for _ in 0..256 {
297        let mut buf: Zeroizing<[u8; 32]> = Zeroizing::new([0u8; 32]);
298        getrandom::fill(buf.as_mut_slice()).map_err(|e| format!("rng failed: {e}"))?;
299        if let Ok(signer) = mkit_attest::signer_k256::Secp256k1Signer::from_seed_zeroizing(&buf) {
300            return Ok((signer, buf));
301        }
302        // `buf` drops here, scrubbing the rejected scalar.
303    }
304    Err("rng produced 256 consecutive invalid secp256k1 scalars (impossible in practice)".into())
305}
306
307fn generate_p256_signer()
308-> Result<(mkit_attest::signer_p256::P256Signer, Zeroizing<[u8; 32]>), String> {
309    for _ in 0..256 {
310        let mut buf: Zeroizing<[u8; 32]> = Zeroizing::new([0u8; 32]);
311        getrandom::fill(buf.as_mut_slice()).map_err(|e| format!("rng failed: {e}"))?;
312        if let Ok(signer) = mkit_attest::signer_p256::P256Signer::from_seed_zeroizing(&buf) {
313            return Ok((signer, buf));
314        }
315    }
316    Err("rng produced 256 consecutive invalid p256 scalars (impossible in practice)".into())
317}
318
319fn print_ed25519_pubkey(kp: &KeyPair) {
320    let mut stdout = std::io::stdout().lock();
321    let _ = writeln!(stdout, "ed25519:{}", hex32(&kp.public.0));
322}
323
324// -- hex helpers --
325
326fn hex32(bytes: &[u8; 32]) -> String {
327    let h: mkit_core::hash::Hash = *bytes;
328    mkit_core::hash::to_hex(&h)
329}
330
331fn hex_lower(b: &[u8]) -> String {
332    const HEX: &[u8; 16] = b"0123456789abcdef";
333    let mut s = String::with_capacity(b.len() * 2);
334    for byte in b {
335        s.push(HEX[(byte >> 4) as usize] as char);
336        s.push(HEX[(byte & 0x0F) as usize] as char);
337    }
338    s
339}
340
341use super::error as emit_err;
342
343#[cfg(test)]
344mod tests {
345    use clap::Parser;
346
347    use super::KeygenOpts;
348
349    #[test]
350    fn parse_defaults() {
351        let p = KeygenOpts::try_parse_from(["mkit keygen"]).unwrap();
352        assert!(p.algorithm.is_none());
353        assert!(!p.force);
354        assert!(!p.print_pubkey);
355    }
356
357    #[test]
358    fn parse_all_flags() {
359        let p = KeygenOpts::try_parse_from([
360            "mkit keygen",
361            "--algorithm",
362            "secp256k1",
363            "--force",
364            "--print-pubkey",
365        ])
366        .unwrap();
367        assert_eq!(p.algorithm.as_deref(), Some("secp256k1"));
368        assert!(p.force);
369        assert!(p.print_pubkey);
370    }
371
372    #[test]
373    fn parse_unknown_flag_rejected() {
374        assert!(KeygenOpts::try_parse_from(["mkit keygen", "--bogus"]).is_err());
375    }
376}