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 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 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 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 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 #[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}