diff --git a/Cargo.lock b/Cargo.lock index cad4e18..1c6ae2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -508,7 +508,7 @@ dependencies = [ [[package]] name = "card-cli" -version = "1.13.11" +version = "1.13.12" dependencies = [ "aes-gcm-stream", "authenticator 0.3.1", @@ -520,6 +520,7 @@ dependencies = [ "digest 0.10.7", "ecdsa 0.16.9", "env_logger", + "external-command-rs", "hex", "jwt", "openpgp-card", @@ -530,6 +531,7 @@ dependencies = [ "p384 0.13.1", "p521", "pem", + "percent-encoding", "pinentry", "rand 0.8.5", "regex", @@ -1133,6 +1135,19 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "external-command-rs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a71b1b81e312b732ee75580d0b7176e13c276e748307b9640d936f93718397f" +dependencies = [ + "base64 0.22.1", + "hex", + "rust_util", + "serde", + "serde_json", +] + [[package]] name = "fastrand" version = "2.3.0" diff --git a/Cargo.toml b/Cargo.toml index c73446c..3939c05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "card-cli" -version = "1.13.11" +version = "1.13.12" authors = ["Hatter Jiang "] edition = "2018" @@ -60,6 +60,8 @@ u2f-hatter-fork = "0.2" security-framework = { version = "3.0", features = ["OSX_10_15"] } rsa = "0.9.8" which = "7.0.3" +percent-encoding = "2.3.1" +external-command-rs = "0.1.1" #lazy_static = "1.4.0" #ssh-key = "0.4.0" #ctap-hid-fido2 = "2.1.3" diff --git a/src/cmd_external_ecdh.rs b/src/cmd_external_ecdh.rs index 187a5e8..c011de4 100644 --- a/src/cmd_external_ecdh.rs +++ b/src/cmd_external_ecdh.rs @@ -128,5 +128,9 @@ pub fn ecdh( simple_error!("Invalid algorithm: {}", key.algorithm.to_str()) } } + KeyUri::ExternalCommandKey(key) => { + let parameter = cmd_hmac_decrypt::try_decrypt(&mut None, &key.parameter)?; + external_command_rs::external_ecdh(&key.external_command, ¶meter, ephemeral_public_key_bytes) + } } } diff --git a/src/cmd_external_public_key.rs b/src/cmd_external_public_key.rs index 6693c28..34bcbb1 100644 --- a/src/cmd_external_public_key.rs +++ b/src/cmd_external_public_key.rs @@ -98,5 +98,9 @@ fn fetch_public_key(parameter: &str, serial_opt: &Option<&str>) -> XResult { + let parameter = cmd_hmac_decrypt::try_decrypt(&mut None, &key.parameter)?; + external_command_rs::external_public_key(&key.external_command, ¶meter) + } } } diff --git a/src/cmd_external_sign.rs b/src/cmd_external_sign.rs index ce660bb..61ded06 100644 --- a/src/cmd_external_sign.rs +++ b/src/cmd_external_sign.rs @@ -122,6 +122,12 @@ pub fn sign(alg: &str, message: &[u8], key_uri: KeyUri, sub_arg_matches: &ArgMat simple_error!("Invalid algorithm: {}", key.algorithm.to_str()) } } + KeyUri::ExternalCommandKey(key) => { + let parameter = cmd_hmac_decrypt::try_decrypt(&mut None, &key.parameter)?; + let alg = key.algorithm.to_jwa_name(); + let signature = external_command_rs::external_sign(&key.external_command, ¶meter, alg, message)?; + Ok(signature) + } } } diff --git a/src/cmd_external_spec.rs b/src/cmd_external_spec.rs index 5bb263e..bb8c081 100644 --- a/src/cmd_external_spec.rs +++ b/src/cmd_external_spec.rs @@ -1,5 +1,5 @@ use crate::util; -use clap::{App, ArgMatches, SubCommand}; +use clap::{App, Arg, ArgMatches, SubCommand}; use rust_util::util_clap::{Command, CommandError}; use serde_json::Value; use std::collections::BTreeMap; @@ -14,19 +14,27 @@ impl Command for CommandImpl { fn subcommand<'a>(&self) -> App<'a, 'a> { SubCommand::with_name(self.name()).about("External spec subcommand") + .arg(Arg::with_name("external-command").long("external-command").takes_value(true).help("External command")) } - fn run(&self, _arg_matches: &ArgMatches, _sub_arg_matches: &ArgMatches) -> CommandError { - let mut json = BTreeMap::new(); - json.insert("success", Value::Bool(true)); - json.insert( - "agent", - format!("card-external-provider/{}", env!("CARGO_PKG_VERSION")).into(), - ); - json.insert("specification", "External/1.0.0-alpha".into()); - json.insert("commands", vec!["external_public_key", "external_sign", "external_ecdh"].into()); + fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { + let external_command_opt = sub_arg_matches.value_of("external-command"); - util::print_pretty_json(&json); + if let Some(external_command) = external_command_opt { + let spec = external_command_rs::external_spec(external_command)?; + util::print_pretty_json(&spec); + } else { + let mut json = BTreeMap::new(); + json.insert("success", Value::Bool(true)); + json.insert( + "agent", + format!("card-external-provider/{}", env!("CARGO_PKG_VERSION")).into(), + ); + json.insert("specification", "External/1.0.0-alpha".into()); + json.insert("commands", vec!["external_public_key", "external_sign", "external_ecdh"].into()); + + util::print_pretty_json(&json); + } Ok(None) } } diff --git a/src/keyutil.rs b/src/keyutil.rs index 79f037e..c8e1f4b 100644 --- a/src/keyutil.rs +++ b/src/keyutil.rs @@ -1,5 +1,6 @@ -use jwt::AlgorithmType; use crate::pivutil::{FromStr, ToStr}; +use jwt::AlgorithmType; +use percent_encoding::NON_ALPHANUMERIC; use regex::Regex; use rust_util::XResult; use yubikey::piv::{AlgorithmId, SlotId}; @@ -10,6 +11,7 @@ pub enum KeyUri { SecureEnclaveKey(SecureEnclaveKey), YubikeyPivKey(YubikeyPivKey), YubikeyHmacEncSoftKey(YubikeyHmacEncSoftKey), + ExternalCommandKey(ExternalCommandKey), } impl KeyUri { @@ -25,10 +27,13 @@ impl KeyUri { KeyUri::SecureEnclaveKey(_) => return AlgorithmType::Es256, KeyUri::YubikeyPivKey(key) => key.algorithm, KeyUri::YubikeyHmacEncSoftKey(key) => key.algorithm, + KeyUri::ExternalCommandKey(key) => key.algorithm, }; match algorithm_id { - KeyAlgorithmId::Rsa1024 | KeyAlgorithmId::Rsa2048 - | KeyAlgorithmId::Rsa3072 | KeyAlgorithmId::Rsa4096 => AlgorithmType::Rs256, + KeyAlgorithmId::Rsa1024 + | KeyAlgorithmId::Rsa2048 + | KeyAlgorithmId::Rsa3072 + | KeyAlgorithmId::Rsa4096 => AlgorithmType::Rs256, KeyAlgorithmId::EccP256 => AlgorithmType::Es256, KeyAlgorithmId::EccP384 => AlgorithmType::Es384, KeyAlgorithmId::EccP521 => AlgorithmType::Es512, @@ -66,6 +71,17 @@ impl ToString for KeyUri { key_uri.push_str("::"); key_uri.push_str(key.hmac_enc_private_key.as_str()); } + // key://external-command-file-name:external_command/p256::parameter + KeyUri::ExternalCommandKey(key) => { + let encoded_external_command = + percent_encoding::utf8_percent_encode(&key.external_command, NON_ALPHANUMERIC) + .to_string(); + key_uri.push_str(&encoded_external_command); + key_uri.push_str(":external_command/"); + key_uri.push_str(key.algorithm.to_str()); + key_uri.push_str("::"); + key_uri.push_str(&key.parameter); + } } key_uri } @@ -106,19 +122,35 @@ impl KeyAlgorithmId { pub fn is_rsa(&self) -> bool { match self { - KeyAlgorithmId::Rsa1024 | KeyAlgorithmId::Rsa2048 - | KeyAlgorithmId::Rsa3072 | KeyAlgorithmId::Rsa4096 => true, + KeyAlgorithmId::Rsa1024 + | KeyAlgorithmId::Rsa2048 + | KeyAlgorithmId::Rsa3072 + | KeyAlgorithmId::Rsa4096 => true, KeyAlgorithmId::EccP256 | KeyAlgorithmId::EccP384 | KeyAlgorithmId::EccP521 => false, } } pub fn is_ecc(&self) -> bool { match self { - KeyAlgorithmId::Rsa1024 | KeyAlgorithmId::Rsa2048 - | KeyAlgorithmId::Rsa3072 | KeyAlgorithmId::Rsa4096 => false, + KeyAlgorithmId::Rsa1024 + | KeyAlgorithmId::Rsa2048 + | KeyAlgorithmId::Rsa3072 + | KeyAlgorithmId::Rsa4096 => false, KeyAlgorithmId::EccP256 | KeyAlgorithmId::EccP384 | KeyAlgorithmId::EccP521 => true, } } + + pub fn to_jwa_name(&self) -> &str { + match self { + KeyAlgorithmId::Rsa1024 + | KeyAlgorithmId::Rsa2048 + | KeyAlgorithmId::Rsa3072 + | KeyAlgorithmId::Rsa4096 => "RS256", + KeyAlgorithmId::EccP256 => "ES256,", + KeyAlgorithmId::EccP384 => "ES384", + KeyAlgorithmId::EccP521 => "ES512", + } + } } impl FromStr for KeyAlgorithmId { @@ -206,6 +238,14 @@ pub struct YubikeyHmacEncSoftKey { pub hmac_enc_private_key: String, } +#[allow(dead_code)] +#[derive(Debug)] +pub struct ExternalCommandKey { + pub external_command: String, + pub algorithm: KeyAlgorithmId, + pub parameter: String, +} + pub fn parse_key_uri(key_uri: &str) -> XResult { let regex = Regex::new(r##"^key://([0-9a-zA-Z\-\._]*):(\w+)/(\w+):((?:\w+)?):(.*)$"##).unwrap(); let captures = match regex.captures(key_uri) { @@ -277,6 +317,28 @@ pub fn parse_key_uri(key_uri: &str) -> XResult { debugging!("Parsed key uri: {:?}", parsed_key_uri); Ok(parsed_key_uri) } + "external_command" => { + if "" != usage { + return simple_error!("Key uri's usage must be empty."); + } + let external_command = opt_result!( + percent_encoding::percent_decode_str(host_or_name).decode_utf8(), + "Decode external command failed: {}" + ); + let algorithm = opt_value_result!( + KeyAlgorithmId::from_str(algorithm), + "Invalid algorithm id: {}", + algorithm + ); + let parameter = left_part.to_string(); + let parsed_key_uri = KeyUri::ExternalCommandKey(ExternalCommandKey { + external_command: external_command.to_string(), + algorithm, + parameter, + }); + debugging!("Parsed key uri: {:?}", parsed_key_uri); + Ok(parsed_key_uri) + } _ => simple_error!("Key uri's module must be se."), } }