1use 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(GenerateOpts),
28 List(ListOpts),
30 Import(ImportOpts),
32 Export(ExportOpts),
34 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 #[arg(long, value_name = "M")]
63 threshold: Option<u32>,
64 #[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 #[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#[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 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 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 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 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 #[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}