feat: v1.10.10, se-ecdh and se-ecsign
This commit is contained in:
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "card-cli"
|
||||
version = "1.10.9"
|
||||
version = "1.10.10"
|
||||
authors = ["Hatter Jiang <jht5945@gmail.com>"]
|
||||
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"
|
||||
|
||||
66
src/cmd_se_ecdh.rs
Normal file
66
src/cmd_se_ecdh.rs
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
76
src/cmd_se_ecsign.rs
Normal file
76
src/cmd_se_ecsign.rs
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
110
src/keyutil.rs
Normal file
110
src/keyutil.rs
Normal file
@@ -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<Self> {
|
||||
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<Self> {
|
||||
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<KeyUri> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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![];
|
||||
|
||||
@@ -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<u8>, 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<Vec<u8>> {
|
||||
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::<String>();
|
||||
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<Vec<u8>> {
|
||||
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::<String>();
|
||||
Ok(base64_decode(&signature)?)
|
||||
}
|
||||
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user