Files
card-cli/src/cmd_piv_summary.rs
2025-05-01 21:46:04 +08:00

183 lines
6.7 KiB
Rust

use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use rust_util::XResult;
use serde::Serialize;
use serde_json::{Map, Value};
use spki::der::Encode;
use tabled::settings::Style;
use tabled::{Table, Tabled};
use x509_parser::parse_x509_certificate;
use yubikey::piv::{metadata, SlotId};
use yubikey::{Certificate, YubiKey};
use crate::{cmdutil, util, yubikeyutil};
use crate::pivutil::{get_algorithm_id_by_certificate, ToStr, ORDERED_SLOTS};
const NA: &str = "N/A";
#[derive(Tabled, Serialize)]
struct PivSlot {
name: String,
id: String,
algorithm: String,
origin: String,
retries: String,
subject: String,
pin_policy: String,
touch_policy: String,
}
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "piv-summary" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("PIV subcommand")
.arg(Arg::with_name("table").long("table").help("Show table"))
.arg(Arg::with_name("all").long("all").help("Show all"))
.arg(Arg::with_name("ordered").long("ordered").help("Show ordered"))
.arg(cmdutil::build_json_arg())
.arg(cmdutil::build_serial_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let show_table = sub_arg_matches.is_present("table");
let show_all = sub_arg_matches.is_present("all");
let show_ordered = sub_arg_matches.is_present("ordered");
let mut output = Map::new();
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
success!("Name: {}", yk.name());
information!("Version: {}", yk.version());
information!("Serial: {}", yk.serial());
output.insert("name".to_string(), Value::String(yk.name().to_string()));
output.insert("version".to_string(), Value::String(yk.version().to_string()));
output.insert("serial".to_string(), Value::String(yk.serial().to_string()));
match yk.chuid() {
Ok(chuid) => {
information!("CHUID: {}",chuid.to_string());
output.insert("chuid".to_string(), Value::String(chuid.to_string()));
}
Err(e) => warning!("CHUID: <none> {}", e),
}
match yk.cccid() {
Ok(cccid) => {
information!("CCCID: {}",cccid.to_string());
output.insert("cccid".to_string(), Value::String(cccid.to_string()));
}
Err(e) => warning!("CCCID: <none> {}", e),
}
match yk.get_pin_retries() {
Ok(pin_retries) => {
information!("PIN retries: {}",pin_retries);
output.insert("pin_retries".to_string(), Value::String(pin_retries.to_string()));
}
Err(e) => warning!("PIN retries: <none> {}", e),
}
match yk.piv_keys() {
Ok(keys) => information!("Found {} PIV keys of {}", keys.len(), ORDERED_SLOTS.len()),
Err(e) => failure!("Get PIV keys failed: {}", e)
}
let mut piv_slots = vec![];
for slot in iff!(show_ordered, ORDERED_SLOTS, yubikey::piv::SLOTS) {
print_summary_info(&mut yk, slot, &mut piv_slots, show_all, show_table, json_output).ok();
}
if show_table {
let mut table = Table::new(piv_slots);
table.with(Style::rounded());
println!("{}", table);
} else if json_output {
let piv_slots_json = serde_json::to_string(&piv_slots).unwrap();
let piv_slots_values: Vec<Value> = serde_json::from_str(&piv_slots_json).unwrap();
output.insert("piv_slots".to_string(), Value::Array(piv_slots_values));
}
if json_output {
util::print_pretty_json(&output);
}
Ok(None)
}
}
fn print_summary_info(yubikey: &mut YubiKey, slot: SlotId, piv_slots: &mut Vec<PivSlot>, show_all: bool, show_table: bool, json_output: bool) -> XResult<()> {
let slot_id: u8 = slot.into();
let mut origin = NA.to_string();
let mut retries = NA.to_string();
let mut pin_policy = NA.to_string();
let mut touch_policy = NA.to_string();
if let Ok(metadata) = metadata(yubikey, slot) {
if let Some((p_policy, t_policy)) = &metadata.policy {
pin_policy = p_policy.to_str().to_string();
touch_policy = t_policy.to_str().to_string();
}
if let Some(o) = &metadata.origin {
origin = o.to_str().to_string();
}
if let Some(r) = &metadata.retries {
retries = format!("{}/{}", r.retry_count, r.remaining_count);
}
}
let cert = match Certificate::read(yubikey, slot) {
Ok(c) => c,
Err(e) => {
if show_all {
if show_table || json_output {
piv_slots.push(PivSlot {
name: slot.to_string(),
id: format!("{:x}", slot_id),
algorithm: NA.to_string(),
origin: origin.to_string(),
retries: retries.to_string(),
subject: NA.to_string(),
pin_policy: pin_policy.to_string(),
touch_policy: touch_policy.to_string(),
});
} else {
warning!("Slot: {:?}, id: {:x}, certificate not found", slot, slot_id);
}
}
return simple_error!("error reading certificate in slot {:?}: {}", slot, e);
}
};
let buf_vec = cert.cert.to_der()?;
let algorithm_id = get_algorithm_id_by_certificate(&cert)
.map(|aid| format!("{:?}", aid))
.unwrap_or_else(|e| format!("Error: {}", e));
let cert_subject = match parse_x509_certificate(&buf_vec) {
Ok((_rem, cert)) => cert.subject.to_string(),
_ => cert.cert.tbs_certificate.subject.to_string(),
};
if show_table || json_output {
piv_slots.push(PivSlot {
name: slot.to_string(),
id: format!("{:x}", slot_id),
algorithm: algorithm_id,
origin: origin.to_string(),
retries: retries.to_string(),
subject: cert_subject,
pin_policy: pin_policy.to_string(),
touch_policy: touch_policy.to_string(),
});
} else {
success!("Slot: {:x}, algorithm: {}, name: {:?}, origin: {}, subject: {}, pin policy: {}, touch policy: {}",
slot_id,
algorithm_id,
slot,
&origin,
&cert_subject,
&pin_policy,
&touch_policy,
);
}
Ok(())
}