diff --git a/Cargo.lock b/Cargo.lock index a6aa0c3..8696d77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -321,6 +321,7 @@ dependencies = [ "group", "rand_core", "subtle", + "zeroize 1.2.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2879bab..3cb5cd6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ dialoguer = "0.7" elliptic-curve = "0.6" gumdrop = "0.8" hex = "0.4" -p256 = "0.5" +p256 = {version = "0.5", features = ["zeroize"]} rand = "0.7" ring = "0.16.15" secrecy = "0.7" diff --git a/src/main.rs b/src/main.rs index af1980e..32725be 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,10 @@ use std::thread::sleep; use std::time::{Duration, SystemTime}; use yubikey_piv::{ certificate::{Certificate, PublicKeyInfo}, - key::{generate as yubikey_generate, AlgorithmId, Key, RetiredSlotId, SlotId}, + key::{ + generate as yubikey_generate, import_ecc_key as yubikey_import, AlgorithmId, Key, + RetiredSlotId, SlotId, + }, policy::{PinPolicy, TouchPolicy}, MgmKey, Readers, }; @@ -296,21 +299,85 @@ fn main() -> Result<(), Error> { yubikey.verify_pin(pin.as_bytes())?; } - if let TouchPolicy::Never = touch_policy { - // No need to touch YubiKey - } else { - eprintln!("👆 Please touch the YubiKey"); - } + let touch_prompt = || { + if let TouchPolicy::Never = touch_policy { + // No need to touch YubiKey + } else { + eprintln!("👆 Please touch the YubiKey"); + } + }; // Generate a new key in the selected slot. - let generated = yubikey_generate( - &mut yubikey, - SlotId::Retired(slot), - AlgorithmId::EccP256, - pin_policy, - touch_policy, - )?; - + let generated = match Select::new() + .with_prompt("Select how the key should be generated") + .items(&[ + "On the YubiKey (The secure option, your computer never sees the private \ + key)", + "On the computer (Less secure, you can backup the private key this way, \ + but the key can be exfiltrated or manipulated during import to the \ + YubiKey)", + "Import existing (Also less secure, see above)", + ]) + .default(0) + .interact_opt()? + { + Some(0) => { + touch_prompt(); + yubikey_generate( + &mut yubikey, + SlotId::Retired(slot), + AlgorithmId::EccP256, + pin_policy, + touch_policy, + )? + } + Some(option @ 1..=2) => { + let private_key = match option { + 1 => p256::PrivateKey::generate(), + 2 => { + let private_key_input = Password::new() + .with_prompt("🔐 Enter the private key as a hex string") + .interact()?; + + match hex::decode(private_key_input) { + Ok(private_key_bytes) => { + match p256::PrivateKey::from_bytes(&private_key_bytes[..]) { + Some(private_key) => private_key, + None => { + eprintln!("Incorrect private key size"); + return Ok(()); + } + } + } + Err(_) => { + eprintln!("Private key must be a hex string"); + return Ok(()); + } + } + } + _ => unreachable!(), + }; + touch_prompt(); + yubikey_import( + &mut yubikey, + SlotId::Retired(slot), + AlgorithmId::EccP256, + private_key.to_bytes().as_ref(), + touch_policy, + pin_policy, + )?; + if option == 1 { + eprintln!( + "Your private key (keep it safe, secure and treat it with the care \ + it deserves, you don't need to use it directly as it's stored on \ + the YubiKey): {}", + hex::encode(private_key.to_bytes())); + } + PublicKeyInfo::EcP256(private_key.to_pubkey()) + } + Some(_) => unreachable!(), + None => return Ok(()), + }; let mut serial = [0; 20]; OsRng.fill_bytes(&mut serial); diff --git a/src/p256.rs b/src/p256.rs index 822e895..5888e3f 100644 --- a/src/p256.rs +++ b/src/p256.rs @@ -1,5 +1,6 @@ use elliptic_curve::sec1::EncodedPoint; -use p256::NistP256; +use p256::{NistP256, SecretKey}; +use rand::rngs::OsRng; use std::fmt; /// Wrapper around a compressed secp256r1 curve point. @@ -43,3 +44,26 @@ impl PublicKey { self.0.decompress().unwrap() } } + +pub struct PrivateKey(SecretKey); + +impl PrivateKey { + pub(crate) fn generate() -> PrivateKey { + PrivateKey(SecretKey::random(&mut OsRng)) + } + + pub(crate) fn to_bytes(&self) -> impl AsRef<[u8]> { + self.0.to_bytes() + } + + pub(crate) fn to_pubkey(&self) -> EncodedPoint { + EncodedPoint::from_secret_key(&self.0, false) + } + + pub(crate) fn from_bytes(bytes: impl AsRef<[u8]>) -> Option { + match SecretKey::from_bytes(bytes) { + Ok(secret_key) => Some(PrivateKey(secret_key)), + Err(_) => None, + } + } +}