Skip to main content

mkit_cli/commands/
key.rs

1//! `mkit key` keystore management commands.
2
3use std::io::Write as _;
4use std::path::Path;
5
6use clap::{Parser, Subcommand};
7use mkit_keystore::{
8    Algorithm, BackendKind, Capabilities, GenerateOptions, ImportOptions, KeyAttrs, KeyLabel,
9    KeyRef, KeySelector, Keystore, SecretKey, open_backend,
10};
11use zeroize::Zeroize;
12
13use crate::clap_shim;
14use crate::config::{self, Config};
15use crate::exit;
16
17#[derive(Debug, Parser)]
18#[command(name = "mkit key", about = "Manage keystore signing keys.")]
19struct KeyOpts {
20    #[command(subcommand)]
21    command: KeyCommand,
22}
23
24#[derive(Debug, Subcommand)]
25enum KeyCommand {
26    /// Generate a new signing key.
27    Generate(GenerateOpts),
28    /// List keys visible to a backend.
29    List(ListOpts),
30    /// Import 32-byte signing key material.
31    Import(ImportOpts),
32    /// Export extractable signing key material.
33    Export(ExportOpts),
34    /// Delete exactly one signing key.
35    Delete(DeleteOpts),
36}
37
38#[derive(Debug, Parser)]
39#[allow(clippy::struct_excessive_bools)]
40struct GenerateOpts {
41    #[arg(long, value_name = "BACKEND")]
42    backend: Option<String>,
43    #[arg(long, value_name = "LABEL")]
44    label: Option<String>,
45    #[arg(long, value_name = "ALG")]
46    algorithm: Option<String>,
47    #[arg(long, conflicts_with = "non_extractable")]
48    extractable: bool,
49    #[arg(long, conflicts_with = "extractable")]
50    non_extractable: bool,
51    #[arg(long)]
52    device_bound: bool,
53    #[arg(long)]
54    require_user_presence: bool,
55    #[arg(long)]
56    force: bool,
57    #[arg(long)]
58    print_pubkey: bool,
59    /// BLS12-381 threshold (M-of-N): quorum required to recover an
60    /// aggregated signature. Required with
61    /// `--algorithm bls12381-thr`.
62    #[arg(long, value_name = "M")]
63    threshold: Option<u32>,
64    /// BLS12-381 threshold total (N): number of shares the dealer
65    /// produces. Required with `--algorithm bls12381-thr`.
66    #[arg(long, value_name = "N")]
67    total: Option<u32>,
68}
69
70#[derive(Debug, Parser)]
71struct ListOpts {
72    #[arg(long, value_name = "BACKEND")]
73    backend: Option<String>,
74    #[arg(long)]
75    json: bool,
76}
77
78#[derive(Debug, Parser)]
79#[allow(clippy::struct_excessive_bools)]
80struct ImportOpts {
81    #[arg(long, value_name = "ALG")]
82    algorithm: Option<String>,
83    #[arg(long, value_name = "BACKEND")]
84    backend: Option<String>,
85    #[arg(long, value_name = "LABEL")]
86    label: Option<String>,
87    #[arg(long, value_name = "HEX")]
88    hex: Option<String>,
89    #[arg(long, value_name = "PATH")]
90    file: Option<String>,
91    #[arg(long, conflicts_with = "non_extractable")]
92    extractable: bool,
93    #[arg(long, conflicts_with = "extractable")]
94    non_extractable: bool,
95    #[arg(long)]
96    device_bound: bool,
97    #[arg(long)]
98    require_user_presence: bool,
99    #[arg(long)]
100    force: bool,
101}
102
103#[derive(Debug, Parser)]
104struct ExportOpts {
105    #[arg(long, value_name = "BACKEND")]
106    backend: Option<String>,
107    #[arg(long, value_name = "LABEL")]
108    label: Option<String>,
109    #[arg(long, value_name = "ALG")]
110    algorithm: Option<String>,
111    #[arg(long)]
112    unsafe_print_secret: bool,
113}
114
115#[derive(Debug, Parser)]
116struct DeleteOpts {
117    #[arg(long, value_name = "BACKEND")]
118    backend: Option<String>,
119    #[arg(long, value_name = "LABEL")]
120    label: Option<String>,
121    #[arg(long, value_name = "ALG")]
122    algorithm: Option<String>,
123    #[arg(long)]
124    yes: bool,
125}
126
127#[must_use]
128pub fn run(args: &[String]) -> u8 {
129    let opts = match clap_shim::parse::<KeyOpts>("mkit key", args) {
130        Ok(opts) => opts,
131        Err(code) => return code,
132    };
133    match opts.command {
134        KeyCommand::Generate(opts) => generate(opts),
135        KeyCommand::List(opts) => list(opts),
136        KeyCommand::Import(opts) => import(opts),
137        KeyCommand::Export(opts) => export(opts),
138        KeyCommand::Delete(opts) => delete(opts),
139    }
140}
141
142fn generate(opts: GenerateOpts) -> u8 {
143    let cfg = match read_config() {
144        Ok(cfg) => cfg,
145        Err(code) => return code,
146    };
147    let algorithm = match optional_algorithm_or_default(opts.algorithm.as_deref()) {
148        Ok(algorithm) => algorithm,
149        Err(code) => return code,
150    };
151
152    // BLS12-381 threshold needs a trusted-dealer ceremony: one
153    // command produces N encrypted shares stored under
154    // `<label>-<index>`, plus prints the cohort public key + keyid for
155    // registration in trust-roots. The `--threshold` / `--total`
156    // flags are required and `--extractable` / `--non-extractable` /
157    // `--device-bound` / `--require-user-presence` do not apply (the
158    // share record carries its own metadata, not the generic
159    // `KeyAttrs`).
160    #[cfg(feature = "bls-threshold")]
161    if algorithm == Algorithm::Bls12381Threshold {
162        return generate_bls_threshold(&cfg, &opts);
163    }
164
165    let attrs = attrs_from_flags(
166        opts.extractable,
167        opts.non_extractable,
168        opts.device_bound,
169        opts.require_user_presence,
170    );
171    let selection = match selection_for(&cfg, opts.backend, opts.label, Some(algorithm)) {
172        Ok(selection) => selection,
173        Err(code) => return code,
174    };
175    let store = match store_for_backend(selection.backend) {
176        Ok(store) => store,
177        Err(code) => return code,
178    };
179    let label = match KeyLabel::new(selection.label.clone()) {
180        Ok(label) => label,
181        Err(error) => return keystore_error(error),
182    };
183    let Some(generator) = store.generator() else {
184        return keystore_error(mkit_keystore::Error::UnsupportedOperation("generate"));
185    };
186    let signer = match generator.generate(
187        &label,
188        algorithm,
189        attrs,
190        GenerateOptions {
191            overwrite: opts.force,
192        },
193    ) {
194        Ok(signer) => signer,
195        Err(error) => return keystore_error(error),
196    };
197    let metadata = match signer.metadata() {
198        Ok(metadata) => metadata,
199        Err(error) => return keystore_error(error),
200    };
201    print_metadata(&metadata);
202    print_capabilities(&store.capabilities());
203    if opts.print_pubkey {
204        let mut stdout = std::io::stdout().lock();
205        let _ = writeln!(stdout, "{}", metadata.keyid());
206    }
207    exit::OK
208}
209
210/// Trusted-dealer keygen for the BLS12-381 threshold cohort. Stores
211/// `total` encrypted shares under `<label>-<index>` in the chosen
212/// keystore root and prints the aggregated cohort public key + keyid
213/// so the caller can register it in their `trust-roots.toml`.
214///
215/// The planned multi-host distribution ceremony (release-party CLI)
216/// replaces the single-host trusted dealer; the keystore-side API is
217/// unchanged.
218#[cfg(feature = "bls-threshold")]
219#[allow(clippy::too_many_lines)]
220fn generate_bls_threshold(cfg: &Config, opts: &GenerateOpts) -> u8 {
221    use commonware_codec::Encode as _;
222    use mkit_attest::BLS_THRESHOLD_KEYID_PREFIX;
223    use mkit_keystore::SoftwareKeystore;
224
225    let Some(total) = opts.total else {
226        return emit_err(
227            "mkit key generate --algorithm bls12381-thr requires --total N",
228            exit::USAGE,
229        );
230    };
231    let Some(threshold) = opts.threshold else {
232        return emit_err(
233            "mkit key generate --algorithm bls12381-thr requires --threshold M",
234            exit::USAGE,
235        );
236    };
237    let Some(total_nz) = core::num::NonZeroU32::new(total) else {
238        return emit_err("--total must be at least 1", exit::USAGE);
239    };
240    if threshold == 0 || threshold > total {
241        return emit_err(
242            "--threshold M must satisfy 1 <= M <= N (--total)",
243            exit::USAGE,
244        );
245    }
246    // The single-host trusted dealer pins the N3f1 fault model, which
247    // fixes `threshold = ceil(2n/3)`. We accept the caller's
248    // `--threshold` so the CLI surface matches the spec wording, but
249    // we validate it against what the dealer will actually produce —
250    // otherwise the caller would silently get a different threshold
251    // than they asked for.
252    let dealer_threshold = mkit_attest::bls_threshold_for(total);
253    if threshold != dealer_threshold {
254        return emit_err(
255            &format!(
256                "--threshold {threshold} does not match the N3f1 quorum for --total {total} \
257                 (expected {dealer_threshold}); the single-host trusted dealer pins this ratio. \
258                 Arbitrary M-of-N will be accepted once a DKG protocol is wired in."
259            ),
260            exit::USAGE,
261        );
262    }
263
264    let backend = match opts.backend.as_deref() {
265        Some(b) => match parse_backend(b) {
266            Ok(parsed) => parsed,
267            Err(code) => return code,
268        },
269        None => match parse_backend(cfg.key.backend_or_fallback()) {
270            Ok(parsed) => parsed,
271            Err(code) => return code,
272        },
273    };
274    if !matches!(backend, BackendKind::Software) {
275        return emit_err(
276            &format!(
277                "BLS threshold shares are currently stored only by the `software` backend; \
278                 `--backend {backend}` is not supported"
279            ),
280            exit::USAGE,
281        );
282    }
283    let base_label = match opts.label.as_deref() {
284        Some(label) => label.to_owned(),
285        None => {
286            return emit_err(
287                "mkit key generate --algorithm bls12381-thr requires --label <BASE>",
288                exit::USAGE,
289            );
290        }
291    };
292
293    // Run the trusted dealer with the OS RNG. The cohort `Sharing`
294    // gives us the aggregated public key + every holder's `Share`.
295    // `SysRng`'s `TryRng::Error` is fallible (`getrandom::Error`);
296    // `UnwrapErr` gives the infallible `CryptoRng` the dealer expects,
297    // panicking only if the OS random source itself fails.
298    let mut rng = rand_core::UnwrapErr(getrandom::SysRng);
299    let (sharing, shares) = mkit_attest::bls_threshold_trusted_dealer(&mut rng, total_nz);
300    let agg_pubkey = sharing.public().encode().to_vec();
301    let keyid = format!("{BLS_THRESHOLD_KEYID_PREFIX}{}", hex_lower(&agg_pubkey));
302
303    let Ok(store) = SoftwareKeystore::new() else {
304        return emit_err("software keystore root not discoverable", exit::UNAVAILABLE);
305    };
306
307    let mut stored: Vec<(String, u32)> = Vec::with_capacity(shares.len());
308    for (offset, share) in shares.iter().enumerate() {
309        // commonware-cryptography's `Share` has a `u32` index; we use
310        // `offset` (0..total) as the CLI-visible holder index so the
311        // emitted labels are `<base>-0` through `<base>-{N-1}`.
312        let share_index = u32::try_from(offset).unwrap_or(u32::MAX);
313        let label_str = format!("{base_label}-{share_index}");
314        let label = match KeyLabel::new(label_str.clone()) {
315            Ok(l) => l,
316            Err(error) => return keystore_error(error),
317        };
318        let share_bytes = share.encode().to_vec();
319        if let Err(error) = store.store_bls_share(
320            &label,
321            &share_bytes,
322            agg_pubkey.clone(),
323            share_index,
324            threshold,
325            total,
326            keyid.clone(),
327            opts.force,
328        ) {
329            return keystore_error(error);
330        }
331        stored.push((label_str, share_index));
332    }
333
334    // Report. The cohort public key + keyid go to stdout so it's
335    // pipeable into `mkit config` / `trust-roots.toml`; the
336    // human-readable share roster goes to stderr.
337    let mut stderr = std::io::stderr().lock();
338    let _ = writeln!(
339        stderr,
340        "generated {total} BLS12-381 threshold shares ({threshold}-of-{total} quorum)"
341    );
342    for (label, index) in &stored {
343        let _ = writeln!(stderr, "  share {index}: software:{label}");
344    }
345    let _ = writeln!(stderr, "register the cohort public key under this keyid:");
346
347    let mut stdout = std::io::stdout().lock();
348    let _ = writeln!(stdout, "{keyid}");
349    if opts.print_pubkey {
350        let _ = writeln!(stdout, "pubkey_hex = {}", hex_lower(&agg_pubkey));
351    }
352    exit::OK
353}
354
355fn list(opts: ListOpts) -> u8 {
356    let cfg = match read_config() {
357        Ok(cfg) => cfg,
358        Err(code) => return code,
359    };
360    let backend = match parse_backend(
361        &opts
362            .backend
363            .unwrap_or_else(|| cfg.key.backend_or_fallback().to_owned()),
364    ) {
365        Ok(backend) => backend,
366        Err(code) => return code,
367    };
368    let store = match store_for_backend(backend) {
369        Ok(store) => store,
370        Err(code) => return code,
371    };
372    let Some(lister) = store.lister() else {
373        return keystore_error(mkit_keystore::Error::UnsupportedOperation("list"));
374    };
375    let mut keys = match lister.list() {
376        Ok(keys) => keys,
377        Err(error) => return keystore_error(error),
378    };
379    let capabilities = store.capabilities();
380    keys.sort_by(|left, right| {
381        (left.backend(), left.label(), left.algorithm()).cmp(&(
382            right.backend(),
383            right.label(),
384            right.algorithm(),
385        ))
386    });
387    let mut stdout = std::io::stdout().lock();
388    if opts.json {
389        let _ = write!(stdout, "[");
390        for (index, key) in keys.iter().enumerate() {
391            if index > 0 {
392                let _ = write!(stdout, ",");
393            }
394            let _ = write!(
395                stdout,
396                "{{\"backend\":\"{}\",\"label\":\"{}\",\"algorithm\":\"{}\",\"keyid\":\"{}\",\"extractable\":{},\"require_user_presence\":{},\"device_bound\":{},\"capabilities\":{}}}",
397                key.backend(),
398                json_escape(key.label()),
399                key.algorithm(),
400                json_escape(key.keyid()),
401                key.extractable,
402                key.require_user_presence,
403                key.device_bound,
404                json_capabilities(&capabilities)
405            );
406        }
407        let _ = writeln!(stdout, "]");
408    } else {
409        for key in keys {
410            let _ = writeln!(
411                stdout,
412                "{} {} {} {} extractable={} user_presence={} device_bound={} can_generate={} can_import={} can_export={} can_delete={} supports_listing={} supports_user_presence={} supports_device_bound={} supports_non_extractable={}",
413                key.backend(),
414                key.label(),
415                key.algorithm(),
416                key.keyid(),
417                key.extractable,
418                key.require_user_presence,
419                key.device_bound,
420                capabilities.can_generate,
421                capabilities.can_import,
422                capabilities.can_export,
423                capabilities.can_delete,
424                capabilities.supports_listing,
425                capabilities.supports_user_presence,
426                capabilities.supports_device_bound,
427                capabilities.supports_non_extractable
428            );
429        }
430    }
431    exit::OK
432}
433
434fn import(opts: ImportOpts) -> u8 {
435    let cfg = match read_config() {
436        Ok(cfg) => cfg,
437        Err(code) => return code,
438    };
439    let Some(algorithm) = opts.algorithm.as_deref() else {
440        return emit_err("mkit key import requires --algorithm", exit::USAGE);
441    };
442    let algorithm = match parse_algorithm(algorithm) {
443        Ok(algorithm) => algorithm,
444        Err(code) => return code,
445    };
446    if opts.hex.is_some() == opts.file.is_some() {
447        return emit_err(
448            "mkit key import requires exactly one of --hex or --file",
449            exit::USAGE,
450        );
451    }
452    let attrs = attrs_from_flags(
453        opts.extractable,
454        opts.non_extractable,
455        opts.device_bound,
456        opts.require_user_presence,
457    );
458    let selection = match selection_for(&cfg, opts.backend, opts.label, Some(algorithm)) {
459        Ok(selection) => selection,
460        Err(code) => return code,
461    };
462    let mut secret = match (opts.hex, opts.file) {
463        (Some(hex), None) => match parse_secret_hex(&hex) {
464            Ok(secret) => secret,
465            Err(code) => return code,
466        },
467        (None, Some(file)) => match mkit_core::sign::load_raw_32(Path::new(&file)) {
468            Ok(secret) => *secret,
469            Err(error) => return emit_err(&format!("read key file: {error}"), exit::DATAERR),
470        },
471        _ => unreachable!(),
472    };
473    let wrapped = SecretKey::new(algorithm, secret);
474    secret.zeroize();
475    let store = match store_for_backend(selection.backend) {
476        Ok(store) => store,
477        Err(code) => return code,
478    };
479    let label = match KeyLabel::new(selection.label) {
480        Ok(label) => label,
481        Err(error) => return keystore_error(error),
482    };
483    let Some(importer) = store.importer() else {
484        return keystore_error(mkit_keystore::Error::UnsupportedOperation("import"));
485    };
486    let signer = match importer.import(
487        &label,
488        wrapped,
489        attrs,
490        ImportOptions {
491            overwrite: opts.force,
492        },
493    ) {
494        Ok(signer) => signer,
495        Err(error) => return keystore_error(error),
496    };
497    match signer.metadata() {
498        Ok(metadata) => {
499            print_metadata(&metadata);
500            exit::OK
501        }
502        Err(error) => keystore_error(error),
503    }
504}
505
506fn export(opts: ExportOpts) -> u8 {
507    let cfg = match read_config() {
508        Ok(cfg) => cfg,
509        Err(code) => return code,
510    };
511    let algorithm = match optional_algorithm(opts.algorithm.as_deref()) {
512        Ok(algorithm) => algorithm,
513        Err(code) => return code,
514    };
515    if !opts.unsafe_print_secret {
516        return emit_err(
517            "mkit key export requires --unsafe-print-secret",
518            exit::USAGE,
519        );
520    }
521    let selection = match selection_for(&cfg, opts.backend, opts.label, algorithm) {
522        Ok(selection) => selection,
523        Err(code) => return code,
524    };
525    let store = match store_for_backend(selection.backend) {
526        Ok(store) => store,
527        Err(code) => return code,
528    };
529    let selector = match KeySelector::new(selection.label, algorithm) {
530        Ok(selector) => selector,
531        Err(error) => return keystore_error(error),
532    };
533    let Some(exporter) = store.exporter() else {
534        return keystore_error(mkit_keystore::Error::UnsupportedOperation("export"));
535    };
536    let secret = match exporter.export(&selector) {
537        Ok(secret) => secret,
538        Err(error) => return keystore_error(error),
539    };
540    let mut stderr = std::io::stderr().lock();
541    let _ = writeln!(stderr, "warning: printing secret key material to stdout");
542    let mut stdout = std::io::stdout().lock();
543    if let Err(error) = writeln!(stdout, "{}", hex_lower(secret.expose_secret())) {
544        return emit_err(&format!("write exported secret: {error}"), exit::CANTCREAT);
545    }
546    exit::OK
547}
548
549fn delete(opts: DeleteOpts) -> u8 {
550    let cfg = match read_config() {
551        Ok(cfg) => cfg,
552        Err(code) => return code,
553    };
554    let algorithm = match optional_algorithm(opts.algorithm.as_deref()) {
555        Ok(algorithm) => algorithm,
556        Err(code) => return code,
557    };
558    if !opts.yes {
559        return emit_err("mkit key delete requires --yes", exit::USAGE);
560    }
561    let selection = match selection_for(&cfg, opts.backend, opts.label, algorithm) {
562        Ok(selection) => selection,
563        Err(code) => return code,
564    };
565    let store = match store_for_backend(selection.backend) {
566        Ok(store) => store,
567        Err(code) => return code,
568    };
569    let selector = match KeySelector::new(selection.label.clone(), algorithm) {
570        Ok(selector) => selector,
571        Err(error) => return keystore_error(error),
572    };
573    let Some(deleter) = store.deleter() else {
574        return keystore_error(mkit_keystore::Error::UnsupportedOperation("delete"));
575    };
576    match deleter.delete(&selector) {
577        Ok(()) => {
578            let mut stdout = std::io::stdout().lock();
579            let _ = writeln!(stdout, "deleted {}:{}", selection.backend, selection.label);
580            exit::OK
581        }
582        Err(error) => keystore_error(error),
583    }
584}
585
586#[derive(Debug)]
587struct Selection {
588    backend: BackendKind,
589    label: String,
590}
591
592fn selection_for(
593    cfg: &Config,
594    backend: Option<String>,
595    label: Option<String>,
596    algorithm: Option<Algorithm>,
597) -> Result<Selection, u8> {
598    let explicit_backend = match backend {
599        Some(backend) => Some(parse_backend(&backend)?),
600        None => None,
601    };
602    if let Some(label) = label {
603        let backend = match explicit_backend {
604            Some(backend) => backend,
605            None => parse_backend(cfg.key.backend_or_fallback())?,
606        };
607        return Ok(Selection { backend, label });
608    }
609    let algorithm = algorithm.unwrap_or(Algorithm::Ed25519);
610    let configured_ref = configured_ref_explicit(cfg, algorithm);
611    let key_ref = match configured_ref
612        .unwrap_or_else(|| configured_ref_or_fallback(cfg, algorithm))
613        .parse::<KeyRef>()
614    {
615        Ok(key_ref) => key_ref,
616        Err(error) => {
617            return Err(emit_err(
618                &format!("config key ref: {error}"),
619                exit::CONFIG_ERROR,
620            ));
621        }
622    };
623    let backend = match explicit_backend {
624        Some(backend) => backend,
625        None if configured_ref.is_some() => key_ref.backend(),
626        None => parse_backend(cfg.key.backend_or_fallback())?,
627    };
628    Ok(Selection {
629        backend,
630        label: key_ref.label().to_owned(),
631    })
632}
633
634fn configured_ref_explicit(cfg: &Config, algorithm: Algorithm) -> Option<&str> {
635    match algorithm {
636        Algorithm::Ed25519 if !cfg.key.ed25519_ref.is_empty() => Some(cfg.key.ed25519_ref.as_str()),
637        Algorithm::Secp256k1 if !cfg.key.secp256k1_ref.is_empty() => {
638            Some(cfg.key.secp256k1_ref.as_str())
639        }
640        Algorithm::P256 if !cfg.key.p256_ref.is_empty() => Some(cfg.key.p256_ref.as_str()),
641        _ if !cfg.key.default_ref.is_empty() => Some(cfg.key.default_ref.as_str()),
642        _ => None,
643    }
644}
645
646fn configured_ref_or_fallback(cfg: &Config, algorithm: Algorithm) -> &str {
647    match algorithm {
648        Algorithm::Ed25519 => cfg.key.ed25519_ref_or_fallback(),
649        Algorithm::Secp256k1 => cfg.key.secp256k1_ref_or_fallback(),
650        Algorithm::P256 => cfg.key.p256_ref_or_fallback(),
651        // BLS threshold keystore ref defaults to the generic
652        // `key.default_ref` (or the documented `software:default`
653        // fallback). The planned release-party CLI will introduce a
654        // dedicated `key.bls12381_thr_ref` knob; until then the
655        // generic ref is enough.
656        #[cfg(feature = "bls-threshold")]
657        Algorithm::Bls12381Threshold => cfg.key.default_ref_or_fallback(),
658    }
659}
660
661fn parse_backend(backend: &str) -> Result<BackendKind, u8> {
662    match backend.parse::<BackendKind>() {
663        Ok(parsed) => Ok(parsed),
664        Err(error) => Err(emit_err(
665            &format!("key backend: {error}"),
666            exit::CONFIG_ERROR,
667        )),
668    }
669}
670
671fn store_for_backend(backend: BackendKind) -> Result<Box<dyn Keystore>, u8> {
672    open_backend(backend)
673        .map_err(|error| emit_err(&format!("keystore backend: {error}"), exit::UNAVAILABLE))
674}
675
676fn read_config() -> Result<Config, u8> {
677    let cwd = match std::env::current_dir() {
678        Ok(cwd) => cwd,
679        Err(error) => return Err(emit_err(&format!("cwd: {error}"), exit::NOINPUT)),
680    };
681    let layout = super::resolve_layout(&cwd)?;
682    config::read_or_default(&layout)
683        .map_err(|error| emit_err(&format!("config: {error}"), exit::CONFIG_ERROR))
684}
685
686fn parse_algorithm(value: &str) -> Result<Algorithm, u8> {
687    value
688        .parse()
689        .map_err(|error| emit_err(&format!("algorithm: {error}"), exit::USAGE))
690}
691
692fn optional_algorithm(value: Option<&str>) -> Result<Option<Algorithm>, u8> {
693    value.map(parse_algorithm).transpose()
694}
695
696fn optional_algorithm_or_default(value: Option<&str>) -> Result<Algorithm, u8> {
697    match optional_algorithm(value)? {
698        Some(algorithm) => Ok(algorithm),
699        None => Ok(Algorithm::Ed25519),
700    }
701}
702
703#[allow(clippy::fn_params_excessive_bools)]
704fn attrs_from_flags(
705    extractable: bool,
706    non_extractable: bool,
707    device_bound: bool,
708    require_user_presence: bool,
709) -> KeyAttrs {
710    let mut attrs = KeyAttrs::default();
711    if extractable {
712        attrs.extractable = true;
713    }
714    if non_extractable {
715        attrs.extractable = false;
716    }
717    attrs.device_bound = device_bound;
718    attrs.require_user_presence = require_user_presence;
719    attrs
720}
721
722fn print_metadata(metadata: &mkit_keystore::KeyMetadata) {
723    let mut stdout = std::io::stdout().lock();
724    let _ = writeln!(stdout, "backend = {}", metadata.backend());
725    let _ = writeln!(stdout, "label = {}", metadata.label());
726    let _ = writeln!(stdout, "algorithm = {}", metadata.algorithm());
727    let _ = writeln!(stdout, "public_key = {}", hex_lower(metadata.public_key()));
728    let _ = writeln!(stdout, "keyid = {}", metadata.keyid());
729    let _ = writeln!(stdout, "extractable = {}", metadata.extractable);
730    let _ = writeln!(
731        stdout,
732        "require_user_presence = {}",
733        metadata.require_user_presence
734    );
735    let _ = writeln!(stdout, "device_bound = {}", metadata.device_bound);
736}
737
738fn print_capabilities(capabilities: &Capabilities) {
739    let mut stdout = std::io::stdout().lock();
740    let _ = writeln!(stdout, "capabilities.backend = {}", capabilities.backend);
741    let _ = writeln!(
742        stdout,
743        "capabilities.algorithms = {}",
744        algorithms_csv(capabilities)
745    );
746    let _ = writeln!(
747        stdout,
748        "capabilities.can_generate = {}",
749        capabilities.can_generate
750    );
751    let _ = writeln!(
752        stdout,
753        "capabilities.can_import = {}",
754        capabilities.can_import
755    );
756    let _ = writeln!(
757        stdout,
758        "capabilities.can_export = {}",
759        capabilities.can_export
760    );
761    let _ = writeln!(
762        stdout,
763        "capabilities.can_delete = {}",
764        capabilities.can_delete
765    );
766    let _ = writeln!(
767        stdout,
768        "capabilities.supports_listing = {}",
769        capabilities.supports_listing
770    );
771    let _ = writeln!(
772        stdout,
773        "capabilities.supports_user_presence = {}",
774        capabilities.supports_user_presence
775    );
776    let _ = writeln!(
777        stdout,
778        "capabilities.supports_device_bound = {}",
779        capabilities.supports_device_bound
780    );
781    let _ = writeln!(
782        stdout,
783        "capabilities.supports_non_extractable = {}",
784        capabilities.supports_non_extractable
785    );
786}
787
788fn algorithms_csv(capabilities: &Capabilities) -> String {
789    capabilities
790        .algorithms
791        .iter()
792        .map(ToString::to_string)
793        .collect::<Vec<_>>()
794        .join(",")
795}
796
797fn json_capabilities(capabilities: &Capabilities) -> String {
798    let algorithms = capabilities
799        .algorithms
800        .iter()
801        .map(|algorithm| format!("\"{algorithm}\""))
802        .collect::<Vec<_>>()
803        .join(",");
804    format!(
805        "{{\"backend\":\"{}\",\"algorithms\":[{}],\"can_generate\":{},\"can_import\":{},\"can_export\":{},\"can_delete\":{},\"supports_listing\":{},\"supports_user_presence\":{},\"supports_device_bound\":{},\"supports_non_extractable\":{}}}",
806        capabilities.backend,
807        algorithms,
808        capabilities.can_generate,
809        capabilities.can_import,
810        capabilities.can_export,
811        capabilities.can_delete,
812        capabilities.supports_listing,
813        capabilities.supports_user_presence,
814        capabilities.supports_device_bound,
815        capabilities.supports_non_extractable
816    )
817}
818
819fn parse_secret_hex(hex: &str) -> Result<[u8; 32], u8> {
820    if hex.len() != 64 {
821        return Err(emit_err(
822            "--hex must be exactly 64 hex characters",
823            exit::DATAERR,
824        ));
825    }
826    let mut out = [0u8; 32];
827    for (index, chunk) in hex.as_bytes().chunks_exact(2).enumerate() {
828        let high = hex_value(chunk[0])?;
829        let low = hex_value(chunk[1])?;
830        out[index] = (high << 4) | low;
831    }
832    Ok(out)
833}
834
835fn hex_value(byte: u8) -> Result<u8, u8> {
836    match byte {
837        b'0'..=b'9' => Ok(byte - b'0'),
838        b'a'..=b'f' => Ok(byte - b'a' + 10),
839        b'A'..=b'F' => Ok(byte - b'A' + 10),
840        _ => Err(emit_err("invalid hex character", exit::DATAERR)),
841    }
842}
843
844fn hex_lower(bytes: &[u8]) -> String {
845    const HEX: &[u8; 16] = b"0123456789abcdef";
846    let mut out = String::with_capacity(bytes.len() * 2);
847    for byte in bytes {
848        out.push(HEX[(byte >> 4) as usize] as char);
849        out.push(HEX[(byte & 0x0f) as usize] as char);
850    }
851    out
852}
853
854fn json_escape(value: &str) -> String {
855    let mut out = String::with_capacity(value.len());
856    for ch in value.chars() {
857        match ch {
858            '\\' => out.push_str("\\\\"),
859            '"' => out.push_str("\\\""),
860            '\n' => out.push_str("\\n"),
861            '\r' => out.push_str("\\r"),
862            '\t' => out.push_str("\\t"),
863            ch => out.push(ch),
864        }
865    }
866    out
867}
868
869#[allow(clippy::needless_pass_by_value)]
870fn keystore_error(error: mkit_keystore::Error) -> u8 {
871    emit_err(&format!("keystore: {error}"), exit::DATAERR)
872}
873
874use super::error as emit_err;
875
876#[cfg(test)]
877mod tests {
878    use super::*;
879
880    fn parse(args: &[&str]) -> Result<KeyOpts, clap::Error> {
881        KeyOpts::try_parse_from(
882            std::iter::once("mkit key".to_owned()).chain(args.iter().map(|arg| (*arg).to_owned())),
883        )
884    }
885
886    #[test]
887    fn generate_accepts_equals_options() {
888        let opts = parse(&["generate", "--backend=software-raw", "--algorithm=ed25519"]).unwrap();
889        let KeyCommand::Generate(generate) = opts.command else {
890            panic!("expected generate command");
891        };
892        assert_eq!(generate.backend.as_deref(), Some("software-raw"));
893        assert_eq!(generate.algorithm.as_deref(), Some("ed25519"));
894    }
895
896    #[test]
897    fn import_accepts_equals_options() {
898        let secret = "03".repeat(32);
899        let opts = parse(&["import", "--algorithm=ed25519", &format!("--hex={secret}")]).unwrap();
900        let KeyCommand::Import(import) = opts.command else {
901            panic!("expected import command");
902        };
903        assert_eq!(import.algorithm.as_deref(), Some("ed25519"));
904        assert_eq!(import.hex.as_deref(), Some(secret.as_str()));
905    }
906
907    #[test]
908    fn extractable_flags_conflict() {
909        assert!(parse(&["generate", "--extractable", "--non-extractable"]).is_err());
910        assert!(parse(&["import", "--extractable", "--non-extractable"]).is_err());
911    }
912}