1use ratatui::{
2 layout::Constraint,
3 style::{Color, Modifier, Style},
4 text::{Line, Span, Text},
5 widgets::{Block, Cell, Row, Table},
6};
7use vta_sdk::prelude::*;
8
9use crate::render::{is_full_display, print_full_entry, print_full_list_title, print_widget};
10
11#[allow(clippy::too_many_arguments)]
12fn key_type_with_posture(key_type: &KeyType) -> String {
23 let rendered = serde_json::to_value(key_type)
24 .ok()
25 .and_then(|v| v.as_str().map(str::to_string))
26 .unwrap_or_else(|| format!("{key_type:?}"));
27 format!("{rendered} ({})", key_type.posture().label())
28}
29
30pub async fn cmd_key_create(
31 client: &VtaClient,
32 key_type: &str,
33 derivation_path: Option<String>,
34 mnemonic: Option<String>,
35 label: Option<String>,
36 context_id: Option<String>,
37 internal: bool,
38 yes: bool,
39) -> Result<(), Box<dyn std::error::Error>> {
40 let key_type = match key_type {
41 "ed25519" => KeyType::Ed25519,
42 "x25519" => KeyType::X25519,
43 "p256" => KeyType::P256,
44 "mldsa44" => KeyType::MlDsa44,
48 "mldsa65" => KeyType::MlDsa65,
49 other => {
50 return Err(format!(
51 "unknown key type '{other}', expected ed25519, x25519, p256, mldsa44 or mldsa65"
52 )
53 .into());
54 }
55 };
56 if internal {
62 eprintln!();
63 eprintln!(" ⚠ You are about to create a NON-RECOVERABLE internal key.");
64 eprintln!();
65 eprintln!(" • Its material is generated from the system CSPRNG and is NOT");
66 eprintln!(" derived from your BIP-39 seed. Your 24-word mnemonic will NOT");
67 eprintln!(" recover it.");
68 eprintln!(" • It is excluded from `pnm backup export`. A restored VTA will");
69 eprintln!(" NOT have it.");
70 eprintln!(" • It can never be exported — not by you, not by an admin, not");
71 eprintln!(" under internal authority. The VTA will only sign with it.");
72 eprintln!(" • If this VTA's storage is lost, every signature this key was");
73 eprintln!(" the sole authority for becomes unproducible, permanently.");
74 eprintln!();
75 eprintln!(" It CANNOT be used to sign did:webvh log entries, precisely");
76 eprintln!(" because losing it would freeze that DID forever. It CAN be a");
77 eprintln!(" signing verificationMethod inside a DID document.");
78 eprintln!();
79
80 if !yes {
81 eprint!(" Type 'i understand this key cannot be recovered' to continue: ");
82 use std::io::{BufRead, Write};
83 std::io::stderr().flush().ok();
84 let mut line = String::new();
85 std::io::stdin().lock().read_line(&mut line)?;
86 if !line
87 .trim()
88 .eq_ignore_ascii_case("i understand this key cannot be recovered")
89 {
90 return Err("aborted: confirmation phrase not matched".into());
91 }
92 }
93 }
94
95 let mut req = CreateKeyRequest::new(key_type);
96 if internal {
97 req.internal = Some(true);
98 }
99 if let Some(p) = derivation_path {
100 req = req.derivation_path(p);
101 }
102 if let Some(m) = mnemonic {
103 req = req.mnemonic(m);
104 }
105 if let Some(l) = label {
106 req = req.label(l);
107 }
108 if let Some(c) = context_id {
109 req = req.context(c);
110 }
111 let resp = client.create_key(req).await?;
112 println!("Key created:");
113 println!(" Key ID: {}", resp.key_id);
114 println!(
115 " Key Type: {}",
116 key_type_with_posture(&resp.key_type)
117 );
118 println!(" Derivation Path: {}", resp.derivation_path);
119 println!(" Public Key: {}", resp.public_key);
120 println!(" Status: {}", resp.status);
121 if resp.origin == vta_sdk::keys::KeyOrigin::Internal {
122 println!();
123 println!(" ⚠ This is an internal key. It cannot be exported, is excluded from");
124 println!(" backups, and cannot be recovered from your mnemonic. If this VTA's");
125 println!(" storage is lost, this key is gone.");
126 }
127 if let Some(label) = &resp.label {
128 println!(" Label: {label}");
129 }
130 println!(
131 " Created At: {}",
132 crate::duration::format_local_datetime(resp.created_at)
133 );
134 Ok(())
135}
136
137pub async fn cmd_key_import(
138 client: &VtaClient,
139 key_type: &str,
140 private_key: Option<String>,
141 private_key_file: Option<std::path::PathBuf>,
142 label: Option<String>,
143 context_id: Option<String>,
144) -> Result<(), Box<dyn std::error::Error>> {
145 let key_type = match key_type {
146 "ed25519" => KeyType::Ed25519,
147 "x25519" => KeyType::X25519,
148 "p256" => KeyType::P256,
149 "mldsa44" | "mldsa65" => {
157 return Err(format!(
158 "importing a post-quantum key ('{key_type}') is not supported yet — the VTA \
159 validates imported key material per algorithm and has no checker for ML-DSA. \
160 Use `keys create --type {key_type}` to have the VTA derive one instead."
161 )
162 .into());
163 }
164 other => {
165 return Err(
166 format!("unknown key type '{other}', expected ed25519, x25519, or p256").into(),
167 );
168 }
169 };
170
171 let private_key_multibase = if let Some(key_str) = private_key {
173 key_str
174 } else if let Some(path) = private_key_file {
175 let bytes = std::fs::read(&path)
176 .map_err(|e| format!("failed to read key file '{}': {e}", path.display()))?;
177 match String::from_utf8(bytes.clone()) {
179 Ok(s) if s.starts_with('z') || s.starts_with('f') || s.starts_with('u') => {
180 s.trim().to_string()
181 }
182 _ => multibase::encode(multibase::Base::Base58Btc, &bytes),
183 }
184 } else {
185 return Err("either --private-key or --private-key-file is required".into());
186 };
187
188 let wrapping_key = client.get_wrapping_key().await.map_err(|e| {
197 format!(
198 "failed to fetch ephemeral wrapping key from {}/keys/import/wrapping-key: {e} \
199 — the VTA must support sealed-transfer key import (vta-sdk ≥ 0.8); \
200 raw `private_key_multibase` over REST is no longer accepted",
201 client.endpoint_label()
202 )
203 })?;
204 let sealed = seal_private_key(&wrapping_key.x, &key_type, &private_key_multibase).await?;
205
206 let req = ImportKeyRequest {
207 key_type,
208 private_key_sealed: Some(sealed),
209 private_key_jwe: None,
210 private_key_multibase: None,
211 label,
212 context_id,
213 };
214 let resp = client.import_key(req).await?;
215
216 println!("Key imported successfully:");
217 println!(" Key ID: {}", resp.key_id);
218 println!(" Key Type: {}", key_type_with_posture(&resp.key_type));
219 println!(" Public Key: {}", resp.public_key);
220 println!(" Status: {}", resp.status);
221 println!(" Origin: imported");
222 if let Some(label) = &resp.label {
223 println!(" Label: {label}");
224 }
225 println!(
226 " Created At: {}",
227 crate::duration::format_local_datetime(resp.created_at)
228 );
229 eprintln!();
230 eprintln!(
231 "\x1b[1;33mWarning: securely delete the source key material \u{2014} the VTA now holds this secret.\x1b[0m"
232 );
233
234 Ok(())
235}
236
237async fn seal_private_key(
241 vta_pub_b64: &str,
242 key_type: &KeyType,
243 private_key_multibase: &str,
244) -> Result<String, Box<dyn std::error::Error>> {
245 use base64::Engine;
246 use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64URL;
247 use vta_sdk::sealed_transfer::{
248 AssertionProof, InMemoryNonceStore, ProducerAssertion, RawPrivateKey, SealedPayloadV1,
249 armor, generate_ed25519_keypair, seal_payload,
250 };
251
252 let vta_pub_bytes: [u8; 32] = B64URL
255 .decode(vta_pub_b64)?
256 .try_into()
257 .map_err(|_| "wrapping public key must be 32 bytes")?;
258
259 let (_, key_bytes) = multibase::decode(private_key_multibase)?;
260
261 let payload = SealedPayloadV1::RawPrivateKey(RawPrivateKey {
262 key_type: key_type.to_string(),
263 key_bytes_b64: B64URL.encode(&key_bytes),
264 });
265
266 let (_seed, producer_ed_pub) = generate_ed25519_keypair();
271 let producer = ProducerAssertion {
272 producer_did: affinidi_crypto::did_key::ed25519_pub_to_did_key(&producer_ed_pub),
273 proof: AssertionProof::PinnedOnly,
274 };
275
276 let bundle_id: [u8; 16] = rand::random();
277
278 let nonce_store = InMemoryNonceStore::new();
279 let bundle = seal_payload(&vta_pub_bytes, bundle_id, producer, &payload, &nonce_store).await?;
280 Ok(armor::encode(&bundle))
281}
282
283pub async fn cmd_key_get(
284 client: &VtaClient,
285 key_id: &str,
286 secret: bool,
287) -> Result<(), Box<dyn std::error::Error>> {
288 if secret {
289 let resp = client.get_key_secret(key_id).await?;
290 println!("Key ID: {}", resp.key_id);
291 println!(
292 "Key Type: {}",
293 key_type_with_posture(&resp.key_type)
294 );
295 println!("Public Key Multibase: {}", resp.public_key_multibase);
296 println!("Secret Key Multibase: {}", resp.private_key_multibase);
297 } else {
298 let resp = client.get_key(key_id).await?;
299 println!("Key ID: {}", resp.key_id);
300 println!("Key Type: {}", key_type_with_posture(&resp.key_type));
301 println!("Derivation Path: {}", resp.derivation_path);
302 println!("Public Key: {}", resp.public_key);
303 println!("Status: {}", resp.status);
304 if resp.exportable == Some(false) {
309 println!("Exportable: no — the private half is never released");
310 }
311 if let Some(label) = &resp.label {
312 println!("Label: {label}");
313 }
314 println!(
315 "Created At: {}",
316 crate::duration::format_local_datetime(resp.created_at)
317 );
318 println!(
319 "Updated At: {}",
320 crate::duration::format_local_datetime(resp.updated_at)
321 );
322 }
323 Ok(())
324}
325
326pub async fn cmd_key_set_exportability(
332 client: &VtaClient,
333 key_id: &str,
334 exportable: bool,
335) -> Result<(), Box<dyn std::error::Error>> {
336 let resp = client.set_key_exportability(key_id, exportable).await?;
337 if crate::render::is_json_output() {
338 crate::render::print_json(&resp)?;
339 return Ok(());
340 }
341 if resp.key.exportable == Some(false) {
342 println!("{} is no longer exportable.", resp.key.key_id);
343 println!(
344 "Its private half will not be released to any caller. It can still be used for \
345 signing and key agreement, so anything that asks the VTA to *use* it keeps working."
346 );
347 println!(
348 "Undoing this needs more authority than setting it did — super-admin, or a fresh \
349 step-up on your session."
350 );
351 } else {
352 println!("{} is exportable.", resp.key.key_id);
353 println!("Ordinary release rules apply: a caller entitled to the key can be given it.");
354 }
355 Ok(())
356}
357
358pub async fn cmd_key_revoke(
359 client: &VtaClient,
360 key_id: &str,
361) -> Result<(), Box<dyn std::error::Error>> {
362 let resp = client.invalidate_key(key_id).await?;
363 println!("Key revoked:");
364 println!(" Key ID: {}", resp.key_id);
365 println!(" Status: {}", resp.status);
366 println!(
367 " Updated At: {}",
368 crate::duration::format_local_datetime(resp.updated_at)
369 );
370 Ok(())
371}
372
373pub async fn cmd_key_rename(
374 client: &VtaClient,
375 key_id: &str,
376 new_key_id: &str,
377) -> Result<(), Box<dyn std::error::Error>> {
378 let resp = client.rename_key(key_id, new_key_id).await?;
379 println!("Key renamed:");
380 println!(" Key ID: {}", resp.key_id);
381 println!(
382 " Updated At: {}",
383 crate::duration::format_local_datetime(resp.updated_at)
384 );
385 Ok(())
386}
387
388pub async fn cmd_key_list(
389 client: &VtaClient,
390 offset: u64,
391 limit: u64,
392 status: Option<String>,
393 context_id: Option<String>,
394) -> Result<(), Box<dyn std::error::Error>> {
395 let resp = client
396 .list_keys(offset, limit, status.as_deref(), context_id.as_deref())
397 .await?;
398
399 if crate::render::is_json_output() {
400 crate::render::print_json(&resp)?;
401 return Ok(());
402 }
403
404 if resp.keys.is_empty() {
405 println!("No keys found.");
406 return Ok(());
407 }
408
409 let end = (offset + resp.keys.len() as u64).min(resp.total);
410
411 if is_full_display() {
412 print_full_list_title(
413 &format!("Keys (showing {}..{} of {}", offset + 1, end, resp.total),
414 resp.keys.len(),
415 );
416 for key in &resp.keys {
417 let label = key.label.as_deref().unwrap_or("—");
418 let created = key
419 .created_at
420 .with_timezone(&chrono::Local)
421 .format("%Y-%m-%d %H:%M:%S %:z")
422 .to_string();
423 let status = key.status.to_string();
424 let key_type = key.key_type.to_string();
425 print_full_entry(&[
426 ("Key ID", &key.key_id),
427 ("Label", label),
428 ("Type", &key_type),
429 ("Status", &status),
430 ("Derivation", &key.derivation_path),
431 ("Created", &created),
432 ]);
433 }
434 return Ok(());
435 }
436
437 let dim = Style::default().fg(Color::DarkGray);
438 let bold = Style::default()
439 .fg(Color::White)
440 .add_modifier(Modifier::BOLD);
441
442 let rows: Vec<Row> = resp
443 .keys
444 .iter()
445 .map(|key| {
446 let label = key.label.clone().unwrap_or_else(|| "\u{2014}".into());
447 let created = key
448 .created_at
449 .with_timezone(&chrono::Local)
450 .format("%Y-%m-%d")
451 .to_string();
452
453 let status_span = match key.status {
454 vta_sdk::keys::KeyStatus::Active => {
455 Span::styled(key.status.to_string(), Style::default().fg(Color::Green))
456 }
457 vta_sdk::keys::KeyStatus::Revoked => {
458 Span::styled(key.status.to_string(), Style::default().fg(Color::Red))
459 }
460 };
461
462 let id_line = Line::from(vec![
463 Span::styled("\u{25b8} ", Style::default().fg(Color::Cyan)),
464 Span::styled(key.key_id.clone(), bold),
465 ]);
466
467 let detail_line = Line::from(vec![
468 Span::raw(" "),
469 Span::styled(label, Style::default().fg(Color::Yellow)),
470 Span::styled(" \u{2502} ", dim),
471 Span::raw(key.key_type.to_string()),
472 Span::styled(" \u{2502} ", dim),
473 status_span,
474 Span::styled(" \u{2502} ", dim),
475 Span::styled(key.derivation_path.clone(), dim),
476 Span::styled(" \u{2502} ", dim),
477 Span::styled(created, dim),
478 ]);
479
480 Row::new(vec![Cell::from(Text::from(vec![id_line, detail_line]))])
481 .height(2)
482 .bottom_margin(1)
483 })
484 .collect();
485
486 let title = format!(" Keys ({}\u{2013}{} of {}) ", offset + 1, end, resp.total);
487
488 let table = Table::new(rows, [Constraint::Min(1)])
489 .block(Block::bordered().title(title).border_style(dim));
490
491 let height = (resp.keys.len() as u16 * 3).saturating_sub(1) + 2;
492 print_widget(table, height);
493
494 Ok(())
495}
496
497pub async fn cmd_seeds_list(client: &VtaClient) -> Result<(), Box<dyn std::error::Error>> {
498 let resp = client.list_seeds().await?;
499
500 if resp.seeds.is_empty() {
501 println!("No seed records found.");
502 println!(" (pre-rotation state: using external seed store as generation 0)");
503 println!(" Active seed ID: {}", resp.active_seed_id);
504 return Ok(());
505 }
506
507 println!("{} seed generation(s):\n", resp.seeds.len());
508 for seed in &resp.seeds {
509 println!(" Seed ID: {}", seed.id);
510 println!(" Status: {}", seed.status);
511 println!(
512 " Created: {}",
513 crate::duration::format_local_datetime(seed.created_at)
514 );
515 if let Some(retired_at) = seed.retired_at {
516 println!(
517 " Retired: {}",
518 crate::duration::format_local_datetime(retired_at)
519 );
520 }
521 println!();
522 }
523 println!("Active seed ID: {}", resp.active_seed_id);
524
525 Ok(())
526}
527
528pub async fn cmd_seeds_rotate(
529 client: &VtaClient,
530 mnemonic: Option<String>,
531) -> Result<(), Box<dyn std::error::Error>> {
532 let resp = client.rotate_seed(mnemonic).await?;
533
534 println!("Seed rotated successfully.");
535 println!(" Previous seed ID: {} (retired)", resp.previous_seed_id);
536 println!(" New active seed ID: {}", resp.new_seed_id);
537
538 Ok(())
539}
540
541pub async fn cmd_key_bundle(
542 client: &VtaClient,
543 context: &str,
544 recipient: crate::sealed_producer::SealedRecipient,
545) -> Result<(), Box<dyn std::error::Error>> {
546 let bundle = client.fetch_did_secrets_bundle(context).await?;
547 crate::sealed_producer::emit_did_secrets_bundle(bundle, &recipient, context, None).await
548}
549
550pub async fn cmd_key_secrets(
551 client: &VtaClient,
552 key_ids: Vec<String>,
553 context: Option<String>,
554) -> Result<(), Box<dyn std::error::Error>> {
555 let key_ids = if key_ids.is_empty() {
556 let ctx = context.as_deref().ok_or(
557 "provide key IDs as arguments, or use --context to export all active keys in a context",
558 )?;
559 let resp = client
560 .list_keys(0, 10000, Some("active"), Some(ctx))
561 .await?;
562 resp.keys.into_iter().map(|k| k.key_id).collect()
563 } else {
564 key_ids
565 };
566 if key_ids.is_empty() {
567 println!("No active keys found.");
568 return Ok(());
569 }
570 for (i, key_id) in key_ids.iter().enumerate() {
571 if i > 0 {
572 println!();
573 }
574 let resp = client.get_key_secret(key_id).await?;
575 println!("Key ID: {}", resp.key_id);
576 println!(
577 "Key Type: {}",
578 key_type_with_posture(&resp.key_type)
579 );
580 println!("Public Key Multibase: {}", resp.public_key_multibase);
581 println!("Secret Key Multibase: {}", resp.private_key_multibase);
582 }
583 Ok(())
584}