feat: 1.13.12, support external command

This commit is contained in:
2025-05-24 10:31:13 +08:00
parent bb8d804505
commit 87e51cc7e4
7 changed files with 121 additions and 20 deletions

17
Cargo.lock generated
View File

@@ -508,7 +508,7 @@ dependencies = [
[[package]] [[package]]
name = "card-cli" name = "card-cli"
version = "1.13.11" version = "1.13.12"
dependencies = [ dependencies = [
"aes-gcm-stream", "aes-gcm-stream",
"authenticator 0.3.1", "authenticator 0.3.1",
@@ -520,6 +520,7 @@ dependencies = [
"digest 0.10.7", "digest 0.10.7",
"ecdsa 0.16.9", "ecdsa 0.16.9",
"env_logger", "env_logger",
"external-command-rs",
"hex", "hex",
"jwt", "jwt",
"openpgp-card", "openpgp-card",
@@ -530,6 +531,7 @@ dependencies = [
"p384 0.13.1", "p384 0.13.1",
"p521", "p521",
"pem", "pem",
"percent-encoding",
"pinentry", "pinentry",
"rand 0.8.5", "rand 0.8.5",
"regex", "regex",
@@ -1133,6 +1135,19 @@ dependencies = [
"windows-sys 0.59.0", "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]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.3.0" version = "2.3.0"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "card-cli" name = "card-cli"
version = "1.13.11" version = "1.13.12"
authors = ["Hatter Jiang <jht5945@gmail.com>"] authors = ["Hatter Jiang <jht5945@gmail.com>"]
edition = "2018" edition = "2018"
@@ -60,6 +60,8 @@ u2f-hatter-fork = "0.2"
security-framework = { version = "3.0", features = ["OSX_10_15"] } security-framework = { version = "3.0", features = ["OSX_10_15"] }
rsa = "0.9.8" rsa = "0.9.8"
which = "7.0.3" which = "7.0.3"
percent-encoding = "2.3.1"
external-command-rs = "0.1.1"
#lazy_static = "1.4.0" #lazy_static = "1.4.0"
#ssh-key = "0.4.0" #ssh-key = "0.4.0"
#ctap-hid-fido2 = "2.1.3" #ctap-hid-fido2 = "2.1.3"

View File

@@ -128,5 +128,9 @@ pub fn ecdh(
simple_error!("Invalid algorithm: {}", key.algorithm.to_str()) 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, &parameter, ephemeral_public_key_bytes)
}
} }
} }

View File

@@ -98,5 +98,9 @@ fn fetch_public_key(parameter: &str, serial_opt: &Option<&str>) -> XResult<Vec<u
simple_error!("Invalid algorithm: {}", key.algorithm.to_str()) 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_public_key(&key.external_command, &parameter)
}
} }
} }

View File

@@ -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()) 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, &parameter, alg, message)?;
Ok(signature)
}
} }
} }

View File

@@ -1,5 +1,5 @@
use crate::util; use crate::util;
use clap::{App, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use serde_json::Value; use serde_json::Value;
use std::collections::BTreeMap; use std::collections::BTreeMap;
@@ -14,9 +14,16 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> { fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("External spec subcommand") 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 { fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let external_command_opt = sub_arg_matches.value_of("external-command");
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(); let mut json = BTreeMap::new();
json.insert("success", Value::Bool(true)); json.insert("success", Value::Bool(true));
json.insert( json.insert(
@@ -27,6 +34,7 @@ impl Command for CommandImpl {
json.insert("commands", vec!["external_public_key", "external_sign", "external_ecdh"].into()); json.insert("commands", vec!["external_public_key", "external_sign", "external_ecdh"].into());
util::print_pretty_json(&json); util::print_pretty_json(&json);
}
Ok(None) Ok(None)
} }
} }

View File

@@ -1,5 +1,6 @@
use jwt::AlgorithmType;
use crate::pivutil::{FromStr, ToStr}; use crate::pivutil::{FromStr, ToStr};
use jwt::AlgorithmType;
use percent_encoding::NON_ALPHANUMERIC;
use regex::Regex; use regex::Regex;
use rust_util::XResult; use rust_util::XResult;
use yubikey::piv::{AlgorithmId, SlotId}; use yubikey::piv::{AlgorithmId, SlotId};
@@ -10,6 +11,7 @@ pub enum KeyUri {
SecureEnclaveKey(SecureEnclaveKey), SecureEnclaveKey(SecureEnclaveKey),
YubikeyPivKey(YubikeyPivKey), YubikeyPivKey(YubikeyPivKey),
YubikeyHmacEncSoftKey(YubikeyHmacEncSoftKey), YubikeyHmacEncSoftKey(YubikeyHmacEncSoftKey),
ExternalCommandKey(ExternalCommandKey),
} }
impl KeyUri { impl KeyUri {
@@ -25,10 +27,13 @@ impl KeyUri {
KeyUri::SecureEnclaveKey(_) => return AlgorithmType::Es256, KeyUri::SecureEnclaveKey(_) => return AlgorithmType::Es256,
KeyUri::YubikeyPivKey(key) => key.algorithm, KeyUri::YubikeyPivKey(key) => key.algorithm,
KeyUri::YubikeyHmacEncSoftKey(key) => key.algorithm, KeyUri::YubikeyHmacEncSoftKey(key) => key.algorithm,
KeyUri::ExternalCommandKey(key) => key.algorithm,
}; };
match algorithm_id { match algorithm_id {
KeyAlgorithmId::Rsa1024 | KeyAlgorithmId::Rsa2048 KeyAlgorithmId::Rsa1024
| KeyAlgorithmId::Rsa3072 | KeyAlgorithmId::Rsa4096 => AlgorithmType::Rs256, | KeyAlgorithmId::Rsa2048
| KeyAlgorithmId::Rsa3072
| KeyAlgorithmId::Rsa4096 => AlgorithmType::Rs256,
KeyAlgorithmId::EccP256 => AlgorithmType::Es256, KeyAlgorithmId::EccP256 => AlgorithmType::Es256,
KeyAlgorithmId::EccP384 => AlgorithmType::Es384, KeyAlgorithmId::EccP384 => AlgorithmType::Es384,
KeyAlgorithmId::EccP521 => AlgorithmType::Es512, KeyAlgorithmId::EccP521 => AlgorithmType::Es512,
@@ -66,6 +71,17 @@ impl ToString for KeyUri {
key_uri.push_str("::"); key_uri.push_str("::");
key_uri.push_str(key.hmac_enc_private_key.as_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 key_uri
} }
@@ -106,19 +122,35 @@ impl KeyAlgorithmId {
pub fn is_rsa(&self) -> bool { pub fn is_rsa(&self) -> bool {
match self { match self {
KeyAlgorithmId::Rsa1024 | KeyAlgorithmId::Rsa2048 KeyAlgorithmId::Rsa1024
| KeyAlgorithmId::Rsa3072 | KeyAlgorithmId::Rsa4096 => true, | KeyAlgorithmId::Rsa2048
| KeyAlgorithmId::Rsa3072
| KeyAlgorithmId::Rsa4096 => true,
KeyAlgorithmId::EccP256 | KeyAlgorithmId::EccP384 | KeyAlgorithmId::EccP521 => false, KeyAlgorithmId::EccP256 | KeyAlgorithmId::EccP384 | KeyAlgorithmId::EccP521 => false,
} }
} }
pub fn is_ecc(&self) -> bool { pub fn is_ecc(&self) -> bool {
match self { match self {
KeyAlgorithmId::Rsa1024 | KeyAlgorithmId::Rsa2048 KeyAlgorithmId::Rsa1024
| KeyAlgorithmId::Rsa3072 | KeyAlgorithmId::Rsa4096 => false, | KeyAlgorithmId::Rsa2048
| KeyAlgorithmId::Rsa3072
| KeyAlgorithmId::Rsa4096 => false,
KeyAlgorithmId::EccP256 | KeyAlgorithmId::EccP384 | KeyAlgorithmId::EccP521 => true, 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 { impl FromStr for KeyAlgorithmId {
@@ -206,6 +238,14 @@ pub struct YubikeyHmacEncSoftKey {
pub hmac_enc_private_key: String, 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<KeyUri> { pub fn parse_key_uri(key_uri: &str) -> XResult<KeyUri> {
let regex = Regex::new(r##"^key://([0-9a-zA-Z\-\._]*):(\w+)/(\w+):((?:\w+)?):(.*)$"##).unwrap(); let regex = Regex::new(r##"^key://([0-9a-zA-Z\-\._]*):(\w+)/(\w+):((?:\w+)?):(.*)$"##).unwrap();
let captures = match regex.captures(key_uri) { let captures = match regex.captures(key_uri) {
@@ -277,6 +317,28 @@ pub fn parse_key_uri(key_uri: &str) -> XResult<KeyUri> {
debugging!("Parsed key uri: {:?}", parsed_key_uri); debugging!("Parsed key uri: {:?}", parsed_key_uri);
Ok(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."), _ => simple_error!("Key uri's module must be se."),
} }
} }