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/// Phase 3 (release-party CLI) replaces the single-host trusted dealer
216/// with a multi-host distribution ceremony; 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 Phase 1 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 Phase 1 trusted dealer pins this ratio. \
258                 Phase 3 will accept arbitrary M-of-N 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 stored by the `software` backend in Phase 2; \
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    let mut rng = rand_core::OsRng;
296    let (sharing, shares) = mkit_attest::bls_threshold_trusted_dealer(&mut rng, total_nz);
297    let agg_pubkey = sharing.public().encode().to_vec();
298    let keyid = format!("{BLS_THRESHOLD_KEYID_PREFIX}{}", hex_lower(&agg_pubkey));
299
300    let Ok(store) = SoftwareKeystore::new() else {
301        return emit_err("software keystore root not discoverable", exit::UNAVAILABLE);
302    };
303
304    let mut stored: Vec<(String, u32)> = Vec::with_capacity(shares.len());
305    for (offset, share) in shares.iter().enumerate() {
306        // commonware-cryptography's `Share` has a `u32` index; we use
307        // `offset` (0..total) as the CLI-visible holder index so the
308        // emitted labels are `<base>-0` through `<base>-{N-1}`.
309        let share_index = u32::try_from(offset).unwrap_or(u32::MAX);
310        let label_str = format!("{base_label}-{share_index}");
311        let label = match KeyLabel::new(label_str.clone()) {
312            Ok(l) => l,
313            Err(error) => return keystore_error(error),
314        };
315        let share_bytes = share.encode().to_vec();
316        if let Err(error) = store.store_bls_share(
317            &label,
318            &share_bytes,
319            agg_pubkey.clone(),
320            share_index,
321            threshold,
322            total,
323            keyid.clone(),
324            opts.force,
325        ) {
326            return keystore_error(error);
327        }
328        stored.push((label_str, share_index));
329    }
330
331    // Report. The cohort public key + keyid go to stdout so it's
332    // pipeable into `mkit config` / `trust-roots.toml`; the
333    // human-readable share roster goes to stderr.
334    let mut stderr = std::io::stderr().lock();
335    let _ = writeln!(
336        stderr,
337        "generated {total} BLS12-381 threshold shares ({threshold}-of-{total} quorum)"
338    );
339    for (label, index) in &stored {
340        let _ = writeln!(stderr, "  share {index}: software:{label}");
341    }
342    let _ = writeln!(stderr, "register the cohort public key under this keyid:");
343
344    let mut stdout = std::io::stdout().lock();
345    let _ = writeln!(stdout, "{keyid}");
346    if opts.print_pubkey {
347        let _ = writeln!(stdout, "pubkey_hex = {}", hex_lower(&agg_pubkey));
348    }
349    exit::OK
350}
351
352fn list(opts: ListOpts) -> u8 {
353    let cfg = match read_config() {
354        Ok(cfg) => cfg,
355        Err(code) => return code,
356    };
357    let backend = match parse_backend(
358        &opts
359            .backend
360            .unwrap_or_else(|| cfg.key.backend_or_fallback().to_owned()),
361    ) {
362        Ok(backend) => backend,
363        Err(code) => return code,
364    };
365    let store = match store_for_backend(backend) {
366        Ok(store) => store,
367        Err(code) => return code,
368    };
369    let Some(lister) = store.lister() else {
370        return keystore_error(mkit_keystore::Error::UnsupportedOperation("list"));
371    };
372    let mut keys = match lister.list() {
373        Ok(keys) => keys,
374        Err(error) => return keystore_error(error),
375    };
376    let capabilities = store.capabilities();
377    keys.sort_by(|left, right| {
378        (left.backend(), left.label(), left.algorithm()).cmp(&(
379            right.backend(),
380            right.label(),
381            right.algorithm(),
382        ))
383    });
384    let mut stdout = std::io::stdout().lock();
385    if opts.json {
386        let _ = write!(stdout, "[");
387        for (index, key) in keys.iter().enumerate() {
388            if index > 0 {
389                let _ = write!(stdout, ",");
390            }
391            let _ = write!(
392                stdout,
393                "{{\"backend\":\"{}\",\"label\":\"{}\",\"algorithm\":\"{}\",\"keyid\":\"{}\",\"extractable\":{},\"require_user_presence\":{},\"device_bound\":{},\"capabilities\":{}}}",
394                key.backend(),
395                json_escape(key.label()),
396                key.algorithm(),
397                json_escape(key.keyid()),
398                key.extractable,
399                key.require_user_presence,
400                key.device_bound,
401                json_capabilities(&capabilities)
402            );
403        }
404        let _ = writeln!(stdout, "]");
405    } else {
406        for key in keys {
407            let _ = writeln!(
408                stdout,
409                "{} {} {} {} extractable={} user_presence={} device_bound={} can_generate={} can_import={} can_export={} can_delete={} supports_listing={} supports_user_presence={} supports_device_bound={} supports_non_extractable={}",
410                key.backend(),
411                key.label(),
412                key.algorithm(),
413                key.keyid(),
414                key.extractable,
415                key.require_user_presence,
416                key.device_bound,
417                capabilities.can_generate,
418                capabilities.can_import,
419                capabilities.can_export,
420                capabilities.can_delete,
421                capabilities.supports_listing,
422                capabilities.supports_user_presence,
423                capabilities.supports_device_bound,
424                capabilities.supports_non_extractable
425            );
426        }
427    }
428    exit::OK
429}
430
431fn import(opts: ImportOpts) -> u8 {
432    let cfg = match read_config() {
433        Ok(cfg) => cfg,
434        Err(code) => return code,
435    };
436    let Some(algorithm) = opts.algorithm.as_deref() else {
437        return emit_err("mkit key import requires --algorithm", exit::USAGE);
438    };
439    let algorithm = match parse_algorithm(algorithm) {
440        Ok(algorithm) => algorithm,
441        Err(code) => return code,
442    };
443    if opts.hex.is_some() == opts.file.is_some() {
444        return emit_err(
445            "mkit key import requires exactly one of --hex or --file",
446            exit::USAGE,
447        );
448    }
449    let attrs = attrs_from_flags(
450        opts.extractable,
451        opts.non_extractable,
452        opts.device_bound,
453        opts.require_user_presence,
454    );
455    let selection = match selection_for(&cfg, opts.backend, opts.label, Some(algorithm)) {
456        Ok(selection) => selection,
457        Err(code) => return code,
458    };
459    let mut secret = match (opts.hex, opts.file) {
460        (Some(hex), None) => match parse_secret_hex(&hex) {
461            Ok(secret) => secret,
462            Err(code) => return code,
463        },
464        (None, Some(file)) => match mkit_core::sign::load_raw_32(Path::new(&file)) {
465            Ok(secret) => *secret,
466            Err(error) => return emit_err(&format!("read key file: {error}"), exit::DATAERR),
467        },
468        _ => unreachable!(),
469    };
470    let wrapped = SecretKey::new(algorithm, secret);
471    secret.zeroize();
472    let store = match store_for_backend(selection.backend) {
473        Ok(store) => store,
474        Err(code) => return code,
475    };
476    let label = match KeyLabel::new(selection.label) {
477        Ok(label) => label,
478        Err(error) => return keystore_error(error),
479    };
480    let Some(importer) = store.importer() else {
481        return keystore_error(mkit_keystore::Error::UnsupportedOperation("import"));
482    };
483    let signer = match importer.import(
484        &label,
485        wrapped,
486        attrs,
487        ImportOptions {
488            overwrite: opts.force,
489        },
490    ) {
491        Ok(signer) => signer,
492        Err(error) => return keystore_error(error),
493    };
494    match signer.metadata() {
495        Ok(metadata) => {
496            print_metadata(&metadata);
497            exit::OK
498        }
499        Err(error) => keystore_error(error),
500    }
501}
502
503fn export(opts: ExportOpts) -> u8 {
504    let cfg = match read_config() {
505        Ok(cfg) => cfg,
506        Err(code) => return code,
507    };
508    let algorithm = match optional_algorithm(opts.algorithm.as_deref()) {
509        Ok(algorithm) => algorithm,
510        Err(code) => return code,
511    };
512    if !opts.unsafe_print_secret {
513        return emit_err(
514            "mkit key export requires --unsafe-print-secret",
515            exit::USAGE,
516        );
517    }
518    let selection = match selection_for(&cfg, opts.backend, opts.label, algorithm) {
519        Ok(selection) => selection,
520        Err(code) => return code,
521    };
522    let store = match store_for_backend(selection.backend) {
523        Ok(store) => store,
524        Err(code) => return code,
525    };
526    let selector = match KeySelector::new(selection.label, algorithm) {
527        Ok(selector) => selector,
528        Err(error) => return keystore_error(error),
529    };
530    let Some(exporter) = store.exporter() else {
531        return keystore_error(mkit_keystore::Error::UnsupportedOperation("export"));
532    };
533    let secret = match exporter.export(&selector) {
534        Ok(secret) => secret,
535        Err(error) => return keystore_error(error),
536    };
537    let mut stderr = std::io::stderr().lock();
538    let _ = writeln!(stderr, "warning: printing secret key material to stdout");
539    let mut stdout = std::io::stdout().lock();
540    if let Err(error) = writeln!(stdout, "{}", hex_lower(secret.expose_secret())) {
541        return emit_err(&format!("write exported secret: {error}"), exit::CANTCREAT);
542    }
543    exit::OK
544}
545
546fn delete(opts: DeleteOpts) -> u8 {
547    let cfg = match read_config() {
548        Ok(cfg) => cfg,
549        Err(code) => return code,
550    };
551    let algorithm = match optional_algorithm(opts.algorithm.as_deref()) {
552        Ok(algorithm) => algorithm,
553        Err(code) => return code,
554    };
555    if !opts.yes {
556        return emit_err("mkit key delete requires --yes", exit::USAGE);
557    }
558    let selection = match selection_for(&cfg, opts.backend, opts.label, algorithm) {
559        Ok(selection) => selection,
560        Err(code) => return code,
561    };
562    let store = match store_for_backend(selection.backend) {
563        Ok(store) => store,
564        Err(code) => return code,
565    };
566    let selector = match KeySelector::new(selection.label.clone(), algorithm) {
567        Ok(selector) => selector,
568        Err(error) => return keystore_error(error),
569    };
570    let Some(deleter) = store.deleter() else {
571        return keystore_error(mkit_keystore::Error::UnsupportedOperation("delete"));
572    };
573    match deleter.delete(&selector) {
574        Ok(()) => {
575            let mut stdout = std::io::stdout().lock();
576            let _ = writeln!(stdout, "deleted {}:{}", selection.backend, selection.label);
577            exit::OK
578        }
579        Err(error) => keystore_error(error),
580    }
581}
582
583#[derive(Debug)]
584struct Selection {
585    backend: BackendKind,
586    label: String,
587}
588
589fn selection_for(
590    cfg: &Config,
591    backend: Option<String>,
592    label: Option<String>,
593    algorithm: Option<Algorithm>,
594) -> Result<Selection, u8> {
595    let explicit_backend = match backend {
596        Some(backend) => Some(parse_backend(&backend)?),
597        None => None,
598    };
599    if let Some(label) = label {
600        let backend = match explicit_backend {
601            Some(backend) => backend,
602            None => parse_backend(cfg.key.backend_or_fallback())?,
603        };
604        return Ok(Selection { backend, label });
605    }
606    let algorithm = algorithm.unwrap_or(Algorithm::Ed25519);
607    let configured_ref = configured_ref_explicit(cfg, algorithm);
608    let key_ref = match configured_ref
609        .unwrap_or_else(|| configured_ref_or_fallback(cfg, algorithm))
610        .parse::<KeyRef>()
611    {
612        Ok(key_ref) => key_ref,
613        Err(error) => {
614            return Err(emit_err(
615                &format!("config key ref: {error}"),
616                exit::CONFIG_ERROR,
617            ));
618        }
619    };
620    let backend = match explicit_backend {
621        Some(backend) => backend,
622        None if configured_ref.is_some() => key_ref.backend(),
623        None => parse_backend(cfg.key.backend_or_fallback())?,
624    };
625    Ok(Selection {
626        backend,
627        label: key_ref.label().to_owned(),
628    })
629}
630
631fn configured_ref_explicit(cfg: &Config, algorithm: Algorithm) -> Option<&str> {
632    match algorithm {
633        Algorithm::Ed25519 if !cfg.key.ed25519_ref.is_empty() => Some(cfg.key.ed25519_ref.as_str()),
634        Algorithm::Secp256k1 if !cfg.key.secp256k1_ref.is_empty() => {
635            Some(cfg.key.secp256k1_ref.as_str())
636        }
637        Algorithm::P256 if !cfg.key.p256_ref.is_empty() => Some(cfg.key.p256_ref.as_str()),
638        _ if !cfg.key.default_ref.is_empty() => Some(cfg.key.default_ref.as_str()),
639        _ => None,
640    }
641}
642
643fn configured_ref_or_fallback(cfg: &Config, algorithm: Algorithm) -> &str {
644    match algorithm {
645        Algorithm::Ed25519 => cfg.key.ed25519_ref_or_fallback(),
646        Algorithm::Secp256k1 => cfg.key.secp256k1_ref_or_fallback(),
647        Algorithm::P256 => cfg.key.p256_ref_or_fallback(),
648        // BLS threshold keystore ref defaults to the generic
649        // `key.default_ref` (or the documented `software:default`
650        // fallback). The Phase 3 release-party CLI will introduce a
651        // dedicated `key.bls12381_thr_ref` knob; until then the
652        // generic ref is enough.
653        #[cfg(feature = "bls-threshold")]
654        Algorithm::Bls12381Threshold => cfg.key.default_ref_or_fallback(),
655    }
656}
657
658fn parse_backend(backend: &str) -> Result<BackendKind, u8> {
659    match backend.parse::<BackendKind>() {
660        Ok(parsed) => Ok(parsed),
661        Err(error) => Err(emit_err(
662            &format!("key backend: {error}"),
663            exit::CONFIG_ERROR,
664        )),
665    }
666}
667
668fn store_for_backend(backend: BackendKind) -> Result<Box<dyn Keystore>, u8> {
669    open_backend(backend)
670        .map_err(|error| emit_err(&format!("keystore backend: {error}"), exit::UNAVAILABLE))
671}
672
673fn read_config() -> Result<Config, u8> {
674    let cwd = match std::env::current_dir() {
675        Ok(cwd) => cwd,
676        Err(error) => return Err(emit_err(&format!("cwd: {error}"), exit::NOINPUT)),
677    };
678    config::read_or_default(&cwd)
679        .map_err(|error| emit_err(&format!("config: {error}"), exit::CONFIG_ERROR))
680}
681
682fn parse_algorithm(value: &str) -> Result<Algorithm, u8> {
683    value
684        .parse()
685        .map_err(|error| emit_err(&format!("algorithm: {error}"), exit::USAGE))
686}
687
688fn optional_algorithm(value: Option<&str>) -> Result<Option<Algorithm>, u8> {
689    value.map(parse_algorithm).transpose()
690}
691
692fn optional_algorithm_or_default(value: Option<&str>) -> Result<Algorithm, u8> {
693    match optional_algorithm(value)? {
694        Some(algorithm) => Ok(algorithm),
695        None => Ok(Algorithm::Ed25519),
696    }
697}
698
699#[allow(clippy::fn_params_excessive_bools)]
700fn attrs_from_flags(
701    extractable: bool,
702    non_extractable: bool,
703    device_bound: bool,
704    require_user_presence: bool,
705) -> KeyAttrs {
706    let mut attrs = KeyAttrs::default();
707    if extractable {
708        attrs.extractable = true;
709    }
710    if non_extractable {
711        attrs.extractable = false;
712    }
713    attrs.device_bound = device_bound;
714    attrs.require_user_presence = require_user_presence;
715    attrs
716}
717
718fn print_metadata(metadata: &mkit_keystore::KeyMetadata) {
719    let mut stdout = std::io::stdout().lock();
720    let _ = writeln!(stdout, "backend = {}", metadata.backend());
721    let _ = writeln!(stdout, "label = {}", metadata.label());
722    let _ = writeln!(stdout, "algorithm = {}", metadata.algorithm());
723    let _ = writeln!(stdout, "public_key = {}", hex_lower(metadata.public_key()));
724    let _ = writeln!(stdout, "keyid = {}", metadata.keyid());
725    let _ = writeln!(stdout, "extractable = {}", metadata.extractable);
726    let _ = writeln!(
727        stdout,
728        "require_user_presence = {}",
729        metadata.require_user_presence
730    );
731    let _ = writeln!(stdout, "device_bound = {}", metadata.device_bound);
732}
733
734fn print_capabilities(capabilities: &Capabilities) {
735    let mut stdout = std::io::stdout().lock();
736    let _ = writeln!(stdout, "capabilities.backend = {}", capabilities.backend);
737    let _ = writeln!(
738        stdout,
739        "capabilities.algorithms = {}",
740        algorithms_csv(capabilities)
741    );
742    let _ = writeln!(
743        stdout,
744        "capabilities.can_generate = {}",
745        capabilities.can_generate
746    );
747    let _ = writeln!(
748        stdout,
749        "capabilities.can_import = {}",
750        capabilities.can_import
751    );
752    let _ = writeln!(
753        stdout,
754        "capabilities.can_export = {}",
755        capabilities.can_export
756    );
757    let _ = writeln!(
758        stdout,
759        "capabilities.can_delete = {}",
760        capabilities.can_delete
761    );
762    let _ = writeln!(
763        stdout,
764        "capabilities.supports_listing = {}",
765        capabilities.supports_listing
766    );
767    let _ = writeln!(
768        stdout,
769        "capabilities.supports_user_presence = {}",
770        capabilities.supports_user_presence
771    );
772    let _ = writeln!(
773        stdout,
774        "capabilities.supports_device_bound = {}",
775        capabilities.supports_device_bound
776    );
777    let _ = writeln!(
778        stdout,
779        "capabilities.supports_non_extractable = {}",
780        capabilities.supports_non_extractable
781    );
782}
783
784fn algorithms_csv(capabilities: &Capabilities) -> String {
785    capabilities
786        .algorithms
787        .iter()
788        .map(ToString::to_string)
789        .collect::<Vec<_>>()
790        .join(",")
791}
792
793fn json_capabilities(capabilities: &Capabilities) -> String {
794    let algorithms = capabilities
795        .algorithms
796        .iter()
797        .map(|algorithm| format!("\"{algorithm}\""))
798        .collect::<Vec<_>>()
799        .join(",");
800    format!(
801        "{{\"backend\":\"{}\",\"algorithms\":[{}],\"can_generate\":{},\"can_import\":{},\"can_export\":{},\"can_delete\":{},\"supports_listing\":{},\"supports_user_presence\":{},\"supports_device_bound\":{},\"supports_non_extractable\":{}}}",
802        capabilities.backend,
803        algorithms,
804        capabilities.can_generate,
805        capabilities.can_import,
806        capabilities.can_export,
807        capabilities.can_delete,
808        capabilities.supports_listing,
809        capabilities.supports_user_presence,
810        capabilities.supports_device_bound,
811        capabilities.supports_non_extractable
812    )
813}
814
815fn parse_secret_hex(hex: &str) -> Result<[u8; 32], u8> {
816    if hex.len() != 64 {
817        return Err(emit_err(
818            "--hex must be exactly 64 hex characters",
819            exit::DATAERR,
820        ));
821    }
822    let mut out = [0u8; 32];
823    for (index, chunk) in hex.as_bytes().chunks_exact(2).enumerate() {
824        let high = hex_value(chunk[0])?;
825        let low = hex_value(chunk[1])?;
826        out[index] = (high << 4) | low;
827    }
828    Ok(out)
829}
830
831fn hex_value(byte: u8) -> Result<u8, u8> {
832    match byte {
833        b'0'..=b'9' => Ok(byte - b'0'),
834        b'a'..=b'f' => Ok(byte - b'a' + 10),
835        b'A'..=b'F' => Ok(byte - b'A' + 10),
836        _ => Err(emit_err("invalid hex character", exit::DATAERR)),
837    }
838}
839
840fn hex_lower(bytes: &[u8]) -> String {
841    const HEX: &[u8; 16] = b"0123456789abcdef";
842    let mut out = String::with_capacity(bytes.len() * 2);
843    for byte in bytes {
844        out.push(HEX[(byte >> 4) as usize] as char);
845        out.push(HEX[(byte & 0x0f) as usize] as char);
846    }
847    out
848}
849
850fn json_escape(value: &str) -> String {
851    let mut out = String::with_capacity(value.len());
852    for ch in value.chars() {
853        match ch {
854            '\\' => out.push_str("\\\\"),
855            '"' => out.push_str("\\\""),
856            '\n' => out.push_str("\\n"),
857            '\r' => out.push_str("\\r"),
858            '\t' => out.push_str("\\t"),
859            ch => out.push(ch),
860        }
861    }
862    out
863}
864
865#[allow(clippy::needless_pass_by_value)]
866fn keystore_error(error: mkit_keystore::Error) -> u8 {
867    emit_err(&format!("keystore: {error}"), exit::DATAERR)
868}
869
870fn emit_err(msg: &str, code: u8) -> u8 {
871    let mut stderr = std::io::stderr().lock();
872    let _ = writeln!(stderr, "error: {msg}");
873    code
874}
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}