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