From 9955341c8190b221d2d150f5d9c0d761367e2b7d Mon Sep 17 00:00:00 2001 From: Hatter Jiang Date: Wed, 15 Mar 2023 00:25:16 +0800 Subject: [PATCH] feat: v1.4.3, add piv-ecdh subcommand --- Cargo.lock | 225 ++++++++++++++++++++++++++++++++++++++------- Cargo.toml | 5 +- src/cmd_pivecdh.rs | 161 ++++++++++++++++++++++++++++++++ src/main.rs | 2 + 4 files changed, 358 insertions(+), 35 deletions(-) create mode 100644 src/cmd_pivecdh.rs diff --git a/Cargo.lock b/Cargo.lock index 8e4ce0d..b403ef8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -169,6 +169,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.11.0" @@ -330,7 +336,7 @@ dependencies = [ [[package]] name = "card-cli" -version = "1.4.2" +version = "1.4.3" dependencies = [ "authenticator", "base64 0.13.1", @@ -342,6 +348,7 @@ dependencies = [ "openpgp-card-pcsc", "openpgp-card-sequoia", "openssl", + "p256 0.13.0", "pem", "rand 0.8.5", "ring", @@ -578,6 +585,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "crypto-bigint" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c2538c4e68e52548bacb3e83ac549f903d44f011ac9d5abb5e132e67d0808f7" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -655,7 +674,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" dependencies = [ "const-oid", - "pem-rfc7468", + "pem-rfc7468 0.6.0", + "zeroize", +] + +[[package]] +name = "der" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc906908ea6458456e5eaa160a9c08543ec3d1e6f71e2235cedd660cb65f9df0" +dependencies = [ + "const-oid", + "pem-rfc7468 0.7.0", "zeroize", ] @@ -777,12 +807,24 @@ version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" dependencies = [ - "der", - "elliptic-curve", - "rfc6979", + "der 0.6.1", + "elliptic-curve 0.12.3", + "rfc6979 0.3.1", "signature 1.6.4", ] +[[package]] +name = "ecdsa" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1b0a1222f8072619e8a6b667a854020a03d363738303203c09468b3424a420a" +dependencies = [ + "der 0.7.1", + "elliptic-curve 0.13.2", + "rfc6979 0.4.0", + "signature 2.0.0", +] + [[package]] name = "either" version = "1.8.1" @@ -795,18 +837,39 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" dependencies = [ - "base16ct", - "crypto-bigint", - "der", + "base16ct 0.1.1", + "crypto-bigint 0.4.9", + "der 0.6.1", "digest 0.10.6", - "ff", + "ff 0.12.1", "generic-array", - "group", + "group 0.12.1", "hkdf", - "pem-rfc7468", - "pkcs8", + "pem-rfc7468 0.6.0", + "pkcs8 0.9.0", "rand_core 0.6.4", - "sec1", + "sec1 0.3.0", + "subtle", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea5a92946e8614bb585254898bb7dd1ddad241ace60c52149e3765e34cc039d" +dependencies = [ + "base16ct 0.2.0", + "crypto-bigint 0.5.1", + "digest 0.10.6", + "ff 0.13.0", + "generic-array", + "group 0.13.0", + "hkdf", + "pem-rfc7468 0.7.0", + "pkcs8 0.10.1", + "rand_core 0.6.4", + "sec1 0.7.1", "subtle", "zeroize", ] @@ -860,6 +923,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "fixedbitset" version = "0.4.2" @@ -927,6 +1000,7 @@ checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -967,7 +1041,18 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" dependencies = [ - "ff", + "ff 0.12.1", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff 0.13.0", "rand_core 0.6.4", "subtle", ] @@ -1623,8 +1708,20 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" dependencies = [ - "ecdsa", - "elliptic-curve", + "ecdsa 0.14.8", + "elliptic-curve 0.12.3", + "sha2", +] + +[[package]] +name = "p256" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7270da3e5caa82afd3deb054cc237905853813aea3859544bc082c3fe55b8d47" +dependencies = [ + "ecdsa 0.16.1", + "elliptic-curve 0.13.2", + "primeorder", "sha2", ] @@ -1634,8 +1731,8 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc8c5bf642dde52bb9e87c0ecd8ca5a76faac2eeed98dedb7c717997e1080aa" dependencies = [ - "ecdsa", - "elliptic-curve", + "ecdsa 0.14.8", + "elliptic-curve 0.12.3", "sha2", ] @@ -1740,6 +1837,15 @@ dependencies = [ "base64ct", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "petgraph" version = "0.6.3" @@ -1765,9 +1871,9 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eff33bdbdfc54cc98a2eca766ebdec3e1b8fb7387523d5c9c9a2891da856f719" dependencies = [ - "der", - "pkcs8", - "spki", + "der 0.6.1", + "pkcs8 0.9.0", + "spki 0.6.0", "zeroize", ] @@ -1777,8 +1883,18 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" dependencies = [ - "der", - "spki", + "der 0.6.1", + "spki 0.6.0", +] + +[[package]] +name = "pkcs8" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d2820d87d2b008616e5c27212dd9e0e694fb4c6b522de06094106813328cb49" +dependencies = [ + "der 0.7.1", + "spki 0.7.0", ] [[package]] @@ -1799,6 +1915,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "primeorder" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7613fdcc0831c10060fa69833ea8fa2caa94b6456f51e25356a885b530a2e3d0" +dependencies = [ + "elliptic-curve 0.13.2", +] + [[package]] name = "proc-macro-hack" version = "0.4.3" @@ -1964,11 +2089,21 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" dependencies = [ - "crypto-bigint", + "crypto-bigint 0.4.9", "hmac 0.12.1", "zeroize", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle", +] + [[package]] name = "ring" version = "0.16.20" @@ -1997,7 +2132,7 @@ dependencies = [ "num-iter", "num-traits", "pkcs1", - "pkcs8", + "pkcs8 0.9.0", "rand_core 0.6.4", "signature 1.6.4", "smallvec 1.10.0", @@ -2018,7 +2153,7 @@ dependencies = [ "num-iter", "num-traits", "pkcs1", - "pkcs8", + "pkcs8 0.9.0", "rand_core 0.6.4", "signature 2.0.0", "subtle", @@ -2122,10 +2257,24 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" dependencies = [ - "base16ct", - "der", + "base16ct 0.1.1", + "der 0.6.1", "generic-array", - "pkcs8", + "pkcs8 0.9.0", + "subtle", + "zeroize", +] + +[[package]] +name = "sec1" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48518a2b5775ba8ca5b46596aae011caa431e6ce7e4a67ead66d92f08884220e" +dependencies = [ + "base16ct 0.2.0", + "der 0.7.1", + "generic-array", + "pkcs8 0.10.1", "subtle", "zeroize", ] @@ -2338,7 +2487,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" dependencies = [ "base64ct", - "der", + "der 0.6.1", +] + +[[package]] +name = "spki" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0445c905640145c7ea8c1993555957f65e7c46d0535b91ba501bc9bfc85522f" +dependencies = [ + "base64ct", + "der 0.7.1", ] [[package]] @@ -3126,19 +3285,19 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10e6fa9476951a9b93d9a31aa5554b5bbac7aafdc5b23e663eb3f9b635c86053" dependencies = [ - "base16ct", + "base16ct 0.1.1", "chrono", "cookie-factory", "der-parser 8.2.0", "des", - "elliptic-curve", + "elliptic-curve 0.12.3", "hmac 0.12.1", "log", "nom", "num-bigint-dig", "num-integer", "num-traits", - "p256", + "p256 0.11.1", "p384", "pbkdf2", "pcsc", diff --git a/Cargo.toml b/Cargo.toml index ccafb32..3caad22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "card-cli" -version = "1.4.2" +version = "1.4.3" authors = ["Hatter Jiang "] edition = "2018" @@ -26,11 +26,12 @@ simpledateformat = "0.1" ring = "0.16" openssl = "0.10" pem = "1.1" -yubikey = "0.7" +yubikey = { version = "0.7", features = ["untested"] } yubico_manager = "0.9" x509 = "0.2" x509-parser = "0.13" ssh-agent = { version = "0.2", features = ["agent"] } +p256 = { version = "0.13.0", features = ["pem", "ecdh"] } #lazy_static = "1.4.0" #ssh-key = "0.4.0" #ctap-hid-fido2 = "2.1.3" diff --git a/src/cmd_pivecdh.rs b/src/cmd_pivecdh.rs new file mode 100644 index 0000000..bbcc4cf --- /dev/null +++ b/src/cmd_pivecdh.rs @@ -0,0 +1,161 @@ +use std::collections::BTreeMap; +use std::fs; +use std::str::FromStr; + +use clap::{App, Arg, ArgMatches, SubCommand}; +use p256::{EncodedPoint, PublicKey}; +use p256::ecdh::EphemeralSecret; +use p256::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint}; +use rand::rngs::OsRng; +use rust_util::util_clap::{Command, CommandError}; +use rust_util::util_msg; +use yubikey::{PinPolicy, YubiKey}; +use yubikey::certificate::PublicKeyInfo; +use yubikey::piv::{AlgorithmId, decrypt_data, metadata, RetiredSlotId, SlotId}; + +pub struct CommandImpl; + +impl Command for CommandImpl { + fn name(&self) -> &str { "piv-ecdh" } + + fn subcommand<'a>(&self) -> App<'a, 'a> { + SubCommand::with_name(self.name()).about("PIV ECDH subcommand") + .arg(Arg::with_name("pin").short("p").long("pin").takes_value(true).help("PIV card user pin")) + .arg(Arg::with_name("slot").short("s").long("slot").takes_value(true).help("PIV slot")) + .arg(Arg::with_name("public").long("public").help("Public key")) + .arg(Arg::with_name("private").long("private").help("Private key(PIV)")) + .arg(Arg::with_name("epk").long("epk").takes_value(true).help("E-Public key")) + .arg(Arg::with_name("public-key").long("public-key").takes_value(true).help("Public key")) + .arg(Arg::with_name("public-key-file").long("public-key-file").takes_value(true).help("Public key")) + .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 public = sub_arg_matches.is_present("public"); + let private = sub_arg_matches.is_present("private"); + if !public && !private { + failure_and_exit!("--public and --private requires one"); + } else if public && private { + failure_and_exit!("--public and --private only allow one"); + } + + let mut json = BTreeMap::<&'_ str, String>::new(); + if public { + let public_key_pem = sub_arg_matches.value_of("public-key").map(ToString::to_string) + .unwrap_or_else(|| match sub_arg_matches.value_of("public-key-file") { + None => failure_and_exit!("--public-key or --public-key-file must require one"), + Some(file) => match fs::read_to_string(file) { + Err(e) => failure_and_exit!("Read from file: {}, failed: {}", file, e), + Ok(key) => key, + } + }); + + debugging!("Public key: {}", &public_key_pem); + + let public_key = opt_result!(public_key_pem.parse::(), "Parse public key failed: {}"); + + let esk = EphemeralSecret::random(&mut OsRng); + let epk = esk.public_key(); + let epk_bytes = EphemeralKeyBytes::from_public_key(&epk); + + let public_key_encoded_point = public_key.to_encoded_point(false); + + let shared_secret = esk.diffie_hellman(&public_key); + if json_output { + json.insert("shared_secret_hex", hex::encode(shared_secret.raw_secret_bytes())); + json.insert("epk_point_hex", hex::encode(epk_bytes.decompress().as_bytes())); + json.insert("pk_point_hex", hex::encode(public_key_encoded_point.as_bytes())); + } else { + information!("Shared secret: {}", hex::encode(shared_secret.raw_secret_bytes())); + information!("EPK point: {}", hex::encode(epk_bytes.decompress().as_bytes())); + information!("Public key point: {}", hex::encode(public_key_encoded_point.as_bytes())); + } + } + + if private { + let pin_opt = sub_arg_matches.value_of("pin"); + + let slot = opt_value_result!(sub_arg_matches.value_of("slot"), "Slot must assigned"); + 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 retired_slot_id = opt_result!(RetiredSlotId::from_str(slot), "Slot not found: {}"); + debugging!("Slot id: {}", retired_slot_id); + let slot_id = SlotId::Retired(retired_slot_id); + if let Ok(meta) = metadata(&mut yk, slot_id) { + debugging!("PIV meta: {:?}", meta); + if let Some((pin_policy, _touch_policy)) = meta.policy { + match pin_policy { + PinPolicy::Never => {} + _ => if pin_opt.is_none() { + failure_and_exit!("Slot pin is required"); + } + } + } + if let Some(public_key) = &meta.public { + match public_key { + PublicKeyInfo::Rsa { algorithm, pubkey } => { + failure_and_exit!("RSA not supported, {:?}, {:?}", algorithm, pubkey); + } + PublicKeyInfo::EcP256(pubkey) => { + if json_output { + json.insert("pk_point_hex", hex::encode(pubkey.as_bytes())); + } else { + information!("EC-P256, {}", hex::encode(pubkey.as_bytes())); + } + } + PublicKeyInfo::EcP384(pubkey) => { + failure_and_exit!("EC-P384 not supported, {}", hex::encode(pubkey.as_bytes())); + } + } + } + } else { + warning!("Get slot: {} meta data failed", slot); + } + + if let Some(pin) = pin_opt { + opt_result!(yk.verify_pin(pin.as_bytes()), "YubiKey verify pin failed: {}"); + } + + let epk_bytes = opt_result!(hex::decode(epk), "Parse epk failed: {}"); + let decrypted_shared_secret = opt_result!(decrypt_data( + &mut yk, + &epk_bytes, + AlgorithmId::EccP256, + slot_id, + ), "Decrypt piv failed: {}"); + + + if json_output { + json.insert("shared_secret_hex", hex::encode(&decrypted_shared_secret)); + } else { + information!("Shared secret: {}", hex::encode(&decrypted_shared_secret)); + } + } + + if json_output { + println!("{}", serde_json::to_string_pretty(&json).unwrap()); + } + Ok(None) + } +} + +// const EPK_BYTES: usize = 33; + +#[derive(Debug)] +pub struct EphemeralKeyBytes(EncodedPoint); + +impl EphemeralKeyBytes { + fn from_public_key(epk: &PublicKey) -> Self { + EphemeralKeyBytes(epk.to_encoded_point(true)) + } + + fn decompress(&self) -> EncodedPoint { + // EphemeralKeyBytes is a valid compressed encoding by construction. + let p = PublicKey::from_encoded_point(&self.0).unwrap(); + p.to_encoded_point(false) + } +} diff --git a/src/main.rs b/src/main.rs index 13cea34..1d85e17 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,6 +24,7 @@ mod cmd_pgpcarddecrypt; mod cmd_pgpcardmake; mod cmd_piv; mod cmd_pivsign; +mod cmd_pivecdh; mod cmd_pivdecrypt; mod cmd_pivgenerate; mod cmd_chall; @@ -66,6 +67,7 @@ fn inner_main() -> CommandError { Box::new(cmd_pgpcardmake::CommandImpl), Box::new(cmd_piv::CommandImpl), Box::new(cmd_pivsign::CommandImpl), + Box::new(cmd_pivecdh::CommandImpl), Box::new(cmd_pivdecrypt::CommandImpl), Box::new(cmd_pivgenerate::CommandImpl), Box::new(cmd_u2fregister::CommandImpl),