diff --git a/Cargo.lock b/Cargo.lock index 80755be..fd94d42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -508,7 +508,7 @@ dependencies = [ [[package]] name = "card-cli" -version = "1.13.4" +version = "1.13.5" dependencies = [ "aes-gcm-stream", "authenticator 0.3.1", diff --git a/Cargo.toml b/Cargo.toml index 059946b..9d90cec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "card-cli" -version = "1.13.4" +version = "1.13.5" authors = ["Hatter Jiang "] edition = "2018" diff --git a/src/cmd_external_ecdh.rs b/src/cmd_external_ecdh.rs new file mode 100644 index 0000000..187a5e8 --- /dev/null +++ b/src/cmd_external_ecdh.rs @@ -0,0 +1,132 @@ +use crate::keyutil::{parse_key_uri, KeyAlgorithmId, KeyUri, KeyUsage}; +use crate::pivutil::ToStr; +use crate::{cmd_hmac_decrypt, cmd_se_ecdh, cmdutil, ecdhutil, pivutil, seutil, util, yubikeyutil}; +use clap::{App, ArgMatches, SubCommand}; +use rust_util::util_clap::{Command, CommandError}; +use rust_util::XResult; +use serde_json::Value; +use std::collections::BTreeMap; +use yubikey::piv::{decrypt_data, AlgorithmId}; +use crate::util::try_decode; + +pub struct CommandImpl; + +impl Command for CommandImpl { + fn name(&self) -> &str { + "external_ecdh" + } + + fn subcommand<'a>(&self) -> App<'a, 'a> { + SubCommand::with_name(self.name()) + .about("External ECDH subcommand") + .arg(cmdutil::build_parameter_arg()) + .arg(cmdutil::build_epk_arg()) + .arg(cmdutil::build_pin_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 epk = sub_arg_matches.value_of("epk").unwrap(); + let ephemeral_public_key_der_bytes = cmd_se_ecdh::parse_epk(epk)?; + + let mut json = BTreeMap::new(); + let key_uri = parse_key_uri(parameter)?; + + match ecdh(&ephemeral_public_key_der_bytes, key_uri, sub_arg_matches) { + Ok(shared_secret_bytes) => { + json.insert("success", Value::Bool(true)); + json.insert( + "shared_secret_hex", + hex::encode(&shared_secret_bytes).into(), + ); + } + Err(e) => { + json.insert("success", Value::Bool(false)); + json.insert("error", e.to_string().into()); + } + } + + util::print_pretty_json(&json); + Ok(None) + } +} + +pub fn ecdh( + ephemeral_public_key_bytes: &[u8], + key_uri: KeyUri, + sub_arg_matches: &ArgMatches, +) -> XResult> { + match key_uri { + KeyUri::SecureEnclaveKey(key) => { + if key.usage != KeyUsage::Singing { + return simple_error!("Not singing key"); + } + let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &key.private_key)?; + seutil::secure_enclave_p256_dh(&private_key, ephemeral_public_key_bytes) + } + KeyUri::YubikeyPivKey(key) => { + 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); + + if let Some(pin) = pin_opt { + opt_result!( + yk.verify_pin(pin.as_bytes()), + "YubiKey verify pin failed: {}" + ); + } + + let algorithm = opt_value_result!( + KeyAlgorithmId::to_algorithm_id(key.algorithm), + "Yubikey not supported algorithm: {}", + key.algorithm.to_str() + ); + + let epk_bytes = match algorithm { + AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => { + return simple_error!("Algorithm is not supported: {:?}", algorithm) + } + AlgorithmId::EccP256 => { + use p256::{elliptic_curve::sec1::ToEncodedPoint, PublicKey}; + use spki::DecodePublicKey; + let public_key = opt_result!(PublicKey::from_public_key_der( + ephemeral_public_key_bytes),"Parse P256 ephemeral public key failed: {}"); + public_key.to_encoded_point(false).as_bytes().to_vec() + } + AlgorithmId::EccP384 => { + use p384::{elliptic_curve::sec1::ToEncodedPoint, PublicKey}; + use spki::DecodePublicKey; + let public_key = opt_result!(PublicKey::from_public_key_der( + ephemeral_public_key_bytes), "Parse P384 ephemeral public key failed: {}"); + public_key.to_encoded_point(false).as_bytes().to_vec() + } + }; + let decrypted_shared_secret = opt_result!( + decrypt_data(&mut yk, &epk_bytes, algorithm, key.slot,), + "Decrypt piv failed: {}" + ); + + Ok(decrypted_shared_secret.to_vec()) + } + KeyUri::YubikeyHmacEncSoftKey(key) => { + if key.algorithm.is_ecc() { + let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &key.hmac_enc_private_key)?; + let private_key_bytes = try_decode(&private_key)?; + + if let Ok(shared_secret) = ecdhutil::parse_p256_private_and_ecdh(&private_key_bytes, ephemeral_public_key_bytes) { + return Ok(shared_secret.to_vec()); + } + if let Ok(shared_secret) = ecdhutil::parse_p384_private_and_ecdh(&private_key_bytes, ephemeral_public_key_bytes) { + return Ok(shared_secret.to_vec()); + } + if let Ok(shared_secret) = ecdhutil::parse_p521_private_and_ecdh(&private_key_bytes, ephemeral_public_key_bytes) { + return Ok(shared_secret.to_vec()); + } + + simple_error!("Invalid private key and/or ephemeral public key") + } else { + simple_error!("Invalid algorithm: {}", key.algorithm.to_str()) + } + } + } +} diff --git a/src/cmd_se_ecdh.rs b/src/cmd_se_ecdh.rs index e4725c1..066c2b8 100644 --- a/src/cmd_se_ecdh.rs +++ b/src/cmd_se_ecdh.rs @@ -1,11 +1,13 @@ use crate::keyutil::parse_key_uri; use crate::{cmd_hmac_decrypt, cmdutil, seutil, util}; -use clap::{App, Arg, ArgMatches, SubCommand}; +use clap::{App, ArgMatches, SubCommand}; use p256::elliptic_curve::sec1::FromEncodedPoint; use p256::{EncodedPoint, PublicKey}; use rust_util::util_clap::{Command, CommandError}; use spki::EncodePublicKey; use std::collections::BTreeMap; +use rust_util::XResult; +use crate::util::base64_decode; pub struct CommandImpl; @@ -18,13 +20,7 @@ impl Command for CommandImpl { SubCommand::with_name(self.name()) .about("Secure Enclave ECDH subcommand") .arg(cmdutil::build_key_uri_arg()) - .arg( - Arg::with_name("epk") - .long("epk") - .required(true) - .takes_value(true) - .help("E-Public key"), - ) + .arg(cmdutil::build_epk_arg()) .arg(cmdutil::build_json_arg()) } @@ -39,24 +35,7 @@ impl Command for CommandImpl { let se_key_uri = key_uri.as_secure_enclave_key()?; debugging!("Secure enclave key URI: {:?}", se_key_uri); - let ephemeral_public_key_der_bytes = if epk.starts_with("04") { - let ephemeral_public_key_point_bytes = opt_result!( - hex::decode(epk), - "Decode public key point from hex failed: {}" - ); - let encoded_point = opt_result!( - EncodedPoint::from_bytes(ephemeral_public_key_point_bytes), - "Parse public key point failed: {}" - ); - let public_key_opt = PublicKey::from_encoded_point(&encoded_point); - if public_key_opt.is_none().into() { - return simple_error!("Parse public key failed."); - } - let public_key = public_key_opt.unwrap(); - public_key.to_public_key_der()?.as_bytes().to_vec() - } else { - opt_result!(hex::decode(epk), "Decode public key from hex failed: {}") - }; + let ephemeral_public_key_der_bytes = parse_epk(epk)?; let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &se_key_uri.private_key)?; let dh = seutil::secure_enclave_p256_dh( @@ -77,3 +56,30 @@ impl Command for CommandImpl { Ok(None) } } + +pub fn parse_epk(epk: &str) -> XResult> { + if epk.starts_with("04") { + let ephemeral_public_key_point_bytes = opt_result!( + hex::decode(epk), + "Decode public key point from hex failed: {}" + ); + let encoded_point = opt_result!( + EncodedPoint::from_bytes(ephemeral_public_key_point_bytes), + "Parse public key point failed: {}" + ); + let public_key_opt = PublicKey::from_encoded_point(&encoded_point); + if public_key_opt.is_none().into() { + return simple_error!("Parse public key failed."); + } + let public_key = public_key_opt.unwrap(); + Ok(public_key.to_public_key_der()?.as_bytes().to_vec()) + } else { + match hex::decode(epk) { + Ok(epk_bytes) => Ok(epk_bytes), + Err(e) => match base64_decode(&epk) { + Ok(epk_bytes) => Ok(epk_bytes), + Err(_) => simple_error!("Decode public key from hex failed: {}", e) + } + } + } +} diff --git a/src/cmdutil.rs b/src/cmdutil.rs index 988622f..26fbf1c 100644 --- a/src/cmdutil.rs +++ b/src/cmdutil.rs @@ -45,6 +45,10 @@ pub fn build_parameter_arg() -> Arg<'static, 'static> { Arg::with_name("parameter").long("parameter").takes_value(true).required(true).help("Parameter") } +pub fn build_epk_arg() -> Arg<'static, 'static> { + Arg::with_name("epk").long("epk").required(true).takes_value(true).help("E-Public key") +} + pub fn build_message_arg() -> Arg<'static, 'static> { Arg::with_name("message-base64").long("message-base64").takes_value(true).required(true).help("Message in base64") } diff --git a/src/ecdhutil.rs b/src/ecdhutil.rs index a98cf61..0b4831b 100644 --- a/src/ecdhutil.rs +++ b/src/ecdhutil.rs @@ -2,8 +2,7 @@ macro_rules! piv_ecdh { ($p_algo: tt, $public_key_pem_opt: expr, $sub_arg_matches: expr, $json: expr, $json_output: expr) => ({ use $p_algo::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint}; - use $p_algo::ecdh::EphemeralSecret; - use $p_algo::{EncodedPoint, PublicKey}; + use $p_algo::{EncodedPoint, PublicKey, ecdh::EphemeralSecret}; let public_key; if let Some(public_key_pem) = $public_key_pem_opt { public_key = opt_result!(public_key_pem.parse::(), "Parse public key failed: {}"); @@ -35,4 +34,30 @@ macro_rules! piv_ecdh { }) } +macro_rules! parse_private_and_ecdh { + ($algo: tt, $private_key_bytes: tt, $ephemeral_public_key_bytes: tt) => ({ + use $algo::{SecretKey, PublicKey, ecdh::diffie_hellman, pkcs8::DecodePrivateKey}; + use spki::DecodePublicKey; + let secret_key= SecretKey::from_pkcs8_der($private_key_bytes)?; + let public_key = opt_result!(PublicKey::from_public_key_der( + $ephemeral_public_key_bytes),"Parse ephemeral public key failed: {}"); + + let shared_secret = diffie_hellman(secret_key.to_nonzero_scalar(), public_key.as_affine()); + Ok(shared_secret.raw_secret_bytes().to_vec()) + }) +} + +pub fn parse_p256_private_and_ecdh(private_key_bytes: &[u8], ephemeral_public_key_bytes: &[u8]) -> XResult> { + parse_private_and_ecdh!(p256, private_key_bytes, ephemeral_public_key_bytes) +} + +pub fn parse_p384_private_and_ecdh(private_key_bytes: &[u8], ephemeral_public_key_bytes: &[u8]) -> XResult> { + parse_private_and_ecdh!(p384, private_key_bytes, ephemeral_public_key_bytes) +} + +pub fn parse_p521_private_and_ecdh(private_key_bytes: &[u8], ephemeral_public_key_bytes: &[u8]) -> XResult> { + parse_private_and_ecdh!(p521, private_key_bytes, ephemeral_public_key_bytes) +} + +use rust_util::XResult; pub(crate) use piv_ecdh; \ No newline at end of file diff --git a/src/ecdsautil.rs b/src/ecdsautil.rs index 049bd26..7f762d5 100644 --- a/src/ecdsautil.rs +++ b/src/ecdsautil.rs @@ -97,8 +97,7 @@ pub fn generate_ecdsa_keypair(algo: EcdsaAlgorithm) -> XResult<(String, String, macro_rules! parse_ecdsa_private_key_to_public_key { ($algo: tt, $parse_ecdsa_private_key: tt) => ({ - use $algo::pkcs8::DecodePrivateKey; - use $algo::SecretKey; + use $algo::{SecretKey, pkcs8::DecodePrivateKey}; let secret_key = match SecretKey::from_pkcs8_pem($parse_ecdsa_private_key) { Ok(secret_key) => secret_key, @@ -130,8 +129,7 @@ pub fn parse_p521_private_key_to_public_key(private_key_pkcs8: &str) -> XResult< macro_rules! parse_ecdsa_private_key { ($algo: tt, $parse_ecdsa_private_key: tt) => ({ - use $algo::pkcs8::DecodePrivateKey; - use $algo::SecretKey; + use $algo::{SecretKey, pkcs8::DecodePrivateKey}; let secret_key = match SecretKey::from_pkcs8_pem($parse_ecdsa_private_key) { Ok(secret_key) => secret_key, @@ -162,8 +160,7 @@ pub fn parse_p521_private_key(private_key_pkcs8: &str) -> XResult> { macro_rules! sign_ecdsa_rs_or_der { ($algo: tt, $private_key_d: tt, $pre_hash: tt, $is_rs: tt) => ({ - use $algo::ecdsa::{SigningKey, Signature}; - use $algo::ecdsa::signature::hazmat::PrehashSigner; + use $algo::ecdsa::{SigningKey, Signature, signature::hazmat::PrehashSigner}; let signing_key = SigningKey::from_slice($private_key_d)?; let signature: Signature = signing_key.sign_prehash($pre_hash)?; diff --git a/src/main.rs b/src/main.rs index c34c586..fe65c9b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ mod cmd_chall_config; mod cmd_convert_jwk_to_pem; mod cmd_convert_pem_to_jwk; mod cmd_ec_verify; +mod cmd_external_ecdh; mod cmd_external_public_key; mod cmd_external_sign; mod cmd_external_spec; @@ -164,6 +165,7 @@ fn inner_main() -> CommandError { Box::new(cmd_external_spec::CommandImpl), Box::new(cmd_external_public_key::CommandImpl), Box::new(cmd_external_sign::CommandImpl), + Box::new(cmd_external_ecdh::CommandImpl), ]; #[allow(clippy::vec_init_then_push)]