diff --git a/Cargo.lock b/Cargo.lock index 8face2e..3f5ded8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -508,7 +508,7 @@ dependencies = [ [[package]] name = "card-cli" -version = "1.12.1" +version = "1.12.2" dependencies = [ "aes-gcm-stream", "authenticator 0.3.1", diff --git a/Cargo.toml b/Cargo.toml index e964652..5aa08aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "card-cli" -version = "1.12.1" +version = "1.12.2" authors = ["Hatter Jiang "] edition = "2018" diff --git a/src/cmd_external_public_key.rs b/src/cmd_external_public_key.rs index ba4cb3d..43453d3 100644 --- a/src/cmd_external_public_key.rs +++ b/src/cmd_external_public_key.rs @@ -1,7 +1,7 @@ use crate::keyutil::{parse_key_uri, KeyUri, KeyUsage}; -use crate::pivutil::slot_equals; use crate::util::base64_encode; -use crate::{cmdutil, seutil, util}; +use crate::yubikeyutil::find_key_or_error; +use crate::{cmdutil, ecdsautil, hmacutil, seutil, util, yubikeyutil}; use clap::{App, ArgMatches, SubCommand}; use ecdsa::elliptic_curve::pkcs8::der::Encode; use rust_util::util_clap::{Command, CommandError}; @@ -9,7 +9,6 @@ use rust_util::XResult; use serde_json::Value; use std::collections::BTreeMap; use x509_parser::parse_x509_certificate; -use yubikey::{Key, YubiKey}; pub struct CommandImpl; @@ -22,13 +21,15 @@ impl Command for CommandImpl { SubCommand::with_name(self.name()) .about("External public key subcommand") .arg(cmdutil::build_parameter_arg()) + .arg(cmdutil::build_serial_arg()) } fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { let parameter = sub_arg_matches.value_of("parameter").unwrap(); + let serial_opt = sub_arg_matches.value_of("serial"); let mut json = BTreeMap::new(); - match fetch_public_key(parameter) { + match fetch_public_key(parameter, &serial_opt) { Ok(public_key_bytes) => { json.insert("success", Value::Bool(true)); json.insert("public_key_base64", base64_encode(&public_key_bytes).into()); @@ -44,7 +45,7 @@ impl Command for CommandImpl { } } -fn fetch_public_key(parameter: &str) -> XResult> { +fn fetch_public_key(parameter: &str, serial_opt: &Option<&str>) -> XResult> { let key_uri = parse_key_uri(parameter)?; match key_uri { KeyUri::SecureEnclaveKey(key) => { @@ -57,18 +58,27 @@ fn fetch_public_key(parameter: &str) -> XResult> { } } KeyUri::YubikeyPivKey(key) => { - let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}"); - let keys = opt_result!(Key::list(&mut yk), "List keys failed: {}"); - for k in &keys { - let slot_str = format!("{:x}", Into::::into(k.slot())); - if slot_equals(&key.slot, &slot_str) { - let cert_der = k.certificate().cert.to_der()?; - let x509_certificate = parse_x509_certificate(cert_der.as_slice()).unwrap().1; - let public_key_bytes = x509_certificate.public_key().raw; - return Ok(public_key_bytes.to_vec()); - } + let mut yk = yubikeyutil::open_yubikey_with_serial(serial_opt)?; + if let Some(key) = find_key_or_error(&mut yk, &key.slot)? { + let cert_der = key.certificate().cert.to_der()?; + let x509_certificate = parse_x509_certificate(cert_der.as_slice()).unwrap().1; + let public_key_bytes = x509_certificate.public_key().raw; + return Ok(public_key_bytes.to_vec()); } simple_error!("Slot {} not found", key.slot) } + KeyUri::YubikeyHmacEncSoftKey(key) => { + let private_key = hmacutil::try_hmac_decrypt_to_string(&key.hmac_enc_private_key)?; + let p256_public_key = ecdsautil::parse_p256_private_key_to_public_key(&private_key).ok(); + let p384_public_key = ecdsautil::parse_p384_private_key_to_public_key(&private_key).ok(); + + if let Some(p256_public_key) = p256_public_key { + return Ok(p256_public_key); + } + if let Some(p384_public_key) = p384_public_key { + return Ok(p384_public_key); + } + simple_error!("Invalid hmac enc private key") + } } } diff --git a/src/cmd_external_sign.rs b/src/cmd_external_sign.rs index f86f19d..f33d217 100644 --- a/src/cmd_external_sign.rs +++ b/src/cmd_external_sign.rs @@ -2,7 +2,7 @@ use crate::cmd_sign_jwt::digest_by_jwt_algorithm; use crate::keyutil::{parse_key_uri, KeyUri, KeyUsage, YubikeyPivKey}; use crate::pivutil::ToStr; use crate::util::{base64_decode, base64_encode}; -use crate::{cmdutil, pivutil, seutil, util}; +use crate::{cmdutil, ecdsautil, hmacutil, pivutil, seutil, util, yubikeyutil}; use clap::{App, ArgMatches, SubCommand}; use jwt::AlgorithmType; use rust_util::util_clap::{Command, CommandError}; @@ -10,7 +10,6 @@ use rust_util::XResult; use serde_json::Value; use std::collections::BTreeMap; use yubikey::piv::{sign_data, AlgorithmId}; -use yubikey::YubiKey; pub struct CommandImpl; @@ -26,6 +25,7 @@ impl Command for CommandImpl { .arg(cmdutil::build_parameter_arg()) .arg(cmdutil::build_message_arg()) .arg(cmdutil::build_pin_arg()) + .arg(cmdutil::build_serial_arg()) } fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { @@ -64,7 +64,7 @@ fn sign(sub_arg_matches: &ArgMatches) -> XResult> { seutil::secure_enclave_p256_sign(&key.private_key, &message_bytes) } KeyUri::YubikeyPivKey(key) => { - let mut yk = opt_result!(YubiKey::open(), "Find YubiKey failed: {}"); + let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?; let pin_opt = pivutil::check_read_pin(&mut yk, key.slot, sub_arg_matches); // FIXME Check Yubikey slot algorithm @@ -84,6 +84,28 @@ fn sign(sub_arg_matches: &ArgMatches) -> XResult> { ); Ok(signed_data.to_vec()) } + KeyUri::YubikeyHmacEncSoftKey(key) => { + let private_key = hmacutil::try_hmac_decrypt_to_string(&key.hmac_enc_private_key)?; + + let p256_private_key_d = ecdsautil::parse_p256_private_key(&private_key).ok(); + let p384_private_key_d = ecdsautil::parse_p384_private_key(&private_key).ok(); + + let (jwt_algorithm, private_key_d) = match (p256_private_key_d, p384_private_key_d) { + (Some(p256_private_key_d), None) => (AlgorithmType::Es256, p256_private_key_d), + (None, Some(p384_private_key_d)) => (AlgorithmType::Es384, p384_private_key_d), + _ => return simple_error!("Invalid private key: {}", private_key), + }; + + let raw_in = digest_by_jwt_algorithm(jwt_algorithm, &message_bytes)?; + + let signed_data = match jwt_algorithm { + AlgorithmType::Es256 => ecdsautil::sign_p256_der(&private_key_d, &raw_in)?, + AlgorithmType::Es384 => ecdsautil::sign_p384_der(&private_key_d, &raw_in)?, + _ => return simple_error!("SHOULD NOT HAPPEN: {:?}", jwt_algorithm), + }; + + Ok(signed_data) + } } } diff --git a/src/cmd_hmac_decrypt.rs b/src/cmd_hmac_decrypt.rs index 543fbde..46f16ca 100644 --- a/src/cmd_hmac_decrypt.rs +++ b/src/cmd_hmac_decrypt.rs @@ -18,6 +18,7 @@ impl Command for CommandImpl { Arg::with_name("ciphertext") .long("ciphertext") .takes_value(true) + .required(true) .help("Ciphertext"), ) .arg(cmdutil::build_json_arg()) diff --git a/src/cmd_hmac_encrypt.rs b/src/cmd_hmac_encrypt.rs index 2bc422f..3a9d77c 100644 --- a/src/cmd_hmac_encrypt.rs +++ b/src/cmd_hmac_encrypt.rs @@ -18,6 +18,7 @@ impl Command for CommandImpl { Arg::with_name("plaintext") .long("plaintext") .takes_value(true) + .required(true) .help("Plaintext"), ) .arg(cmdutil::build_json_arg()) diff --git a/src/cmd_keypair_generate.rs b/src/cmd_keypair_generate.rs index 7ecfd9f..b154a9e 100644 --- a/src/cmd_keypair_generate.rs +++ b/src/cmd_keypair_generate.rs @@ -1,8 +1,10 @@ use crate::keychain::{KeychainKey, KeychainKeyValue}; -use crate::{cmdutil, ecdsautil, hmacutil, util}; +use crate::{cmdutil, ecdsautil, hmacutil, util, yubikeyutil}; use clap::{App, Arg, ArgMatches, SubCommand}; use rust_util::util_clap::{Command, CommandError}; use std::collections::BTreeMap; +use yubikey::piv::AlgorithmId; +use crate::keyutil::{KeyUri, YubikeyHmacEncSoftKey}; use crate::util::base64_encode; pub struct CommandImpl; @@ -83,8 +85,22 @@ impl Command for CommandImpl { let mut json = BTreeMap::<&'_ str, String>::new(); match keychain_key_uri { None => { - json.insert("private_key_base64", pkcs8_base64); + json.insert("private_key_base64", pkcs8_base64.clone()); json.insert("private_key_pem", secret_key_pem); + let algorithm_id = match key_type.as_str() { + "p256" => Some(AlgorithmId::EccP256), + "p384" => Some(AlgorithmId::EccP384), + _ => None, + }; + if let (true, Some(algorithm_id)) = (with_hmac_encrypt, algorithm_id) { + let yk = yubikeyutil::open_yubikey()?; + let yubikey_hmac_enc_soft_key = YubikeyHmacEncSoftKey { + key_name: format!("yubikey{}-{}", yk.version().major, yk.serial().0), + algorithm: algorithm_id, + hmac_enc_private_key: pkcs8_base64, + }; + json.insert("key_uri", KeyUri::YubikeyHmacEncSoftKey(yubikey_hmac_enc_soft_key).to_string()); + } } Some(keychain_key_uri) => { json.insert("keychain_key_uri", keychain_key_uri); diff --git a/src/cmd_list.rs b/src/cmd_list.rs index 8174765..2cdfe1c 100644 --- a/src/cmd_list.rs +++ b/src/cmd_list.rs @@ -2,8 +2,8 @@ use std::collections::BTreeMap; use clap::{App, ArgMatches, SubCommand}; use rust_util::util_clap::{Command, CommandError}; -use yubikey::YubiKey; -use crate::{cmdutil, util}; +use serde_json::Value; +use crate::{cmdutil, util, yubikeyutil}; pub struct CommandImpl; @@ -13,29 +13,33 @@ impl Command for CommandImpl { fn subcommand<'a>(&self) -> App<'a, 'a> { SubCommand::with_name(self.name()).about("YubiKey list") .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 mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}"); + let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?; if json_output { - let mut json = BTreeMap::<&'_ str, String>::new(); - json.insert("name", yk.name().to_string()); - json.insert("version", yk.version().to_string()); - json.insert("serial", yk.serial().0.to_string()); + let mut json = BTreeMap::<&'_ str, Value>::new(); + json.insert("name", yk.name().into()); + json.insert("version", yk.version().to_string().into()); + json.insert("serial", yk.serial().0.into()); if let Ok(pin_retries) = yk.get_pin_retries() { - json.insert("pin_retries", pin_retries.to_string()); + json.insert("pin_retries", pin_retries.into()); } if let Ok(chuid) = yk.chuid() { - json.insert("chuid", chuid.to_string()); + json.insert("chuid", chuid.to_string().into()); } if let Ok(ccuid) = yk.cccid() { - json.insert("ccuid", ccuid.to_string()); + json.insert("ccuid", ccuid.to_string().into()); } if let Ok(piv_keys) = yk.piv_keys() { - json.insert("keys", piv_keys.iter().map(|k| format!("{}", k.slot())).collect::>().join(", ")); + let key_list = piv_keys.iter().map(|k| Value::String(format!("{}", k.slot()))).collect::>(); + json.insert("key_list", key_list.into()); + let keys = piv_keys.iter().map(|k| format!("{}", k.slot())).collect::>().join(", "); + json.insert("keys", keys.into()); } util::print_pretty_json(&json); diff --git a/src/cmd_piv.rs b/src/cmd_piv.rs index 28738e1..9698af7 100644 --- a/src/cmd_piv.rs +++ b/src/cmd_piv.rs @@ -10,7 +10,7 @@ use spki::der::Encode; use x509_parser::parse_x509_certificate; use yubikey::{Certificate, YubiKey}; use yubikey::piv::SlotId; - +use crate::{cmdutil, yubikeyutil}; use crate::pivutil::get_algorithm_id; use crate::pkiutil::{bytes_to_pem, get_pki_algorithm}; @@ -23,6 +23,7 @@ impl Command for CommandImpl { SubCommand::with_name(self.name()).about("PIV subcommand") .arg(Arg::with_name("detail").long("detail").help("Detail output")) .arg(Arg::with_name("show-config").long("show-config").help("Show config output")) + .arg(cmdutil::build_serial_arg()) // .arg(Arg::with_name("json").long("json").help("JSON output")) } @@ -30,7 +31,7 @@ impl Command for CommandImpl { let detail_output = sub_arg_matches.is_present("detail"); let show_config = sub_arg_matches.is_present("show-config"); - let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}"); + let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?; success!("Name: {}", yk.name()); information!("Version: {}", yk.version()); information!("Serial: {}", yk.serial()); diff --git a/src/cmd_piv_decrypt.rs b/src/cmd_piv_decrypt.rs index abf7d0f..df4a81d 100644 --- a/src/cmd_piv_decrypt.rs +++ b/src/cmd_piv_decrypt.rs @@ -3,9 +3,8 @@ use std::collections::BTreeMap; use clap::{App, Arg, ArgMatches, SubCommand}; use rust_util::util_clap::{Command, CommandError}; use yubikey::piv::AlgorithmId; -use yubikey::YubiKey; -use crate::{cmdutil, pinutil, pivutil, util}; +use crate::{cmdutil, pinutil, pivutil, util, yubikeyutil}; use crate::util::{read_stdin, try_decode}; pub struct CommandImpl; @@ -21,6 +20,7 @@ impl Command for CommandImpl { .arg(Arg::with_name("ciphertext").long("ciphertext").short("c").takes_value(true).help("Encrypted data (HEX or Base64)")) .arg(Arg::with_name("stdin").long("stdin").help("Standard input (Ciphertext)")) .arg(cmdutil::build_json_arg()) + .arg(cmdutil::build_serial_arg()) } fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { @@ -38,7 +38,7 @@ impl Command for CommandImpl { return simple_error!("Argument --ciphertext must be assigned"); }; - let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}"); + let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?; if let Some(pin) = &pin_opt { opt_result!(yk.verify_pin(pin.as_bytes()), "YubiKey verify pin failed: {}"); } diff --git a/src/cmd_piv_ecdh.rs b/src/cmd_piv_ecdh.rs index 7762880..665a2ad 100644 --- a/src/cmd_piv_ecdh.rs +++ b/src/cmd_piv_ecdh.rs @@ -4,10 +4,10 @@ use std::fs; use clap::{App, Arg, ArgMatches, SubCommand}; use rand::rngs::OsRng; use rust_util::util_clap::{Command, CommandError}; -use yubikey::{PinPolicy, YubiKey}; +use yubikey::PinPolicy; use yubikey::piv::{AlgorithmId, decrypt_data, metadata}; -use crate::{cmdutil, ecdhutil, pinutil, pivutil, util}; +use crate::{cmdutil, ecdhutil, pinutil, pivutil, util, yubikeyutil}; use crate::pivutil::get_algorithm_id; pub struct CommandImpl; @@ -28,6 +28,7 @@ impl Command for CommandImpl { .arg(Arg::with_name("public-key-file").long("public-key-file").takes_value(true).help("Public key")) .arg(Arg::with_name("public-key-point-hex").long("public-key-point-hex").takes_value(true).help("Public key point hex")) .arg(cmdutil::build_json_arg()) + .arg(cmdutil::build_serial_arg()) } fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { @@ -74,7 +75,7 @@ impl Command for CommandImpl { let slot = opt_value_result!(sub_arg_matches.value_of("slot"), "--slot must assigned, e.g. 82, 83 ..."); let epk = opt_value_result!(sub_arg_matches.value_of("epk"), "--epk must assigned"); - let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}"); + let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?; let slot_id = pivutil::get_slot_id(slot)?; debugging!("Slot id: {}", slot_id); if let Ok(meta) = metadata(&mut yk, slot_id) { diff --git a/src/cmd_piv_ecsign.rs b/src/cmd_piv_ecsign.rs index 9bda0b1..a84041b 100644 --- a/src/cmd_piv_ecsign.rs +++ b/src/cmd_piv_ecsign.rs @@ -4,10 +4,9 @@ use clap::{App, Arg, ArgMatches, SubCommand}; use rust_util::util_clap::{Command, CommandError}; use x509_parser::nom::AsBytes; use yubikey::piv::{metadata, sign_data, AlgorithmId, ManagementAlgorithmId}; -use yubikey::YubiKey; use crate::util::base64_encode; -use crate::{argsutil, cmdutil, pivutil, util}; +use crate::{argsutil, cmdutil, pivutil, util, yubikeyutil}; use crate::digestutil::DigestAlgorithm; pub struct CommandImpl; @@ -25,6 +24,7 @@ impl Command for CommandImpl { .arg(Arg::with_name("input").short("i").long("input").takes_value(true).help("Input")) .arg(Arg::with_name("hash-hex").short("x").long("hash-hex").takes_value(true).help("Hash")) .arg(cmdutil::build_json_arg()) + .arg(cmdutil::build_serial_arg()) } fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { @@ -40,7 +40,7 @@ impl Command for CommandImpl { }; let hash_bytes = argsutil::get_digest_or_hash(sub_arg_matches, digest_algorithm)?; - let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}"); + let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?; let slot_id = pivutil::get_slot_id(slot)?; let pin_opt = pivutil::check_read_pin(&mut yk, slot_id, sub_arg_matches); diff --git a/src/cmd_piv_generate.rs b/src/cmd_piv_generate.rs index ccd3184..6c677b7 100644 --- a/src/cmd_piv_generate.rs +++ b/src/cmd_piv_generate.rs @@ -1,9 +1,9 @@ use clap::{App, Arg, ArgMatches, SubCommand}; use rust_util::util_clap::{Command, CommandError}; -use yubikey::{PinPolicy, piv, TouchPolicy, YubiKey}; +use yubikey::{PinPolicy, piv, TouchPolicy}; use yubikey::piv::{AlgorithmId, SlotId}; -use crate::{cmdutil, pinutil}; +use crate::{cmdutil, pinutil, yubikeyutil}; pub struct CommandImpl; @@ -14,6 +14,7 @@ impl Command for CommandImpl { SubCommand::with_name(self.name()).about("PIV generate subcommand") .arg(cmdutil::build_pin_arg()) .arg(Arg::with_name("force").long("force").help("Force generate")) + .arg(cmdutil::build_serial_arg()) // .arg(cmdutil::build_json_arg()) } @@ -28,7 +29,7 @@ impl Command for CommandImpl { failure_and_exit!("--force must be assigned"); } - let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}"); + let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?; opt_result!(yk.verify_pin(pin.as_bytes()), "YubiKey verify pin failed: {}"); let public_key_info = opt_result!(piv::generate(&mut yk,SlotId::Signature, AlgorithmId::Rsa2048, diff --git a/src/cmd_piv_meta.rs b/src/cmd_piv_meta.rs index 1e02edb..7389a5f 100644 --- a/src/cmd_piv_meta.rs +++ b/src/cmd_piv_meta.rs @@ -6,10 +6,10 @@ use rust_util::util_clap::{Command, CommandError}; use rust_util::util_msg; use rust_util::util_msg::MessageType; use x509_parser::parse_x509_certificate; -use yubikey::{Key, YubiKey}; +use yubikey::Key; use yubikey::piv::{AlgorithmId, metadata}; -use crate::{cmdutil, pivutil, util}; +use crate::{cmdutil, pivutil, util, yubikeyutil}; use crate::keyutil::{KeyUri, YubikeyPivKey}; use crate::pivutil::{get_algorithm_id_by_certificate, slot_equals, ToStr}; use crate::pkiutil::bytes_to_pem; @@ -25,6 +25,7 @@ impl Command for CommandImpl { SubCommand::with_name(self.name()).about("PIV meta subcommand") .arg(cmdutil::build_slot_arg()) .arg(cmdutil::build_json_arg()) + .arg(cmdutil::build_serial_arg()) } fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { @@ -34,7 +35,7 @@ impl Command for CommandImpl { let slot = opt_value_result!(sub_arg_matches.value_of("slot"), "--slot must assigned, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e"); - let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}"); + let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?; let slot_id = pivutil::get_slot_id(slot)?; json.insert("slot", pivutil::to_slot_hex(&slot_id)); diff --git a/src/cmd_piv_rsasign.rs b/src/cmd_piv_rsasign.rs index b97a8f0..a60da5b 100644 --- a/src/cmd_piv_rsasign.rs +++ b/src/cmd_piv_rsasign.rs @@ -2,10 +2,10 @@ use std::collections::BTreeMap; use clap::{App, Arg, ArgMatches, SubCommand}; use rust_util::util_clap::{Command, CommandError}; -use yubikey::{piv, YubiKey}; +use yubikey::piv; use yubikey::piv::{AlgorithmId, SlotId}; -use crate::{cmdutil, pinutil, pivutil, rsautil, util}; +use crate::{cmdutil, pinutil, pivutil, rsautil, util, yubikeyutil}; use crate::util::base64_encode; pub struct CommandImpl; @@ -20,6 +20,7 @@ impl Command for CommandImpl { .arg(cmdutil::build_no_pin_arg()) .arg(Arg::with_name("sha256").short("2").long("sha256").takes_value(true).help("Digest SHA256 HEX")) .arg(cmdutil::build_json_arg()) + .arg(cmdutil::build_serial_arg()) } fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { @@ -29,7 +30,7 @@ impl Command for CommandImpl { let sha256_hex_opt = sub_arg_matches.value_of("sha256").map(|s| s.to_string()); - let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}"); + let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?; if let Some(pin) = &pin_opt { opt_result!(yk.verify_pin(pin.as_bytes()), "YubiKey verify pin failed: {}"); } diff --git a/src/cmd_piv_summary.rs b/src/cmd_piv_summary.rs index a3b19c6..6d7f802 100644 --- a/src/cmd_piv_summary.rs +++ b/src/cmd_piv_summary.rs @@ -9,7 +9,7 @@ use tabled::{Table, Tabled}; use x509_parser::parse_x509_certificate; use yubikey::piv::{metadata, SlotId}; use yubikey::{Certificate, YubiKey}; -use crate::{cmdutil, util}; +use crate::{cmdutil, util, yubikeyutil}; use crate::pivutil::{get_algorithm_id_by_certificate, ToStr, ORDERED_SLOTS}; const NA: &str = "N/A"; @@ -38,6 +38,7 @@ impl Command for CommandImpl { .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 { @@ -48,7 +49,7 @@ impl Command for CommandImpl { let show_ordered = sub_arg_matches.is_present("ordered"); let mut output = Map::new(); - let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}"); + let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?; success!("Name: {}", yk.name()); information!("Version: {}", yk.version()); diff --git a/src/cmd_piv_verify.rs b/src/cmd_piv_verify.rs index 704c761..29f8b95 100644 --- a/src/cmd_piv_verify.rs +++ b/src/cmd_piv_verify.rs @@ -2,14 +2,11 @@ use std::collections::BTreeMap; use clap::{App, Arg, ArgMatches, SubCommand}; use openssl::rsa::{Padding, Rsa}; -use rust_util::XResult; use rust_util::util_clap::{Command, CommandError}; -use yubikey::{Key, YubiKey}; -use yubikey::piv::{AlgorithmId, SlotId}; +use yubikey::piv::AlgorithmId; -use crate::{argsutil, cmdutil, ecdsautil, pivutil, util}; +use crate::{argsutil, cmdutil, ecdsautil, pivutil, util, yubikeyutil}; use crate::ecdsautil::EcdsaAlgorithm; -use crate::pivutil::slot_equals; pub struct CommandImpl; @@ -24,6 +21,7 @@ impl Command for CommandImpl { .arg(Arg::with_name("input").short("i").long("input").takes_value(true).help("Input")) .arg(Arg::with_name("hash-hex").short("x").long("hash-hex").takes_value(true).help("Hash")) .arg(cmdutil::build_json_arg()) + .arg(cmdutil::build_serial_arg()) } fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { @@ -42,7 +40,7 @@ impl Command for CommandImpl { let slot_id = pivutil::get_slot_id(slot)?; json.insert("slot", pivutil::to_slot_hex(&slot_id)); - if let Some(key) = find_key(&slot_id)? { + if let Some(key) = yubikeyutil::open_and_find_key(&slot_id, sub_arg_matches)? { let certificate = key.certificate(); let tbs_certificate = &certificate.cert.tbs_certificate; if let Ok(algorithm_id) = pivutil::get_algorithm_id_by_certificate(certificate) { @@ -104,17 +102,3 @@ impl Command for CommandImpl { Ok(None) } } - -fn find_key(slot_id: &SlotId) -> XResult> { - let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}"); - match Key::list(&mut yk) { - Err(e) => warning!("List keys failed: {}", e), - Ok(keys) => for k in keys { - let slot_str = format!("{:x}", Into::::into(k.slot())); - if slot_equals(slot_id, &slot_str) { - return Ok(Some(k)); - } - }, - } - Ok(None) -} diff --git a/src/cmd_se_generate.rs b/src/cmd_se_generate.rs index d236558..9bef44a 100644 --- a/src/cmd_se_generate.rs +++ b/src/cmd_se_generate.rs @@ -85,7 +85,7 @@ pub fn print_se_key( if let Some(public_key_jwk) = public_key_jwk { json.insert("public_key_jwk", base64_encode(public_key_jwk)); } - json.insert("key", key_uri.to_string()); + json.insert("key_uri", key_uri.to_string()); util::print_pretty_json(&json); } else { diff --git a/src/cmdutil.rs b/src/cmdutil.rs index 606b578..612015e 100644 --- a/src/cmdutil.rs +++ b/src/cmdutil.rs @@ -9,6 +9,11 @@ pub fn build_slot_arg() -> Arg<'static, 'static> { .help("PIV slot, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e") } +pub fn build_serial_arg() -> Arg<'static, 'static> { + Arg::with_name("serial").long("serial").takes_value(true).help("Serial number") +} + + pub fn build_pin_arg() -> Arg<'static, 'static> { Arg::with_name("pin").short("p").long("pin").takes_value(true).help("PIV card user PIN") } diff --git a/src/ecdsautil.rs b/src/ecdsautil.rs index f33be91..b77c7df 100644 --- a/src/ecdsautil.rs +++ b/src/ecdsautil.rs @@ -76,6 +76,35 @@ pub fn generate_p384_keypair() -> XResult<(String, String, String, Vec, JwkE Ok((secret_key_der_base64, secret_key_pem, public_key_pem, public_key_der, jwk_ec_key)) } + +macro_rules! parse_ecdsa_private_key_to_public_key { + ($algo: tt, $parse_ecdsa_private_key: tt) => ({ + use $algo::pkcs8::DecodePrivateKey; + use $algo::SecretKey; + + let secret_key = match SecretKey::from_pkcs8_pem($parse_ecdsa_private_key) { + Ok(secret_key) => secret_key, + Err(_) => match try_decode($parse_ecdsa_private_key) { + Ok(private_key_der) => match SecretKey::from_pkcs8_der(&private_key_der) { + Ok(secret_key) => secret_key, + Err(e) => return simple_error!("Invalid PKCS#8 private key {}, error: {}", $parse_ecdsa_private_key, e), + } + Err(_) => return simple_error!("Invalid PKCS#8 private key: {}", $parse_ecdsa_private_key), + } + }; + let public_key_document = opt_result!(secret_key.public_key().to_public_key_der(), "Conver to public key failed: {}"); + Ok(public_key_document.to_vec()) + }) +} + +pub fn parse_p256_private_key_to_public_key(private_key_pkcs8: &str) -> XResult> { + parse_ecdsa_private_key_to_public_key!(p256, private_key_pkcs8) +} + +pub fn parse_p384_private_key_to_public_key(private_key_pkcs8: &str) -> XResult> { + parse_ecdsa_private_key_to_public_key!(p384, private_key_pkcs8) +} + macro_rules! parse_ecdsa_private_key { ($algo: tt, $parse_ecdsa_private_key: tt) => ({ use $algo::pkcs8::DecodePrivateKey; @@ -113,6 +142,16 @@ pub fn sign_p256_rs(private_key_d: &[u8], pre_hash: &[u8]) -> XResult> { Ok(signature.to_bytes().to_vec()) } +pub fn sign_p256_der(private_key_d: &[u8], pre_hash: &[u8]) -> XResult> { + use p256::ecdsa::{SigningKey, Signature}; + use p256::ecdsa::signature::hazmat::PrehashSigner; + + let signing_key = SigningKey::from_slice(private_key_d)?; + let signature: Signature = signing_key.sign_prehash(pre_hash)?; + + Ok(signature.to_der().as_bytes().to_vec()) +} + pub fn sign_p384_rs(private_key_d: &[u8], pre_hash: &[u8]) -> XResult> { use p384::ecdsa::{SigningKey, Signature}; use p384::ecdsa::signature::hazmat::PrehashSigner; @@ -123,6 +162,16 @@ pub fn sign_p384_rs(private_key_d: &[u8], pre_hash: &[u8]) -> XResult> { Ok(signature.to_bytes().to_vec()) } +pub fn sign_p384_der(private_key_d: &[u8], pre_hash: &[u8]) -> XResult> { + use p384::ecdsa::{SigningKey, Signature}; + use p384::ecdsa::signature::hazmat::PrehashSigner; + + let signing_key = SigningKey::from_slice(private_key_d)?; + let signature: Signature = signing_key.sign_prehash(pre_hash)?; + + Ok(signature.to_der().as_bytes().to_vec()) +} + macro_rules! ecdsa_verify_signature { ($algo: tt, $pk_point: tt, $prehash: tt, $signature: tt) => ({ use ecdsa::Signature; diff --git a/src/keyutil.rs b/src/keyutil.rs index 4fe2129..735f46c 100644 --- a/src/keyutil.rs +++ b/src/keyutil.rs @@ -8,6 +8,7 @@ use crate::pivutil::{ToStr, FromStr}; pub enum KeyUri { SecureEnclaveKey(SecureEnclaveKey), YubikeyPivKey(YubikeyPivKey), + YubikeyHmacEncSoftKey(YubikeyHmacEncSoftKey), } impl KeyUri { @@ -33,7 +34,7 @@ impl ToString for KeyUri { key_uri.push_str(":"); key_uri.push_str(&key.private_key); } - // key://yubikey-5n:piv/p256:*:9a + // key://yubikey-5n:piv/p256::9a KeyUri::YubikeyPivKey(key) => { key_uri.push_str(&key.key_name); key_uri.push_str(":piv/"); @@ -41,6 +42,14 @@ impl ToString for KeyUri { key_uri.push_str("::"); key_uri.push_str(key.slot.to_str()); } + // key://-:soft/p256::hmac_enc:... + KeyUri::YubikeyHmacEncSoftKey(key) => { + key_uri.push_str(&key.key_name); + key_uri.push_str(":soft/"); + key_uri.push_str(key.algorithm.to_str()); + key_uri.push_str("::"); + key_uri.push_str(key.hmac_enc_private_key.as_str()); + } } key_uri } @@ -90,6 +99,14 @@ pub struct YubikeyPivKey { pub slot: SlotId, } +#[allow(dead_code)] +#[derive(Debug)] +pub struct YubikeyHmacEncSoftKey { + pub key_name: String, + pub algorithm: AlgorithmId, + pub hmac_enc_private_key: String, +} + pub fn parse_key_uri(key_uri: &str) -> XResult { let regex = Regex::new(r##"^key://([0-9a-zA-Z\-\._]*):(\w+)/(\w+):((?:\w+)?):(.*)$"##).unwrap(); let captures = match regex.captures(key_uri) { @@ -135,6 +152,20 @@ pub fn parse_key_uri(key_uri: &str) -> XResult { debugging!("Parsed key uri: {:?}", parsed_key_uri); Ok(parsed_key_uri) } + "soft" => { + if "" != usage { + return simple_error!("Key uri's usage must be empty."); + } + let algorithm = opt_value_result!(AlgorithmId::from_str(algorithm), "Invalid algorithm id: {}", algorithm); + let hmac_enc_private_key = left_part.to_string(); + let parsed_key_uri = KeyUri::YubikeyHmacEncSoftKey(YubikeyHmacEncSoftKey { + key_name: host_or_name.to_string(), + algorithm, + hmac_enc_private_key, + }); + debugging!("Parsed key uri: {:?}", parsed_key_uri); + Ok(parsed_key_uri) + } _ => simple_error!("Key uri's module must be se."), } } diff --git a/src/main.rs b/src/main.rs index a97ee48..2d724dd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -78,6 +78,7 @@ mod seutil; mod signfile; mod sshutil; mod util; +mod yubikeyutil; pub struct DefaultCommandImpl; diff --git a/src/yubikeyutil.rs b/src/yubikeyutil.rs new file mode 100644 index 0000000..d1ebc85 --- /dev/null +++ b/src/yubikeyutil.rs @@ -0,0 +1,58 @@ +use crate::pivutil::slot_equals; +use clap::ArgMatches; +use rust_util::XResult; +use yubikey::piv::SlotId; +use yubikey::{Key, Serial, YubiKey}; + +pub fn open_yubikey_with_args(sub_arg_matches: &ArgMatches) -> XResult { + let serial_opt = sub_arg_matches.value_of("serial"); + open_yubikey_with_serial(&serial_opt) +} + +pub fn open_yubikey_with_serial(serial_opt: &Option<&str>) -> XResult { + match serial_opt { + None => open_yubikey(), + Some(serial) => { + let serial_no: u32 = opt_result!(serial.parse(), "{}"); + Ok(opt_result!( + YubiKey::open_by_serial(Serial(serial_no)), + "YubiKey with serial: {} not found: {}", + serial + )) + } + } +} + +pub fn open_yubikey() -> XResult { + Ok(opt_result!(YubiKey::open(), "YubiKey not found: {}")) +} + +pub fn open_and_find_key(slot_id: &SlotId, sub_arg_matches: &ArgMatches) -> XResult> { + let mut yk = open_yubikey_with_args(sub_arg_matches)?; + find_key(&mut yk, slot_id) +} + +pub fn find_key(yk: &mut YubiKey, slot_id: &SlotId) -> XResult> { + match Key::list(yk) { + Err(e) => warning!("List keys failed: {}", e), + Ok(keys) => return Ok(filter_key(keys, slot_id)), + } + Ok(None) +} + +pub fn find_key_or_error(yk: &mut YubiKey, slot_id: &SlotId) -> XResult> { + match Key::list(yk) { + Err(e) => simple_error!("List keys failed: {}", e), + Ok(keys) => Ok(filter_key(keys, slot_id)), + } +} + +fn filter_key(keys: Vec, slot_id: &SlotId) -> Option { + for k in keys { + let slot_str = format!("{:x}", Into::::into(k.slot())); + if slot_equals(slot_id, &slot_str) { + return Some(k); + } + } + None +}