From 0b616e7b6e0aa344deaa3266bb57e4eb608c760a Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Mon, 24 Oct 2022 18:23:38 +0300 Subject: [PATCH] implement output formats, version This is where we actually implement support for the new, versioned JSON/YAML output formatting. --- tools/src/bin/opgpcard/main.rs | 372 ++++++++++++++++++--------------- 1 file changed, 199 insertions(+), 173 deletions(-) diff --git a/tools/src/bin/opgpcard/main.rs b/tools/src/bin/opgpcard/main.rs index 6a866be..08006dd 100644 --- a/tools/src/bin/opgpcard/main.rs +++ b/tools/src/bin/opgpcard/main.rs @@ -16,7 +16,7 @@ use sequoia_openpgp::types::{HashAlgorithm, SymmetricAlgorithm}; use sequoia_openpgp::Cert; use openpgp_card::algorithm::AlgoSimple; -use openpgp_card::card_do::{Sex, TouchPolicy}; +use openpgp_card::card_do::TouchPolicy; use openpgp_card::{CardBackend, KeyType, OpenPgp}; use openpgp_card_sequoia::card::{Admin, Card, Open}; use openpgp_card_sequoia::util::{ @@ -28,7 +28,12 @@ use crate::util::{load_pin, print_gnuk_note}; use std::io::Write; mod cli; +mod output; mod util; +mod versioned_output; + +use cli::OUTPUT_VERSIONS; +use versioned_output::{OutputBuilder, OutputFormat, OutputVariant, OutputVersion}; const ENTER_USER_PIN: &str = "Enter User PIN:"; const ENTER_ADMIN_PIN: &str = "Enter Admin PIN:"; @@ -39,29 +44,37 @@ fn main() -> Result<(), Box> { let cli = cli::Cli::parse(); match cli.cmd { + cli::Command::OutputVersions {} => { + output_versions(cli.output_version); + } cli::Command::List {} => { - println!("Available OpenPGP cards:"); - list_cards()?; + list_cards(cli.output_format, cli.output_version)?; } cli::Command::Status { ident, verbose, pkm, } => { - print_status(ident, verbose, pkm)?; + print_status(cli.output_format, cli.output_version, ident, verbose, pkm)?; } cli::Command::Info { ident } => { - print_info(ident)?; + print_info(cli.output_format, cli.output_version, ident)?; } cli::Command::Ssh { ident } => { - print_ssh(ident)?; + print_ssh(cli.output_format, cli.output_version, ident)?; } cli::Command::Pubkey { ident, user_pin, user_id, } => { - print_pubkey(ident, user_pin, user_id)?; + print_pubkey( + cli.output_format, + cli.output_version, + ident, + user_pin, + user_id, + )?; } cli::Command::SetIdentity { ident, id } => { set_identity(&ident, id)?; @@ -89,15 +102,20 @@ fn main() -> Result<(), Box> { } cli::Command::Attestation { cmd } => match cmd { cli::AttCommand::Cert { ident } => { + let mut output = output::AttestationCert::default(); + let card = pick_card_for_reading(ident)?; let mut pgp = OpenPgp::new(card); let mut open = Open::new(pgp.transaction()?)?; + output.ident(open.application_identifier()?.ident()); if let Ok(ac) = open.attestation_certificate() { let pem = util::pem_encode(ac); - println!("{}", pem); + output.attestation_cert(pem); } + + println!("{}", output.print(cli.output_format, cli.output_version)?); } cli::AttCommand::Generate { ident, @@ -309,6 +327,8 @@ fn main() -> Result<(), Box> { let user_pin = util::get_pin(&mut open, user_pin, ENTER_USER_PIN); generate_keys( + cli.output_format, + cli.output_version, open, admin_pin.as_deref(), user_pin.as_deref(), @@ -558,17 +578,27 @@ fn main() -> Result<(), Box> { Ok(()) } -fn list_cards() -> Result<()> { +fn output_versions(chosen: OutputVersion) { + for v in OUTPUT_VERSIONS.iter() { + if v == &chosen { + println!("* {}", v); + } else { + println!(" {}", v); + } + } +} + +fn list_cards(format: OutputFormat, output_version: OutputVersion) -> Result<()> { let cards = util::cards()?; + let mut output = output::List::default(); if !cards.is_empty() { for card in cards { let mut pgp = OpenPgp::new(card); let open = Open::new(pgp.transaction()?)?; - println!(" {}", open.application_identifier()?.ident()); + output.push(open.application_identifier()?.ident()); } - } else { - println!("No OpenPGP cards found."); } + println!("{}", output.print(format, output_version)?); Ok(()) } @@ -595,7 +625,7 @@ fn pick_card_for_reading(ident: Option) -> Result'"); @@ -606,7 +636,16 @@ fn pick_card_for_reading(ident: Option) -> Result, verbose: bool, pkm: bool) -> Result<()> { +fn print_status( + format: OutputFormat, + output_version: OutputVersion, + ident: Option, + verbose: bool, + pkm: bool, +) -> Result<()> { + let mut output = output::Status::default(); + output.verbose(verbose); + let card = pick_card_for_reading(ident)?; let mut pgp = OpenPgp::new(card); @@ -615,61 +654,44 @@ fn print_status(ident: Option, verbose: bool, pkm: bool) -> Result<()> { let ard = pgpt.application_related_data()?; let mut open = Open::new(pgpt)?; - - print!("OpenPGP card {}", open.application_identifier()?.ident()); + output.ident(open.application_identifier()?.ident()); let ai = open.application_identifier()?; let version = ai.version().to_be_bytes(); - println!(" (card version {}.{})\n", version[0], version[1]); + output.card_version(format!("{}.{}", version[0], version[1])); // card / cardholder metadata let crd = open.cardholder_related_data()?; - // Remember if any cardholder information is printed (if so, we print a newline later) - let mut card_holder_output = false; - if let Some(name) = crd.name() { // FIXME: decoding as utf8 is wrong (the spec defines this field as latin1 encoded) let name = String::from_utf8_lossy(name).to_string(); - print!("Cardholder: "); - - // This field is silly, maybe ignore it?! - if let Some(sex) = crd.sex() { - if sex == Sex::Male { - print!("Mr. "); - } else if sex == Sex::Female { - print!("Mrs. "); - } - } + // // This field is silly, maybe ignore it?! + // if let Some(sex) = crd.sex() { + // if sex == Sex::Male { + // print!("Mr. "); + // } else if sex == Sex::Female { + // print!("Mrs. "); + // } + // } // re-format name ("last< = name.split("<<").collect(); let name = name.iter().cloned().rev().collect::>().join(" "); - println!("{}", name); - - card_holder_output = true; + output.card_holder(name); } let url = open.url()?; if !url.is_empty() { - println!("URL: {}", url); - card_holder_output = true; + output.url(url); } if let Some(lang) = crd.lang() { - let l = lang - .iter() - .map(|l| format!("{}", l)) - .collect::>() - .join(", "); - println!("Language preferences: '{}'", l); - card_holder_output = true; - } - - if card_holder_output { - println!(); + for lang in lang { + output.language_preference(format!("{}", lang)); + } } // key information (imported vs. generated on card) @@ -682,152 +704,131 @@ fn print_status(ident: Option, verbose: bool, pkm: bool) -> Result<()> { let fps = open.fingerprints()?; let kgt = open.key_generation_times()?; - println!("Signature key"); + let mut signature_key = output::KeySlotInfo::default(); if let Some(fp) = fps.signature() { - println!(" Fingerprint: {}", fp.to_spaced_hex()); + signature_key.fingerprint(fp.to_spaced_hex()); } - println! {" Algorithm: {}", open.algorithm_attributes(KeyType::Signing)?}; + signature_key.algorithm(format!("{}", open.algorithm_attributes(KeyType::Signing)?)); if let Some(kgt) = kgt.signature() { - println! {" Created: {}", kgt.to_datetime()}; + signature_key.created(format!("{}", kgt.to_datetime())); } - if verbose { - if let Some(uif) = ard.uif_pso_cds()? { - println!( - " Touch policy: {} [Features: {}]", - uif.touch_policy(), - uif.features() - ); - } - if let Some(ks) = ki.as_ref().map(|ki| ki.sig_status()) { - println!(" Key Status: {}", ks); - } + if let Some(uif) = ard.uif_pso_cds()? { + signature_key.touch_policy(format!("{}", uif.touch_policy())); + signature_key.touch_features(format!("{}", uif.features())); + } + if let Some(ks) = ki.as_ref().map(|ki| ki.sig_status()) { + signature_key.status(format!("{}", ks)); } - if verbose { - if pws.pw1_cds_valid_once() { - println!(" User PIN presentation valid for one signature"); - } else { - println!(" User PIN presentation valid for unlimited signatures"); - } + if pws.pw1_cds_valid_once() { + signature_key.pin_valid_once(); } - let sst = open.security_support_template()?; - println!(" Signatures made: {}", sst.signature_count()); - if pkm { if let Ok(pkm) = open.public_key(KeyType::Signing) { - println! {" Public key material: {}", pkm}; + signature_key.public_key_material(pkm.to_string()); } } - println!(); - println!("Decryption key"); + output.signature_key(signature_key); + + let sst = open.security_support_template()?; + output.signature_count(sst.signature_count()); + + let mut decryption_key = output::KeySlotInfo::default(); if let Some(fp) = fps.decryption() { - println!(" Fingerprint: {}", fp.to_spaced_hex()); + decryption_key.fingerprint(fp.to_spaced_hex()); } - println! {" Algorithm: {}", open.algorithm_attributes(KeyType::Decryption)?}; + decryption_key.algorithm(format!( + "{}", + open.algorithm_attributes(KeyType::Decryption)? + )); if let Some(kgt) = kgt.decryption() { - println! {" Created: {}", kgt.to_datetime()}; + decryption_key.created(format!("{}", kgt.to_datetime())); } - if verbose { - if let Some(uif) = ard.uif_pso_dec()? { - println!( - " Touch policy: {} [Features: {}]", - uif.touch_policy(), - uif.features() - ); - } - if let Some(ks) = ki.as_ref().map(|ki| ki.dec_status()) { - println!(" Key Status: {}", ks); - } + if let Some(uif) = ard.uif_pso_dec()? { + decryption_key.touch_policy(format!("{}", uif.touch_policy())); + decryption_key.touch_features(format!("{}", uif.features())); + } + if let Some(ks) = ki.as_ref().map(|ki| ki.dec_status()) { + decryption_key.status(format!("{}", ks)); } if pkm { if let Ok(pkm) = open.public_key(KeyType::Decryption) { - println! {" Public key material: {}", pkm}; + decryption_key.public_key_material(pkm.to_string()); } } + output.decryption_key(decryption_key); - println!(); - println!("Authentication key"); + let mut authentication_key = output::KeySlotInfo::default(); if let Some(fp) = fps.authentication() { - println!(" Fingerprint: {}", fp.to_spaced_hex()); + authentication_key.fingerprint(fp.to_spaced_hex()); } - println! {" Algorithm: {}", open.algorithm_attributes(KeyType::Authentication)?}; + authentication_key.algorithm(format!( + "{}", + open.algorithm_attributes(KeyType::Authentication)? + )); if let Some(kgt) = kgt.authentication() { - println! {" Created: {}", kgt.to_datetime()}; + authentication_key.created(format!("{}", kgt.to_datetime())); } - if verbose { - if let Some(uif) = ard.uif_pso_aut()? { - println!( - " Touch policy: {} [Features: {}]", - uif.touch_policy(), - uif.features() - ); - } - if let Some(ks) = ki.as_ref().map(|ki| ki.aut_status()) { - println!(" Key Status: {}", ks); - } + if let Some(uif) = ard.uif_pso_aut()? { + authentication_key.touch_policy(format!("{}", uif.touch_policy())); + authentication_key.touch_features(format!("{}", uif.features())); + } + if let Some(ks) = ki.as_ref().map(|ki| ki.aut_status()) { + authentication_key.status(format!("{}", ks)); } if pkm { if let Ok(pkm) = open.public_key(KeyType::Authentication) { - println! {" public key material: {}", pkm}; + authentication_key.public_key_material(pkm.to_string()); } } + output.authentication_key(authentication_key); // technical details about the card's state - println!(); + output.user_pin_remaining_attempts(pws.err_count_pw1()); + output.admin_pin_remaining_attempts(pws.err_count_pw3()); + output.reset_code_remaining_attempts(pws.err_count_rc()); - println!( - "Remaining PIN attempts: User: {}, Admin: {}, Reset Code: {}", - pws.err_count_pw1(), - pws.err_count_pw3(), - pws.err_count_rc(), - ); + // FIXME: Handle attestation key information as a separate + // KeySlotInfo! Attestation touch information should go into its + // own `Option`, and (if any information about the + // attestation key exists at all, which is not the case for most + // cards) it should be printed as a fourth KeySlot block. + if let Some(uif) = ard.uif_attestation()? { + output.card_touch_policy(uif.touch_policy().to_string()); + output.card_touch_features(uif.features().to_string()); + } - if verbose { - println!(); - - if let Some(uif) = ard.uif_attestation()? { - println!( - "Touch policy attestation: {} [Features: {}]", - uif.touch_policy(), - uif.features() - ); - println!(); + if let Some(ki) = ki { + let num = ki.num_additional(); + for i in 0..num { + output.key_status(ki.additional_ref(i), ki.additional_status(i).to_string()); } + } - if let Some(ki) = ki { - let num = ki.num_additional(); - for i in 0..num { - println!( - "Key Status (#{}): {}", - ki.additional_ref(i), - ki.additional_status(i) - ); - } - - if num > 0 { - println!(); - } - } - - if let Ok(fps) = ard.ca_fingerprints() { - for (num, fp) in fps.iter().enumerate() { - if let Some(fp) = fp { - println!("CA fingerprint {}: {:x?}", num + 1, fp); - } - } + if let Ok(fps) = ard.ca_fingerprints() { + for fp in fps.iter().flatten() { + output.ca_fingerprint(fp.to_string()); } } // FIXME: print "Login Data" + println!("{}", output.print(format, output_version)?); + Ok(()) } /// print metadata information about a card -fn print_info(ident: Option) -> Result<()> { +fn print_info( + format: OutputFormat, + output_version: OutputVersion, + ident: Option, +) -> Result<()> { + let mut output = output::Info::default(); + let card = pick_card_for_reading(ident)?; let mut pgp = OpenPgp::new(card); @@ -835,36 +836,44 @@ fn print_info(ident: Option) -> Result<()> { let ai = open.application_identifier()?; - print!("OpenPGP card {}", ai.ident()); + output.ident(ai.ident()); let version = ai.version().to_be_bytes(); - println!(" (card version {}.{})\n", version[0], version[1]); + output.card_version(format!("{}.{}", version[0], version[1])); - println!("Application Identifier: {}", ai); - println!( - "Manufacturer [{:04X}]: {}\n", - ai.manufacturer(), - ai.manufacturer_name() - ); + output.application_id(ai.to_string()); + output.manufacturer_id(format!("{:04X}", ai.manufacturer())); + output.manufacturer_name(ai.manufacturer_name().to_string()); if let Some(cc) = open.historical_bytes()?.card_capabilities() { - println!("Card Capabilities:\n{}", cc); + for line in cc.to_string().lines() { + let line = line.strip_prefix("- ").unwrap_or(line); + output.card_capability(line.to_string()); + } } if let Some(csd) = open.historical_bytes()?.card_service_data() { - println!("Card service data:\n{}", csd); + output.card_service_data(csd.to_string()); } if let Some(eli) = open.extended_length_information()? { - println!("Extended Length Info:\n{}", eli); + for line in eli.to_string().lines() { + let line = line.strip_prefix("- ").unwrap_or(line); + output.extended_length_info(line.to_string()); + } } let ec = open.extended_capabilities()?; - println!("Extended Capabilities:\n{}", ec); + for line in ec.to_string().lines() { + let line = line.strip_prefix("- ").unwrap_or(line); + output.extended_capability(line.to_string()); + } // Algorithm information (list of supported algorithms) if let Ok(Some(ai)) = open.algorithm_information() { - println!("Supported algorithms:"); - println!("{}", ai); + for line in ai.to_string().lines() { + let line = line.strip_prefix("- ").unwrap_or(line); + output.algorithm(line.to_string()); + } } // FIXME: print KDF info @@ -872,55 +881,63 @@ fn print_info(ident: Option) -> Result<()> { // YubiKey specific (?) firmware version if let Ok(ver) = open.firmware_version() { let ver = ver.iter().map(u8::to_string).collect::>().join("."); - - println!("Firmware Version: {}\n", ver); + output.firmware_version(ver); } + println!("{}", output.print(format, output_version)?); + Ok(()) } -fn print_ssh(ident: Option) -> Result<()> { +fn print_ssh( + format: OutputFormat, + output_version: OutputVersion, + ident: Option, +) -> Result<()> { + let mut output = output::Ssh::default(); + let card = pick_card_for_reading(ident)?; let mut pgp = OpenPgp::new(card); let mut open = Open::new(pgp.transaction()?)?; let ident = open.application_identifier()?.ident(); - - println!("OpenPGP card {}", ident); + output.ident(ident.clone()); // Print fingerprint of authentication subkey let fps = open.fingerprints()?; - println!(); if let Some(fp) = fps.authentication() { - println!("Authentication key fingerprint:\n{}", fp); + output.authentication_key_fingerprint(fp.to_string()); } // Show authentication subkey as openssh public key string if let Ok(pkm) = open.public_key(KeyType::Authentication) { if let Ok(ssh) = util::get_ssh_pubkey_string(&pkm, ident) { - println!(); - println!("SSH public key:\n{}", ssh); + output.ssh_public_key(ssh); } } + println!("{}", output.print(format, output_version)?); Ok(()) } fn print_pubkey( + format: OutputFormat, + output_version: OutputVersion, ident: Option, user_pin: Option, user_ids: Vec, ) -> Result<()> { + let mut output = output::PublicKey::default(); + let card = pick_card_for_reading(ident)?; let mut pgp = OpenPgp::new(card); let mut open = Open::new(pgp.transaction()?)?; let ident = open.application_identifier()?.ident(); - - println!("OpenPGP card {}", ident); + output.ident(ident); let user_pin = util::get_pin(&mut open, user_pin, ENTER_USER_PIN); @@ -971,8 +988,9 @@ fn print_pubkey( )?; let armored = String::from_utf8(cert.armored().to_vec()?)?; - println!("{}", armored); + output.public_key(armored); + println!("{}", output.print(format, output_version)?); Ok(()) } @@ -1096,15 +1114,20 @@ fn get_cert( #[allow(clippy::too_many_arguments)] fn generate_keys( + format: OutputFormat, + version: OutputVersion, mut open: Open, admin_pin: Option<&[u8]>, user_pin: Option<&[u8]>, - output: Option, + output_file: Option, decrypt: bool, auth: bool, algo: Option, user_ids: Vec, ) -> Result<()> { + let mut output = output::AdminGenerate::default(); + output.ident(open.application_identifier()?.ident()); + // 1) Interpret the user's choice of algorithm. // // Unset (None) means that the algorithm that is specified on the card @@ -1131,6 +1154,7 @@ fn generate_keys( }; log::info!(" Key generation will be attempted with algo: {:?}", a); + output.algorithm(format!("{:?}", a)); // 2) Then, generate keys on the card. // We need "admin" access to the card for this). @@ -1156,10 +1180,12 @@ fn generate_keys( )?; let armored = String::from_utf8(cert.armored().to_vec()?)?; + output.public_key(armored); // Write armored certificate to the output file (or stdout) - let mut output = util::open_or_stdout(output.as_deref())?; - output.write_all(armored.as_bytes())?; + let mut handle = util::open_or_stdout(output_file.as_deref())?; + handle.write_all(output.print(format, version)?.as_bytes())?; + let _ = handle.write(b"\n")?; Ok(()) }