diff --git a/Cargo.lock b/Cargo.lock index 91946bd..72d57f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -487,7 +487,7 @@ dependencies = [ [[package]] name = "card-cli" -version = "1.10.9" +version = "1.10.10" dependencies = [ "authenticator 0.3.1", "base64 0.21.7", @@ -509,6 +509,7 @@ dependencies = [ "pem", "pinentry", "rand 0.8.5", + "regex", "reqwest", "ring 0.17.8", "rpassword", diff --git a/Cargo.toml b/Cargo.toml index 56b8994..e6e3dc2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "card-cli" -version = "1.10.9" +version = "1.10.10" authors = ["Hatter Jiang "] edition = "2018" @@ -51,6 +51,7 @@ secrecy = "0.8" der-parser = "9.0" sshcerts = "0.13" swift-rs = { version = "1.0.7", optional = true } +regex = "1.4.6" #lazy_static = "1.4.0" #ssh-key = "0.4.0" #ctap-hid-fido2 = "2.1.3" diff --git a/src/cmd_se_ecdh.rs b/src/cmd_se_ecdh.rs new file mode 100644 index 0000000..ae261d0 --- /dev/null +++ b/src/cmd_se_ecdh.rs @@ -0,0 +1,66 @@ +use crate::keyutil::{parse_key_uri, KeyUri}; +use crate::seutil; +use clap::{App, Arg, ArgMatches, SubCommand}; +use rust_util::util_clap::{Command, CommandError}; +use rust_util::util_msg; +use std::collections::BTreeMap; + +pub struct CommandImpl; + +impl Command for CommandImpl { + fn name(&self) -> &str { + "se-ecdh" + } + + fn subcommand<'a>(&self) -> App<'a, 'a> { + SubCommand::with_name(self.name()) + .about("Secure Enclave ECDH subcommand") + .arg( + Arg::with_name("key") + .long("key") + .required(true) + .takes_value(true) + .help("Key uri"), + ) + .arg( + Arg::with_name("epk") + .long("epk") + .required(true) + .takes_value(true) + .help("E-Public key"), + ) + .arg(Arg::with_name("json").long("json").help("JSON output")) + } + + fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { + if !seutil::is_support_se() { + return simple_error!("Secure Enclave is NOT supported."); + } + let key = sub_arg_matches.value_of("key").unwrap(); + let epk = sub_arg_matches.value_of("epk").unwrap(); + + let json_output = sub_arg_matches.is_present("json"); + if json_output { + util_msg::set_logger_std_out(false); + } + + let se_key_uri = match parse_key_uri(key)? { + KeyUri::SecureEnclaveKey(se_key_uri) => se_key_uri, + }; + + let ephemeral_public_key_bytes = hex::decode(epk)?; + let dh = + seutil::secure_enclave_p256_dh(&se_key_uri.private_key, &ephemeral_public_key_bytes)?; + let dh_hex = hex::encode(&dh); + + if json_output { + let mut json = BTreeMap::<&'_ str, String>::new(); + + json.insert("shared_secret_hex", dh_hex); + } else { + information!("Shared secret: {}", dh_hex); + } + + Ok(None) + } +} diff --git a/src/cmd_se_ecsign.rs b/src/cmd_se_ecsign.rs new file mode 100644 index 0000000..177e9df --- /dev/null +++ b/src/cmd_se_ecsign.rs @@ -0,0 +1,76 @@ +use crate::keyutil::{parse_key_uri, KeyUri}; +use crate::seutil; +use crate::util::{base64_decode, base64_encode}; +use clap::{App, Arg, ArgMatches, SubCommand}; +use rust_util::util_clap::{Command, CommandError}; +use rust_util::util_msg; +use std::collections::BTreeMap; + +pub struct CommandImpl; + +impl Command for CommandImpl { + fn name(&self) -> &str { + "se-ecsign" + } + + fn subcommand<'a>(&self) -> App<'a, 'a> { + SubCommand::with_name(self.name()) + .about("Secure Enclave EC sign subcommand") + .arg( + Arg::with_name("key") + .long("key") + .required(true) + .takes_value(true) + .help("Key uri"), + ) + .arg( + Arg::with_name("message") + .long("message") + .takes_value(true) + .help("Message"), + ) + .arg( + Arg::with_name("message-base64") + .long("message-base64") + .takes_value(true) + .help("Message in base64"), + ) + .arg(Arg::with_name("json").long("json").help("JSON output")) + } + + fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { + if !seutil::is_support_se() { + return simple_error!("Secure Enclave is NOT supported."); + } + let key = sub_arg_matches.value_of("key").unwrap(); + let message_bytes = match sub_arg_matches.value_of("message") { + None => match sub_arg_matches.value_of("message-base64") { + None => return simple_error!("Argument --message or --message-base64 is required"), + Some(message_base64) => base64_decode(message_base64)?, + }, + Some(message) => message.as_bytes().to_vec(), + }; + let json_output = sub_arg_matches.is_present("json"); + if json_output { + util_msg::set_logger_std_out(false); + } + + let se_key_uri = match parse_key_uri(key)? { + KeyUri::SecureEnclaveKey(se_key_uri) => se_key_uri, + }; + + let signature = seutil::secure_enclave_p256_sign(&se_key_uri.private_key, &message_bytes)?; + let signature_base64 = base64_encode(&signature); + + if json_output { + let mut json = BTreeMap::<&'_ str, String>::new(); + json.insert("signature", signature_base64); + + println!("{}", serde_json::to_string_pretty(&json).unwrap()); + } else { + success!("Signature: {}", signature_base64); + } + + Ok(None) + } +} diff --git a/src/cmd_se_generate.rs b/src/cmd_se_generate.rs index 14392f2..f8e5090 100644 --- a/src/cmd_se_generate.rs +++ b/src/cmd_se_generate.rs @@ -15,7 +15,7 @@ impl Command for CommandImpl { fn subcommand<'a>(&self) -> App<'a, 'a> { SubCommand::with_name(self.name()) - .about("Secure Enclave subcommand") + .about("Secure Enclave generate subcommand") .arg( Arg::with_name("type") .long("type") diff --git a/src/keyutil.rs b/src/keyutil.rs new file mode 100644 index 0000000..d6251be --- /dev/null +++ b/src/keyutil.rs @@ -0,0 +1,110 @@ +use regex::Regex; +use rust_util::XResult; + +// reference: https://git.hatter.ink/hatter/card-cli/issues/6 +#[derive(Debug)] +pub enum KeyUri { + SecureEnclaveKey(SecureEnclaveKey), +} + +#[derive(Debug, PartialEq, Eq)] +pub enum KeyModule { + SecureEnclave, + OpenPgpCard, + PersonalIdentityVerification, +} + +impl KeyModule { + pub fn from(module: &str) -> Option { + match module { + "se" => Some(Self::SecureEnclave), + "pgp" => Some(Self::OpenPgpCard), + "piv" => Some(Self::PersonalIdentityVerification), + _ => None, + } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum KeyUsage { + Any, + Singing, + KeyAgreement, +} + +impl KeyUsage { + pub fn from(usage: &str) -> Option { + match usage { + "signing" => Some(Self::Singing), + "key_agreement" => Some(Self::KeyAgreement), + "*" => Some(Self::Singing), + _ => None, + } + } +} + +#[derive(Debug)] +pub struct SecureEnclaveKey { + pub host: String, + pub usage: KeyUsage, + pub private_key: String, +} + +pub fn parse_key_uri(key_uri: &str) -> XResult { + let regex = Regex::new(r##"^key://([a-zA-Z\-\._]*):(\w+)/(\w+):(\w+)?:(.*)$"##).unwrap(); + let captures = match regex.captures(key_uri) { + None => return simple_error!("Invalid key uri: {}", key_uri), + Some(captures) => captures, + }; + let host = captures.get(1).unwrap().as_str(); + let module = captures.get(2).unwrap().as_str(); + let algorithm = captures.get(3).unwrap().as_str(); + let usage = captures.get(4).unwrap().as_str(); + let left_part = captures.get(5).unwrap().as_str(); + + if "se" != module { + return simple_error!("Key uri's module must be se."); + } + if "p256" != algorithm { + return simple_error!("Key uri's algorithm must be p256."); + } + let key_usage = match KeyUsage::from(usage) { + None | Some(KeyUsage::Any) => { + return simple_error!("Key uri's usage must be signing or key_agreement.") + } + Some(key_usage) => key_usage, + }; + + Ok(KeyUri::SecureEnclaveKey(SecureEnclaveKey { + host: host.to_string(), + usage: key_usage, + private_key: left_part.to_string(), + })) +} + +#[test] +fn test_parse_key_uri_01() { + let se_key_uri = + parse_key_uri("key://hatter-mac-pro:se/p256:signing:BASE64(dataRepresentation)").unwrap(); + match se_key_uri { + KeyUri::SecureEnclaveKey(se_key_uri) => { + assert_eq!("hatter-mac-pro", se_key_uri.host); + assert_eq!(KeyUsage::Singing, se_key_uri.usage); + assert_eq!("BASE64(dataRepresentation)", se_key_uri.private_key); + } + } +} + +#[test] +fn test_parse_key_uri_02() { + let se_key_uri = + parse_key_uri("key://hatter-mac-pro:se/p256:key_agreement:BASE64(dataRepresentation)") + .unwrap(); + match se_key_uri { + KeyUri::SecureEnclaveKey(se_key_uri) => { + assert_eq!("hatter-mac-pro", se_key_uri.host); + assert_eq!(KeyUsage::KeyAgreement, se_key_uri.usage); + assert_eq!("BASE64(dataRepresentation)", se_key_uri.private_key); + } + } +} diff --git a/src/main.rs b/src/main.rs index 94de46b..1575c69 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,6 +34,10 @@ mod cmd_rsaverify; mod cmd_se; #[cfg(feature = "with-secure-enclave")] mod cmd_se_generate; +#[cfg(feature = "with-secure-enclave")] +mod cmd_se_ecsign; +#[cfg(feature = "with-secure-enclave")] +mod cmd_se_ecdh; mod cmd_signfile; mod cmd_signjwt; mod cmd_sshagent; @@ -60,6 +64,7 @@ mod seutil; mod signfile; mod sshutil; mod util; +mod keyutil; pub struct DefaultCommandImpl; @@ -127,6 +132,10 @@ fn inner_main() -> CommandError { Box::new(cmd_se::CommandImpl), #[cfg(feature = "with-secure-enclave")] Box::new(cmd_se_generate::CommandImpl), + #[cfg(feature = "with-secure-enclave")] + Box::new(cmd_se_ecsign::CommandImpl), + #[cfg(feature = "with-secure-enclave")] + Box::new(cmd_se_ecdh::CommandImpl), ]; let mut features: Vec<&str> = vec![]; diff --git a/src/seutil.rs b/src/seutil.rs index ebd7533..13051a7 100644 --- a/src/seutil.rs +++ b/src/seutil.rs @@ -1,4 +1,4 @@ -use crate::util::base64_decode; +use crate::util::{base64_decode, base64_encode}; use rust_util::XResult; use swift_rs::swift; use swift_rs::{Bool, SRString}; @@ -7,6 +7,7 @@ swift!(fn is_support_secure_enclave() -> Bool); swift!(fn generate_secure_enclave_p256_ecdh_keypair() -> SRString); swift!(fn generate_secure_enclave_p256_ecsign_keypair() -> SRString); swift!(fn compute_secure_enclave_p256_ecdh(private_key_base64: SRString, ephemera_public_key_base64: SRString) -> SRString); +swift!(fn compute_secure_enclave_p256_ecsign(private_key_base64: SRString, content: SRString) -> SRString); pub fn is_support_se() -> bool { unsafe { is_support_secure_enclave() } @@ -44,3 +45,49 @@ pub fn generate_secure_enclave_p256_keypair(sign: bool) -> XResult<(Vec, Vec let private_key = public_key_and_private_keys[2].to_string(); Ok((public_key_point, public_key_der, private_key)) } + +pub fn secure_enclave_p256_dh( + private_key: &str, + ephemeral_public_key_bytes: &[u8], +) -> XResult> { + let dh_result = unsafe { + compute_secure_enclave_p256_ecdh( + SRString::from(private_key), + SRString::from(base64_encode(ephemeral_public_key_bytes).as_str()), + ) + }; + let dh_result_str = dh_result.as_str(); + if !dh_result_str.starts_with("ok:SharedSecret:") { + return simple_error!("ECDH P256 in secure enclave failed: {}", dh_result_str); + } + + let shared_secret_hex = dh_result_str + .chars() + .skip("ok:SharedSecret:".len()) + .collect::(); + let shared_secret_hex = shared_secret_hex.trim(); + + Ok(opt_result!( + hex::decode(shared_secret_hex), + "Decrypt shared secret hex: {}, failed: {}", + shared_secret_hex + )) +} + +pub fn secure_enclave_p256_sign(private_key: &str, content: &[u8]) -> XResult> { + let signature_result = unsafe { + compute_secure_enclave_p256_ecsign( + SRString::from(private_key), + SRString::from(base64_encode(content).as_str()), + ) + }; + let signature_result_str = signature_result.as_str(); + if !signature_result_str.starts_with("ok:") { + return simple_error!( + "Sign P256 in secure enclave failed: {}", + signature_result_str + ); + } + let signature = signature_result_str.chars().skip(3).collect::(); + Ok(base64_decode(&signature)?) +} diff --git a/swift-lib/src/lib.swift b/swift-lib/src/lib.swift index 0c4fa23..9910043 100644 --- a/swift-lib/src/lib.swift +++ b/swift-lib/src/lib.swift @@ -81,4 +81,32 @@ func computeSecureEnclaveP256Ecdh(privateKeyDataRepresentation: SRString, epheme } catch { return SRString("err:\(error)") } +} + +@_cdecl("compute_secure_enclave_p256_ecsign") +func computeSecureEnclaveP256Ecsign(privateKeyDataRepresentation: SRString, content: SRString) -> SRString { + guard let privateKeyDataRepresentation = Data( + base64Encoded: privateKeyDataRepresentation.toString() + ) else { + return SRString("err:private key base64 decode failed") + } + guard let contentData = Data( + base64Encoded: content.toString() + ) else { + return SRString("err:content base64 decode failed") + } + do { + let context = LAContext(); + let p = try SecureEnclave.P256.Signing.PrivateKey( + dataRepresentation: privateKeyDataRepresentation, + authenticationContext: context + ) + + let digest = SHA256.hash(data: contentData) + let signature = try p.signature(for: digest) + + return SRString("ok:\(signature.derRepresentation.base64EncodedString()))") + } catch { + return SRString("err:\(error)") + } } \ No newline at end of file diff --git a/swift-rs/Cargo.toml b/swift-rs/Cargo.toml index a466285..330d9a9 100644 --- a/swift-rs/Cargo.toml +++ b/swift-rs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "swift-rs" -version = "1.0.6" +version = "1.0.7" description = "Call Swift from Rust with ease!" authors = ["The swift-rs contributors"] license = "MIT OR Apache-2.0"