feat: v1.12.0

This commit is contained in:
2025-05-01 00:22:42 +08:00
parent 3af863762f
commit c270c2e369
14 changed files with 383 additions and 103 deletions

2
Cargo.lock generated
View File

@@ -508,7 +508,7 @@ dependencies = [
[[package]] [[package]]
name = "card-cli" name = "card-cli"
version = "1.11.17" version = "1.12.0"
dependencies = [ dependencies = [
"aes-gcm-stream", "aes-gcm-stream",
"authenticator 0.3.1", "authenticator 0.3.1",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "card-cli" name = "card-cli"
version = "1.11.17" version = "1.12.0"
authors = ["Hatter Jiang <jht5945@gmail.com>"] authors = ["Hatter Jiang <jht5945@gmail.com>"]
edition = "2018" edition = "2018"

View File

@@ -5,7 +5,7 @@ use std::io::Read;
use clap::ArgMatches; use clap::ArgMatches;
use rust_util::XResult; use rust_util::XResult;
use crate::digestutil::{sha256, sha256_bytes}; use crate::digestutil::DigestAlgorithm;
pub fn get_sha256_digest_or_hash(sub_arg_matches: &ArgMatches) -> XResult<Vec<u8>> { pub fn get_sha256_digest_or_hash(sub_arg_matches: &ArgMatches) -> XResult<Vec<u8>> {
@@ -13,6 +13,10 @@ pub fn get_sha256_digest_or_hash(sub_arg_matches: &ArgMatches) -> XResult<Vec<u8
} }
pub fn get_sha256_digest_or_hash_with_file_opt(sub_arg_matches: &ArgMatches, file_opt: &Option<String>) -> XResult<Vec<u8>> { pub fn get_sha256_digest_or_hash_with_file_opt(sub_arg_matches: &ArgMatches, file_opt: &Option<String>) -> XResult<Vec<u8>> {
get_digest_or_hash_with_file_opt(sub_arg_matches, file_opt, DigestAlgorithm::Sha256)
}
pub fn get_digest_or_hash_with_file_opt(sub_arg_matches: &ArgMatches, file_opt: &Option<String>, digest: DigestAlgorithm) -> XResult<Vec<u8>> {
let file_opt = file_opt.as_ref().map(String::as_str); let file_opt = file_opt.as_ref().map(String::as_str);
if let Some(file) = sub_arg_matches.value_of("file").or(file_opt) { if let Some(file) = sub_arg_matches.value_of("file").or(file_opt) {
let metadata = opt_result!(fs::metadata(file), "Read file: {} metadata filed: {}", file); let metadata = opt_result!(fs::metadata(file), "Read file: {} metadata filed: {}", file);
@@ -28,9 +32,9 @@ pub fn get_sha256_digest_or_hash_with_file_opt(sub_arg_matches: &ArgMatches, fil
let mut f = opt_result!(File::open(file), "Open file: {} failed: {}", file); let mut f = opt_result!(File::open(file), "Open file: {} failed: {}", file);
let mut content = vec![]; let mut content = vec![];
opt_result!(f.read_to_end(&mut content), "Read file: {} failed: {}", file); opt_result!(f.read_to_end(&mut content), "Read file: {} failed: {}", file);
Ok(sha256_bytes(&content)) Ok(digest.digest(&content))
} else if let Some(input) = sub_arg_matches.value_of("input") { } else if let Some(input) = sub_arg_matches.value_of("input") {
Ok(sha256(input)) Ok(digest.digest_str(input))
} else if let Some(hash_hex) = sub_arg_matches.value_of("hash-hex") { } else if let Some(hash_hex) = sub_arg_matches.value_of("hash-hex") {
Ok(opt_result!(hex::decode(hash_hex), "Parse hash-hex failed: {}")) Ok(opt_result!(hex::decode(hash_hex), "Parse hash-hex failed: {}"))
} else { } else {

View File

@@ -1,8 +1,15 @@
use crate::util; use crate::keyutil::{parse_key_uri, KeyUri, KeyUsage};
use clap::{App, Arg, ArgMatches, SubCommand}; use crate::pivutil::slot_equals;
use crate::util::base64_encode;
use crate::{cmdutil, seutil, util};
use clap::{App, ArgMatches, SubCommand};
use ecdsa::elliptic_curve::pkcs8::der::Encode;
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use rust_util::XResult;
use serde_json::Value; use serde_json::Value;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use x509_parser::parse_x509_certificate;
use yubikey::{Key, YubiKey};
pub struct CommandImpl; pub struct CommandImpl;
@@ -14,25 +21,54 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> { fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()) SubCommand::with_name(self.name())
.about("External public key subcommand") .about("External public key subcommand")
.arg( .arg(cmdutil::build_parameter_arg())
Arg::with_name("parameter")
.long("parameter")
.takes_value(true)
.required(true)
.help("Parameter"),
)
} }
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let _parameter = sub_arg_matches.value_of("parameter").unwrap(); let parameter = sub_arg_matches.value_of("parameter").unwrap();
// TODO do get public key
let mut json = BTreeMap::new(); let mut json = BTreeMap::new();
json.insert("success", Value::Bool(true)); match fetch_public_key(parameter) {
json.insert("public_key_base64", "**".into()); Ok(public_key_bytes) => {
json.insert("success", Value::Bool(true));
json.insert("public_key_base64", base64_encode(&public_key_bytes).into());
}
Err(e) => {
json.insert("success", Value::Bool(false));
json.insert("error", e.to_string().into());
}
}
util::print_pretty_json(&json); util::print_pretty_json(&json);
Ok(None) Ok(None)
} }
} }
fn fetch_public_key(parameter: &str) -> XResult<Vec<u8>> {
let key_uri = parse_key_uri(parameter)?;
match key_uri {
KeyUri::SecureEnclaveKey(key) => {
if key.usage != KeyUsage::Singing {
simple_error!("Not singing key")
} else {
let (_, public_key_der, _) =
seutil::recover_secure_enclave_p256_public_key(&key.private_key, true)?;
Ok(public_key_der)
}
}
KeyUri::YubikeyPivKey(key) => {
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
let keys = opt_result!(Key::list(&mut yk), "List keys failed: {}");
for k in &keys {
let slot_str = format!("{:x}", Into::<u8>::into(k.slot()));
if slot_equals(&key.slot, &slot_str) {
let cert_der = k.certificate().cert.to_der()?;
let x509_certificate = parse_x509_certificate(cert_der.as_slice()).unwrap().1;
let public_key_bytes = x509_certificate.public_key().raw;
return Ok(public_key_bytes.to_vec());
}
}
simple_error!("Slot {} not found", key.slot)
}
}
}

View File

@@ -1,8 +1,16 @@
use crate::util; use crate::cmd_sign_jwt::digest_by_jwt_algorithm;
use clap::{App, Arg, ArgMatches, SubCommand}; use crate::keyutil::{parse_key_uri, KeyUri, KeyUsage};
use crate::pivutil::ToStr;
use crate::util::{base64_decode, base64_encode};
use crate::{cmdutil, pivutil, seutil, util};
use clap::{App, ArgMatches, SubCommand};
use jwt::AlgorithmType;
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use rust_util::XResult;
use serde_json::Value; use serde_json::Value;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use yubikey::piv::{sign_data, AlgorithmId};
use yubikey::YubiKey;
pub struct CommandImpl; pub struct CommandImpl;
@@ -14,41 +22,85 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> { fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()) SubCommand::with_name(self.name())
.about("External sign subcommand") .about("External sign subcommand")
.arg( .arg(cmdutil::build_alg_arg())
Arg::with_name("alg") .arg(cmdutil::build_parameter_arg())
.long("alg") .arg(cmdutil::build_message_arg())
.takes_value(true) .arg(cmdutil::build_pin_arg())
.required(true)
.help("Algorithm, e.g. RS256, RS384, RS512, ES256, ES384, ES512"),
)
.arg(
Arg::with_name("parameter")
.long("parameter")
.takes_value(true)
.required(true)
.help("Parameter"),
)
.arg(
Arg::with_name("message-base64")
.long("message-base64")
.takes_value(true)
.required(true)
.help("Message in base64"),
)
} }
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let _alg = sub_arg_matches.value_of("alg").unwrap();
let _parameter = sub_arg_matches.value_of("parameter").unwrap();
let _message_base64 = sub_arg_matches.value_of("message-base64").unwrap();
// TODO do sign
let mut json = BTreeMap::new(); let mut json = BTreeMap::new();
json.insert("success", Value::Bool(true)); match sign(sub_arg_matches) {
json.insert("signature_base64", "**".into()); Ok(signature_bytes) => {
json.insert("success", Value::Bool(true));
json.insert("signature_base64", base64_encode(&signature_bytes).into());
}
Err(e) => {
json.insert("success", Value::Bool(false));
json.insert("error", e.to_string().into());
}
}
util::print_pretty_json(&json); util::print_pretty_json(&json);
Ok(None) Ok(None)
} }
} }
fn sign(sub_arg_matches: &ArgMatches) -> XResult<Vec<u8>> {
let alg = sub_arg_matches.value_of("alg").unwrap();
let parameter = sub_arg_matches.value_of("parameter").unwrap();
let message_base64 = sub_arg_matches.value_of("message-base64").unwrap();
let key_uri = parse_key_uri(parameter)?;
let message_bytes = base64_decode(message_base64)?;
match key_uri {
KeyUri::SecureEnclaveKey(key) => {
if key.usage != KeyUsage::Singing {
simple_error!("Not singing key")
} else {
Ok(seutil::secure_enclave_p256_sign(
&key.private_key,
&message_bytes,
)?)
}
}
KeyUri::YubikeyPivKey(key) => {
let mut yk = opt_result!(YubiKey::open(), "Find YubiKey failed: {}");
let pin_opt = pivutil::check_read_pin(&mut yk, key.slot, sub_arg_matches);
// FIXME Check Yubikey slot algorithm
let jwt_algorithm = match alg {
"ES256" => AlgorithmType::Es256,
"ES384" => AlgorithmType::Es384,
"RS256" => AlgorithmType::Rs256,
_ => return simple_error!("Invalid alg: {}", alg),
};
let is_p256_mismatch =
key.algorithm == AlgorithmId::EccP256 && jwt_algorithm != AlgorithmType::Es256;
let is_p384_mismatch =
key.algorithm == AlgorithmId::EccP384 && jwt_algorithm != AlgorithmType::Es384;
let is_rsa =
key.algorithm == AlgorithmId::Rsa1024 || key.algorithm == AlgorithmId::Rsa2048;
let is_rsa_mismatch = is_rsa && jwt_algorithm != AlgorithmType::Rs256;
if is_p256_mismatch || is_p384_mismatch || is_rsa_mismatch {
return simple_error!("Invalid algorithm: {} vs {}", key.algorithm.to_str(), alg);
}
let raw_in = digest_by_jwt_algorithm(jwt_algorithm, &message_bytes)?;
if let Some(pin) = pin_opt {
opt_result!(
yk.verify_pin(pin.as_bytes()),
"YubiKey verify pin failed: {}"
);
}
let signed_data = opt_result!(
sign_data(&mut yk, &raw_in, key.algorithm, key.slot),
"Sign YubiKey failed: {}"
);
Ok(signed_data.to_vec())
}
}
}

View File

@@ -10,6 +10,7 @@ use yubikey::{Key, YubiKey};
use yubikey::piv::{AlgorithmId, metadata}; use yubikey::piv::{AlgorithmId, metadata};
use crate::{cmdutil, pivutil, util}; use crate::{cmdutil, pivutil, util};
use crate::keyutil::{KeyUri, YubikeyPivKey};
use crate::pivutil::{get_algorithm_id_by_certificate, slot_equals, ToStr}; use crate::pivutil::{get_algorithm_id_by_certificate, slot_equals, ToStr};
use crate::pkiutil::bytes_to_pem; use crate::pkiutil::bytes_to_pem;
use crate::sshutil::SshVecWriter; use crate::sshutil::SshVecWriter;
@@ -96,11 +97,18 @@ impl Command for CommandImpl {
ssh_public_key.write_string(format!("nistp{}", ec_bit_len).as_bytes()); ssh_public_key.write_string(format!("nistp{}", ec_bit_len).as_bytes());
ssh_public_key.write_string(pk_point_hex); ssh_public_key.write_string(pk_point_hex);
let ssh_public_key_str = format!( let ssh_public_key_str = format!(
"ecdsa-sha2-nistp{} {} PIV:{}", ec_bit_len, base64_encode(ssh_public_key), slot_id); "ecdsa-sha2-nistp{} {} Yubikey-PIV-{}", ec_bit_len, base64_encode(ssh_public_key), slot_id);
json.insert("ssh_public_key", ssh_public_key_str.to_string()); json.insert("ssh_public_key", ssh_public_key_str.to_string());
} }
_ => {} _ => {}
} }
let yubikey_piv_key = YubikeyPivKey {
key_name: format!("yubikey{}-{}", yk.version().major, yk.serial().0),
algorithm: algorithm_id,
slot: slot_id,
};
json.insert("key_uri", KeyUri::YubikeyPivKey(yubikey_piv_key).to_string());
} }
let serial_lower = cert.serial_number.to_string().to_lowercase(); let serial_lower = cert.serial_number.to_string().to_lowercase();
json.insert("serial", if serial_lower.starts_with("00:") { serial_lower.chars().skip(3).collect() } else { serial_lower }); json.insert("serial", if serial_lower.starts_with("00:") { serial_lower.chars().skip(3).collect() } else { serial_lower });

View File

@@ -1,4 +1,4 @@
use crate::keyutil::{parse_key_uri, KeyUri}; use crate::keyutil::parse_key_uri;
use crate::{cmdutil, seutil, util}; use crate::{cmdutil, seutil, util};
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use p256::elliptic_curve::sec1::FromEncodedPoint; use p256::elliptic_curve::sec1::FromEncodedPoint;
@@ -41,7 +41,8 @@ impl Command for CommandImpl {
let key = sub_arg_matches.value_of("key").unwrap(); let key = sub_arg_matches.value_of("key").unwrap();
let epk = sub_arg_matches.value_of("epk").unwrap(); let epk = sub_arg_matches.value_of("epk").unwrap();
let KeyUri::SecureEnclaveKey(se_key_uri) = parse_key_uri(key)?; let key_uri = parse_key_uri(key)?;
let se_key_uri = key_uri.as_secure_enclave_key()?;
debugging!("Secure enclave key URI: {:?}", se_key_uri); debugging!("Secure enclave key URI: {:?}", se_key_uri);
let ephemeral_public_key_der_bytes = if epk.starts_with("04") { let ephemeral_public_key_der_bytes = if epk.starts_with("04") {

View File

@@ -1,4 +1,4 @@
use crate::keyutil::{parse_key_uri, KeyUri}; use crate::keyutil::parse_key_uri;
use crate::{cmdutil, seutil, util}; use crate::{cmdutil, seutil, util};
use crate::util::{base64_decode, base64_encode}; use crate::util::{base64_decode, base64_encode};
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
@@ -51,7 +51,8 @@ impl Command for CommandImpl {
Some(input) => input.as_bytes().to_vec(), Some(input) => input.as_bytes().to_vec(),
}; };
let KeyUri::SecureEnclaveKey(se_key_uri) = parse_key_uri(key)?; let key_uri = parse_key_uri(key)?;
let se_key_uri = key_uri.as_secure_enclave_key()?;
debugging!("Secure enclave key URI: {:?}", se_key_uri); debugging!("Secure enclave key URI: {:?}", se_key_uri);
let signature = seutil::secure_enclave_p256_sign(&se_key_uri.private_key, &input_bytes)?; let signature = seutil::secure_enclave_p256_sign(&se_key_uri.private_key, &input_bytes)?;

View File

@@ -1,5 +1,5 @@
use crate::cmd_se_generate::print_se_key; use crate::cmd_se_generate::print_se_key;
use crate::keyutil::{parse_key_uri, KeyUri, KeyUsage}; use crate::keyutil::{parse_key_uri, KeyUsage};
use crate::{cmdutil, seutil}; use crate::{cmdutil, seutil};
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
@@ -28,9 +28,9 @@ impl Command for CommandImpl {
let json_output = cmdutil::check_json_output(sub_arg_matches); let json_output = cmdutil::check_json_output(sub_arg_matches);
seutil::check_se_supported()?; seutil::check_se_supported()?;
let key_uri = sub_arg_matches.value_of("key").unwrap(); let key = sub_arg_matches.value_of("key").unwrap();
let key_uri = parse_key_uri(key)?;
let KeyUri::SecureEnclaveKey(se_key_uri) = parse_key_uri(key_uri)?; let se_key_uri = key_uri.as_secure_enclave_key()?;
debugging!("Secure enclave key URI: {:?}", se_key_uri); debugging!("Secure enclave key URI: {:?}", se_key_uri);
let (public_key_point, public_key_der, _private_key) = let (public_key_point, public_key_der, _private_key) =
@@ -39,7 +39,7 @@ impl Command for CommandImpl {
se_key_uri.usage == KeyUsage::Singing, se_key_uri.usage == KeyUsage::Singing,
)?; )?;
print_se_key(json_output, &public_key_point, &public_key_der, key_uri); print_se_key(json_output, &public_key_point, &public_key_der, key);
Ok(None) Ok(None)
} }

View File

@@ -7,7 +7,7 @@ use serde_json::{Map, Value};
use crate::cmd_sign_jwt::{build_jwt_parts, merge_header_claims, merge_payload_claims, print_jwt_token}; use crate::cmd_sign_jwt::{build_jwt_parts, merge_header_claims, merge_payload_claims, print_jwt_token};
use crate::ecdsautil::parse_ecdsa_to_rs; use crate::ecdsautil::parse_ecdsa_to_rs;
use crate::keyutil::{parse_key_uri, KeyUri}; use crate::keyutil::parse_key_uri;
use crate::{cmd_sign_jwt, cmdutil, hmacutil, util}; use crate::{cmd_sign_jwt, cmdutil, hmacutil, util};
use crate::util::base64_decode; use crate::util::base64_decode;
@@ -35,7 +35,8 @@ impl Command for CommandImpl {
"Private key PKCS#8 DER base64 encoded or PEM" "Private key PKCS#8 DER base64 encoded or PEM"
); );
let private_key = hmacutil::try_hmac_decrypt_to_string(private_key)?; let private_key = hmacutil::try_hmac_decrypt_to_string(private_key)?;
let KeyUri::SecureEnclaveKey(se_key_uri) = parse_key_uri(&private_key)?; let key_uri = parse_key_uri(&private_key)?;
let se_key_uri = key_uri.as_secure_enclave_key()?;
debugging!("Secure enclave key URI: {:?}", se_key_uri); debugging!("Secure enclave key URI: {:?}", se_key_uri);
let (header, payload, jwt_claims) = build_jwt_parts(sub_arg_matches)?; let (header, payload, jwt_claims) = build_jwt_parts(sub_arg_matches)?;

View File

@@ -13,6 +13,18 @@ pub fn build_pin_arg() -> Arg<'static, 'static> {
Arg::with_name("pin").short("p").long("pin").takes_value(true).help("PIV card user PIN") Arg::with_name("pin").short("p").long("pin").takes_value(true).help("PIV card user PIN")
} }
pub fn build_alg_arg() -> Arg<'static, 'static> {
Arg::with_name("alg").long("alg").takes_value(true).required(true).help("Algorithm, e.g. RS256, ES256, ES384")
}
pub fn build_parameter_arg() -> Arg<'static, 'static> {
Arg::with_name("parameter").long("parameter").takes_value(true).required(true).help("Parameter")
}
pub fn build_message_arg() -> Arg<'static, 'static> {
Arg::with_name("message-base64").long("message-base64").takes_value(true).required(true).help("Message in base64")
}
pub fn build_no_pin_arg() -> Arg<'static, 'static> { pub fn build_no_pin_arg() -> Arg<'static, 'static> {
Arg::with_name("no-pin").long("no-pin").help("No PIN") Arg::with_name("no-pin").long("no-pin").help("No PIN")
} }

View File

@@ -1,6 +1,25 @@
use sha1::Sha1; use sha1::Sha1;
use sha2::{Digest, Sha256, Sha384, Sha512}; use sha2::{Digest, Sha256, Sha384, Sha512};
pub enum DigestAlgorithm {
Sha256,
#[allow(dead_code)]
Sha384,
}
impl DigestAlgorithm {
pub fn digest(&self, data: &[u8]) -> Vec<u8> {
match self {
DigestAlgorithm::Sha256 => sha256_bytes(data),
DigestAlgorithm::Sha384 => sha384_bytes(data),
}
}
pub fn digest_str(&self, s: &str) -> Vec<u8> {
self.digest(s.as_bytes())
}
}
pub fn sha256(input: &str) -> Vec<u8> { pub fn sha256(input: &str) -> Vec<u8> {
sha256_bytes(input.as_bytes()) sha256_bytes(input.as_bytes())
} }

View File

@@ -1,29 +1,50 @@
use regex::Regex; use regex::Regex;
use rust_util::XResult; use rust_util::XResult;
use yubikey::piv::{AlgorithmId, SlotId};
use crate::pivutil::{ToStr, FromStr};
// reference: https://git.hatter.ink/hatter/card-cli/issues/6 // reference: https://git.hatter.ink/hatter/card-cli/issues/6
#[derive(Debug)] #[derive(Debug)]
pub enum KeyUri { pub enum KeyUri {
SecureEnclaveKey(SecureEnclaveKey), SecureEnclaveKey(SecureEnclaveKey),
YubikeyPivKey(YubikeyPivKey),
} }
// #[derive(Debug, PartialEq, Eq)] impl KeyUri {
// pub enum KeyModule { pub fn as_secure_enclave_key(&self) -> XResult<&SecureEnclaveKey> {
// SecureEnclave, match self {
// OpenPgpCard, KeyUri::SecureEnclaveKey(key) => Ok(key),
// PersonalIdentityVerification, _ => simple_error!("Not a secure enclave key."),
// } }
// }
// impl KeyModule { }
// pub fn from(module: &str) -> Option<Self> {
// match module { impl ToString for KeyUri {
// "se" => Some(Self::SecureEnclave), fn to_string(&self) -> String {
// "pgp" => Some(Self::OpenPgpCard), let mut key_uri = String::with_capacity(64);
// "piv" => Some(Self::PersonalIdentityVerification), key_uri.push_str("key://");
// _ => None, match self {
// } // key://hatter-mac-pro:se/p256:signing:BASE64(dataRepresentation)
// } // key://hatter-mac-pro:se/p256:key_agreement:BASE64(dataRepresentation)
// } KeyUri::SecureEnclaveKey(key) => {
key_uri.push_str(&key.host);
key_uri.push_str(":se/p256:");
key_uri.push_str(&key.usage.to_string());
key_uri.push_str(":");
key_uri.push_str(&key.private_key);
}
// key://yubikey-5n:piv/p256:*:9a
KeyUri::YubikeyPivKey(key) => {
key_uri.push_str(&key.key_name);
key_uri.push_str(":piv/");
key_uri.push_str(key.algorithm.to_str());
key_uri.push_str("::");
key_uri.push_str(key.slot.to_str());
}
}
key_uri
}
}
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum KeyUsage { pub enum KeyUsage {
@@ -43,6 +64,16 @@ impl KeyUsage {
} }
} }
impl ToString for KeyUsage {
fn to_string(&self) -> String {
match self {
KeyUsage::Any => "*",
KeyUsage::Singing => "signing",
KeyUsage::KeyAgreement => "key_agreement"
}.to_string()
}
}
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub struct SecureEnclaveKey { pub struct SecureEnclaveKey {
@@ -51,64 +82,110 @@ pub struct SecureEnclaveKey {
pub private_key: String, pub private_key: String,
} }
#[allow(dead_code)]
#[derive(Debug)]
pub struct YubikeyPivKey {
pub key_name: String,
pub algorithm: AlgorithmId,
pub slot: SlotId,
}
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://([a-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) {
None => return simple_error!("Invalid key uri: {}", key_uri), None => return simple_error!("Invalid key uri: {}", key_uri),
Some(captures) => captures, Some(captures) => captures,
}; };
let host = captures.get(1).unwrap().as_str(); let host_or_name = captures.get(1).unwrap().as_str();
let module = captures.get(2).unwrap().as_str(); let module = captures.get(2).unwrap().as_str();
let algorithm = captures.get(3).unwrap().as_str(); let algorithm = captures.get(3).unwrap().as_str();
let usage = captures.get(4).unwrap().as_str(); let usage = captures.get(4).unwrap().as_str();
let left_part = captures.get(5).unwrap().as_str(); let left_part = captures.get(5).unwrap().as_str();
if "se" != module { match module {
return simple_error!("Key uri's module must be se."); "se" => {
} if "p256" != algorithm {
if "p256" != algorithm { return simple_error!("Key uri's algorithm must be p256.");
return simple_error!("Key uri's algorithm must be p256."); }
} let key_usage = match KeyUsage::from(usage) {
let key_usage = match KeyUsage::from(usage) { None | Some(KeyUsage::Any) => {
None | Some(KeyUsage::Any) => { return simple_error!("Key uri's usage must be signing or key_agreement.")
return simple_error!("Key uri's usage must be signing or key_agreement.") }
Some(key_usage) => key_usage,
};
let parsed_key_uri = KeyUri::SecureEnclaveKey(SecureEnclaveKey {
host: host_or_name.to_string(),
usage: key_usage,
private_key: left_part.to_string(),
});
debugging!("Parsed key uri: {:?}", parsed_key_uri);
Ok(parsed_key_uri)
} }
Some(key_usage) => key_usage, "piv" => {
}; if "" != usage {
return simple_error!("Key uri's usage must be empty.");
let parsed_key_uri = KeyUri::SecureEnclaveKey(SecureEnclaveKey { }
host: host.to_string(), let algorithm = opt_value_result!(AlgorithmId::from_str(algorithm), "Invalid algorithm id: {}", algorithm);
usage: key_usage, let slot = opt_value_result!(SlotId::from_str(left_part), "Invalid slot id: {}", left_part);
private_key: left_part.to_string(), let parsed_key_uri = KeyUri::YubikeyPivKey(YubikeyPivKey {
}); key_name: host_or_name.to_string(),
algorithm,
debugging!("Parsed key uri: {:?}", parsed_key_uri); slot,
Ok(parsed_key_uri) });
debugging!("Parsed key uri: {:?}", parsed_key_uri);
Ok(parsed_key_uri)
}
_ => simple_error!("Key uri's module must be se."),
}
} }
#[test] #[test]
fn test_parse_key_uri_01() { fn test_parse_key_uri_01() {
let se_key_uri = let se_key_uri =
parse_key_uri("key://hatter-mac-pro:se/p256:signing:BASE64(dataRepresentation)").unwrap(); parse_key_uri("key://hatter-mac-pro:se/p256:signing:BASE64(dataRepresentation)").unwrap();
assert_eq!("key://hatter-mac-pro:se/p256:signing:BASE64(dataRepresentation)", se_key_uri.to_string());
match se_key_uri { match se_key_uri {
KeyUri::SecureEnclaveKey(se_key_uri) => { KeyUri::SecureEnclaveKey(se_key_uri) => {
assert_eq!("hatter-mac-pro", se_key_uri.host); assert_eq!("hatter-mac-pro", se_key_uri.host);
assert_eq!(KeyUsage::Singing, se_key_uri.usage); assert_eq!(KeyUsage::Singing, se_key_uri.usage);
assert_eq!("BASE64(dataRepresentation)", se_key_uri.private_key); assert_eq!("BASE64(dataRepresentation)", se_key_uri.private_key);
} }
_ => {
panic!("Key uri not parsed")
}
} }
} }
#[test] #[test]
fn test_parse_key_uri_02() { fn test_parse_key_uri_02() {
let se_key_uri = let se_key_uri =
parse_key_uri("key://hatter-mac-pro:se/p256:key_agreement:BASE64(dataRepresentation)") parse_key_uri("key://hatter-mac-m1:se/p256:key_agreement:BASE64(dataRepresentation)")
.unwrap(); .unwrap();
assert_eq!("key://hatter-mac-m1:se/p256:key_agreement:BASE64(dataRepresentation)", se_key_uri.to_string());
match se_key_uri { match se_key_uri {
KeyUri::SecureEnclaveKey(se_key_uri) => { KeyUri::SecureEnclaveKey(se_key_uri) => {
assert_eq!("hatter-mac-pro", se_key_uri.host); assert_eq!("hatter-mac-m1", se_key_uri.host);
assert_eq!(KeyUsage::KeyAgreement, se_key_uri.usage); assert_eq!(KeyUsage::KeyAgreement, se_key_uri.usage);
assert_eq!("BASE64(dataRepresentation)", se_key_uri.private_key); assert_eq!("BASE64(dataRepresentation)", se_key_uri.private_key);
} }
_ => {
panic!("Key uri not parsed")
}
}
}
#[test]
fn test_parse_key_uri_03() {
let se_key_uri = parse_key_uri("key://yubikey-5n:piv/p256::9a").unwrap();
assert_eq!("key://yubikey-5n:piv/p256::authentication", se_key_uri.to_string());
match se_key_uri {
KeyUri::YubikeyPivKey(piv_key_uri) => {
assert_eq!("yubikey-5n", piv_key_uri.key_name);
assert_eq!(AlgorithmId::EccP256, piv_key_uri.algorithm);
assert_eq!(SlotId::Authentication, piv_key_uri.slot);
}
_ => {
panic!("Key uri not parsed")
}
} }
} }

View File

@@ -56,6 +56,12 @@ pub trait ToStr {
fn to_str(&self) -> &str; fn to_str(&self) -> &str;
} }
pub trait FromStr {
fn from_str(s: &str) -> Option<Self>
where
Self: Sized;
}
impl ToStr for PinPolicy { impl ToStr for PinPolicy {
fn to_str(&self) -> &str { fn to_str(&self) -> &str {
match self { match self {
@@ -78,6 +84,21 @@ impl ToStr for TouchPolicy {
} }
} }
impl FromStr for AlgorithmId {
fn from_str(s: &str) -> Option<Self>
where
Self: Sized,
{
match s {
"rsa1024" => Some(AlgorithmId::Rsa1024),
"rsa2048" => Some(AlgorithmId::Rsa2048),
"p256" => Some(AlgorithmId::EccP256),
"p384" => Some(AlgorithmId::EccP384),
_ => None,
}
}
}
impl ToStr for AlgorithmId { impl ToStr for AlgorithmId {
fn to_str(&self) -> &str { fn to_str(&self) -> &str {
match self { match self {
@@ -194,6 +215,54 @@ pub fn get_slot_id(slot: &str) -> XResult<SlotId> {
}) })
} }
impl FromStr for SlotId {
fn from_str(s: &str) -> Option<Self>
where
Self: Sized,
{
get_slot_id(s).ok()
}
}
impl ToStr for SlotId {
fn to_str(&self) -> &str {
match self {
SlotId::Authentication => "authentication",
SlotId::Signature => "signature",
SlotId::KeyManagement => "keymanagement",
SlotId::CardAuthentication => "cardauthentication",
SlotId::Retired(retried) => match retried {
RetiredSlotId::R1 => "r1",
RetiredSlotId::R2 => "r2",
RetiredSlotId::R3 => "r3",
RetiredSlotId::R4 => "r4",
RetiredSlotId::R5 => "r5",
RetiredSlotId::R6 => "r6",
RetiredSlotId::R7 => "r7",
RetiredSlotId::R8 => "r8",
RetiredSlotId::R9 => "r9",
RetiredSlotId::R10 => "r10",
RetiredSlotId::R11 => "r11",
RetiredSlotId::R12 => "r12",
RetiredSlotId::R13 => "r13",
RetiredSlotId::R14 => "r14",
RetiredSlotId::R15 => "r15",
RetiredSlotId::R16 => "r16",
RetiredSlotId::R17 => "r17",
RetiredSlotId::R18 => "r18",
RetiredSlotId::R19 => "r19",
RetiredSlotId::R20 => "r20",
}
SlotId::Attestation => "attestation",
SlotId::Management(management) => match management {
ManagementSlotId::Pin => "pin",
ManagementSlotId::Puk => "puk",
ManagementSlotId::Management => "management",
}
}
}
}
pub fn check_read_pin(yk: &mut YubiKey, slot_id: SlotId, sub_arg_matches: &ArgMatches) -> Option<String> { pub fn check_read_pin(yk: &mut YubiKey, slot_id: SlotId, sub_arg_matches: &ArgMatches) -> Option<String> {
if never_use_pin(yk, slot_id) { if never_use_pin(yk, slot_id) {
None None