diff --git a/Cargo.lock b/Cargo.lock index 969fc3f..373141b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -326,7 +326,7 @@ dependencies = [ [[package]] name = "card-cli" -version = "1.7.8" +version = "1.7.9" dependencies = [ "authenticator", "base64 0.21.4", @@ -334,6 +334,7 @@ dependencies = [ "chrono", "clap", "digest 0.10.7", + "ecdsa", "env_logger", "hex", "openpgp-card", @@ -341,6 +342,7 @@ dependencies = [ "openpgp-card-sequoia", "openssl", "p256", + "p384", "pem", "rand 0.8.5", "ring", diff --git a/Cargo.toml b/Cargo.toml index b5876a6..2a306e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "card-cli" -version = "1.7.8" +version = "1.7.9" authors = ["Hatter Jiang "] edition = "2018" @@ -32,10 +32,12 @@ x509 = "0.2" x509-parser = "0.15" ssh-agent = { version = "0.2", features = ["agent"] } p256 = { version = "0.13", features = ["pem", "ecdh"] } +p384 = { version = "0.13.0", features = ["pem", "ecdh"] } spki = { version = "0.7", features = ["pem"] } tabled = "0.14.0" env_logger = "0.10" bech32 = "0.9.1" +ecdsa = { version = "0.16.8", features = ["verifying", "spki", "pem", "der"] } #lazy_static = "1.4.0" #ssh-key = "0.4.0" #ctap-hid-fido2 = "2.1.3" diff --git a/src/cmd_pivverify.rs b/src/cmd_pivverify.rs new file mode 100644 index 0000000..410402f --- /dev/null +++ b/src/cmd_pivverify.rs @@ -0,0 +1,113 @@ +use std::collections::BTreeMap; + +use clap::{App, Arg, ArgMatches, SubCommand}; +use rust_util::{util_msg, XResult}; +use rust_util::util_clap::{Command, CommandError}; +use yubikey::{Key, YubiKey}; +use yubikey::piv::{AlgorithmId, SlotId}; + +use crate::{ecdsautil, pivutil}; +use crate::digest::sha256; +use crate::ecdsautil::EcdsaAlgorithm; +use crate::pivutil::slot_equals; + +pub struct CommandImpl; + +impl Command for CommandImpl { + fn name(&self) -> &str { "piv-verify" } + + fn subcommand<'a>(&self) -> App<'a, 'a> { + SubCommand::with_name(self.name()).about("PIV verify subcommand") + .arg(Arg::with_name("slot").short("s").long("slot").takes_value(true).help("PIV slot, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e")) + .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(Arg::with_name("signature-hex").short("t").long("signature-hex").takes_value(true).help("Signature")) + .arg(Arg::with_name("json").long("json").help("JSON output")) + } + + fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { + let json_output = sub_arg_matches.is_present("json"); + if json_output { util_msg::set_logger_std_out(false); } + + let hash_hex = if let Some(input) = sub_arg_matches.value_of("input") { + hex::encode(sha256(input)) + } else { + opt_value_result!(sub_arg_matches.value_of("hash-hex"), "--hash-hex must assigned").to_string() + }; + let hash = opt_result!(hex::decode(hash_hex), "Parse hash in hex failed: {}"); + let signature = if let Some(signature_hex) = sub_arg_matches.value_of("signature-hex") { + opt_result!(hex::decode(signature_hex), "Parse signature-hex failed: {}") + } else { + return simple_error!("--signature-hex required."); + }; + + let mut json = BTreeMap::<&'_ str, String>::new(); + + let slot = opt_value_result!(sub_arg_matches.value_of("slot"), "--slot must assigned, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e"); + + 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)? { + let certificate = key.certificate(); + let tbs_certificate = &certificate.cert.tbs_certificate; + if let Ok(algorithm_id) = pivutil::get_algorithm_id(&tbs_certificate.subject_public_key_info) { + let public_key_bit_string = &tbs_certificate.subject_public_key_info.subject_public_key; + match algorithm_id { + AlgorithmId::EccP256 | AlgorithmId::EccP384 => { + let pk_point = public_key_bit_string.raw_bytes(); + debugging!("ECDSA public key point: {}", hex::encode(pk_point)); + debugging!("Pre hash: {}", hex::encode(&hash)); + debugging!("Signature: {}", hex::encode(&signature)); + if json_output { + json.insert("public_key_hex", hex::encode(pk_point)); + json.insert("hash_hex", hex::encode(&hash)); + json.insert("signature_hex", hex::encode(&signature)); + } + + let algorithm = iff!(algorithm_id == AlgorithmId::EccP256, EcdsaAlgorithm::P256, EcdsaAlgorithm::P384); + match ecdsautil::ecdsaverify(algorithm, pk_point, &hash, &signature) { + Ok(_) => { + success!("Verify ECDSA succeed."); + if json_output { + json.insert("success", "true".to_string()); + } + } + Err(e) => { + failure!("Verify ECDSA failed: {}", &e); + if json_output { + json.insert("success", "false".to_string()); + json.insert("message", format!("{}", e)); + } + } + } + } + AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => { + let pk_rsa = public_key_bit_string.raw_bytes(); + // TODO ... + debugging!("RSA public key pem: {}", hex::encode(pk_rsa)); + failure!("Current NOT supported."); + } + } + } + } + + if json_output { + println!("{}", serde_json::to_string_pretty(&json).unwrap()); + } + 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/ecdsautil.rs b/src/ecdsautil.rs new file mode 100644 index 0000000..778e982 --- /dev/null +++ b/src/ecdsautil.rs @@ -0,0 +1,34 @@ +use ecdsa::VerifyingKey; +use p256::NistP256; +use p256::ecdsa::signature::hazmat::PrehashVerifier; +use p384::NistP384; +use ecdsa::Signature; +use rust_util::XResult; + +#[derive(Copy, Clone)] +pub enum EcdsaAlgorithm { + P256, + P384, +} + +macro_rules! ecdsa_verify_signature { + ($algo: tt, $pk_point: tt, $prehash: tt, $signature: tt) => ({ + let verifying_key: VerifyingKey<$algo> = opt_result!(VerifyingKey::<$algo>::from_sec1_bytes($pk_point), "Parse public key failed: {}"); + let sign = if let Ok(signature) = Signature::from_der($signature) { + signature + } else if let Ok(signature) = Signature::from_slice($signature) { + signature + } else { + return simple_error!("Parse signature failed: {}", hex::encode($signature)); + }; + opt_result!(verifying_key.verify_prehash($prehash, &sign), "Verify signature failed: {}"); + }) +} + +pub fn ecdsaverify(algo: EcdsaAlgorithm, pk_point: &[u8], prehash: &[u8], signature: &[u8]) -> XResult<()> { + match algo { + EcdsaAlgorithm::P256 => ecdsa_verify_signature!(NistP256, pk_point, prehash, signature), + EcdsaAlgorithm::P384 => ecdsa_verify_signature!(NistP384, pk_point, prehash, signature), + } + Ok(()) +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 88393a1..2a4dbd8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,7 @@ mod pivutil; mod rsautil; mod pkiutil; mod hmacutil; +mod ecdsautil; mod pgpcardutil; mod cmd_list; mod cmd_u2fregister; @@ -28,6 +29,7 @@ mod cmd_pgpcardmake; mod cmd_piv; mod cmd_pivsummary; mod cmd_pivmeta; +mod cmd_pivverify; mod cmd_pivrsasign; mod cmd_pivecdh; mod cmd_pivecsign; @@ -81,6 +83,7 @@ fn inner_main() -> CommandError { Box::new(cmd_piv::CommandImpl), Box::new(cmd_pivsummary::CommandImpl), Box::new(cmd_pivmeta::CommandImpl), + Box::new(cmd_pivverify::CommandImpl), Box::new(cmd_pivrsasign::CommandImpl), Box::new(cmd_pivecdh::CommandImpl), Box::new(cmd_pivecsign::CommandImpl),