From c270c2e3695e91b6398956a1af58cab6b99ffcb5 Mon Sep 17 00:00:00 2001 From: Hatter Jiang Date: Thu, 1 May 2025 00:22:42 +0800 Subject: [PATCH] feat: v1.12.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/argsutil.rs | 10 ++- src/cmd_external_public_key.rs | 64 ++++++++++--- src/cmd_external_sign.rs | 114 ++++++++++++++++------- src/cmd_piv_meta.rs | 10 ++- src/cmd_se_ecdh.rs | 5 +- src/cmd_se_ecsign.rs | 5 +- src/cmd_se_recover.rs | 10 +-- src/cmd_sign_jwt_se.rs | 5 +- src/cmdutil.rs | 12 +++ src/digestutil.rs | 19 ++++ src/keyutil.rs | 159 ++++++++++++++++++++++++--------- src/pivutil.rs | 69 ++++++++++++++ 14 files changed, 383 insertions(+), 103 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6605f36..9a6be18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -508,7 +508,7 @@ dependencies = [ [[package]] name = "card-cli" -version = "1.11.17" +version = "1.12.0" dependencies = [ "aes-gcm-stream", "authenticator 0.3.1", diff --git a/Cargo.toml b/Cargo.toml index 2f01838..817ae2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "card-cli" -version = "1.11.17" +version = "1.12.0" authors = ["Hatter Jiang "] edition = "2018" diff --git a/src/argsutil.rs b/src/argsutil.rs index 39b00bb..2e17970 100644 --- a/src/argsutil.rs +++ b/src/argsutil.rs @@ -5,7 +5,7 @@ use std::io::Read; use clap::ArgMatches; 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> { @@ -13,6 +13,10 @@ pub fn get_sha256_digest_or_hash(sub_arg_matches: &ArgMatches) -> XResult) -> XResult> { + 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, digest: DigestAlgorithm) -> XResult> { let file_opt = file_opt.as_ref().map(String::as_str); 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); @@ -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 content = vec![]; 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") { - Ok(sha256(input)) + Ok(digest.digest_str(input)) } else if let Some(hash_hex) = sub_arg_matches.value_of("hash-hex") { Ok(opt_result!(hex::decode(hash_hex), "Parse hash-hex failed: {}")) } else { diff --git a/src/cmd_external_public_key.rs b/src/cmd_external_public_key.rs index ebf6d2a..ba4cb3d 100644 --- a/src/cmd_external_public_key.rs +++ b/src/cmd_external_public_key.rs @@ -1,8 +1,15 @@ -use crate::util; -use clap::{App, Arg, ArgMatches, SubCommand}; +use crate::keyutil::{parse_key_uri, KeyUri, KeyUsage}; +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::XResult; use serde_json::Value; use std::collections::BTreeMap; +use x509_parser::parse_x509_certificate; +use yubikey::{Key, YubiKey}; pub struct CommandImpl; @@ -14,25 +21,54 @@ impl Command for CommandImpl { fn subcommand<'a>(&self) -> App<'a, 'a> { SubCommand::with_name(self.name()) .about("External public key subcommand") - .arg( - Arg::with_name("parameter") - .long("parameter") - .takes_value(true) - .required(true) - .help("Parameter"), - ) + .arg(cmdutil::build_parameter_arg()) } fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { - let _parameter = sub_arg_matches.value_of("parameter").unwrap(); - - // TODO do get public key + let parameter = sub_arg_matches.value_of("parameter").unwrap(); let mut json = BTreeMap::new(); - json.insert("success", Value::Bool(true)); - json.insert("public_key_base64", "**".into()); + match fetch_public_key(parameter) { + 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); Ok(None) } } + +fn fetch_public_key(parameter: &str) -> XResult> { + 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::::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) + } + } +} diff --git a/src/cmd_external_sign.rs b/src/cmd_external_sign.rs index 50a6ba9..9685f6c 100644 --- a/src/cmd_external_sign.rs +++ b/src/cmd_external_sign.rs @@ -1,8 +1,16 @@ -use crate::util; -use clap::{App, Arg, ArgMatches, SubCommand}; +use crate::cmd_sign_jwt::digest_by_jwt_algorithm; +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::XResult; use serde_json::Value; use std::collections::BTreeMap; +use yubikey::piv::{sign_data, AlgorithmId}; +use yubikey::YubiKey; pub struct CommandImpl; @@ -14,41 +22,85 @@ impl Command for CommandImpl { fn subcommand<'a>(&self) -> App<'a, 'a> { SubCommand::with_name(self.name()) .about("External sign subcommand") - .arg( - Arg::with_name("alg") - .long("alg") - .takes_value(true) - .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"), - ) + .arg(cmdutil::build_alg_arg()) + .arg(cmdutil::build_parameter_arg()) + .arg(cmdutil::build_message_arg()) + .arg(cmdutil::build_pin_arg()) } 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(); - json.insert("success", Value::Bool(true)); - json.insert("signature_base64", "**".into()); + match sign(sub_arg_matches) { + 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); Ok(None) } } + +fn sign(sub_arg_matches: &ArgMatches) -> XResult> { + 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()) + } + } +} diff --git a/src/cmd_piv_meta.rs b/src/cmd_piv_meta.rs index 9408d52..1e02edb 100644 --- a/src/cmd_piv_meta.rs +++ b/src/cmd_piv_meta.rs @@ -10,6 +10,7 @@ use yubikey::{Key, YubiKey}; use yubikey::piv::{AlgorithmId, metadata}; use crate::{cmdutil, pivutil, util}; +use crate::keyutil::{KeyUri, YubikeyPivKey}; use crate::pivutil::{get_algorithm_id_by_certificate, slot_equals, ToStr}; use crate::pkiutil::bytes_to_pem; 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(pk_point_hex); 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()); } _ => {} } + + 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(); json.insert("serial", if serial_lower.starts_with("00:") { serial_lower.chars().skip(3).collect() } else { serial_lower }); diff --git a/src/cmd_se_ecdh.rs b/src/cmd_se_ecdh.rs index fcee800..ac69acb 100644 --- a/src/cmd_se_ecdh.rs +++ b/src/cmd_se_ecdh.rs @@ -1,4 +1,4 @@ -use crate::keyutil::{parse_key_uri, KeyUri}; +use crate::keyutil::parse_key_uri; use crate::{cmdutil, seutil, util}; use clap::{App, Arg, ArgMatches, SubCommand}; use p256::elliptic_curve::sec1::FromEncodedPoint; @@ -41,7 +41,8 @@ impl Command for CommandImpl { let key = sub_arg_matches.value_of("key").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); let ephemeral_public_key_der_bytes = if epk.starts_with("04") { diff --git a/src/cmd_se_ecsign.rs b/src/cmd_se_ecsign.rs index a786142..8acbe7e 100644 --- a/src/cmd_se_ecsign.rs +++ b/src/cmd_se_ecsign.rs @@ -1,4 +1,4 @@ -use crate::keyutil::{parse_key_uri, KeyUri}; +use crate::keyutil::parse_key_uri; use crate::{cmdutil, seutil, util}; use crate::util::{base64_decode, base64_encode}; use clap::{App, Arg, ArgMatches, SubCommand}; @@ -51,7 +51,8 @@ impl Command for CommandImpl { 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); let signature = seutil::secure_enclave_p256_sign(&se_key_uri.private_key, &input_bytes)?; diff --git a/src/cmd_se_recover.rs b/src/cmd_se_recover.rs index 8f3dc08..550d8f4 100644 --- a/src/cmd_se_recover.rs +++ b/src/cmd_se_recover.rs @@ -1,5 +1,5 @@ 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 clap::{App, Arg, ArgMatches, SubCommand}; 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); seutil::check_se_supported()?; - let key_uri = sub_arg_matches.value_of("key").unwrap(); - - let KeyUri::SecureEnclaveKey(se_key_uri) = parse_key_uri(key_uri)?; + let key = sub_arg_matches.value_of("key").unwrap(); + 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); let (public_key_point, public_key_der, _private_key) = @@ -39,7 +39,7 @@ impl Command for CommandImpl { 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) } diff --git a/src/cmd_sign_jwt_se.rs b/src/cmd_sign_jwt_se.rs index 24d75c4..33fac85 100644 --- a/src/cmd_sign_jwt_se.rs +++ b/src/cmd_sign_jwt_se.rs @@ -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::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::util::base64_decode; @@ -35,7 +35,8 @@ impl Command for CommandImpl { "Private key PKCS#8 DER base64 encoded or PEM" ); 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); let (header, payload, jwt_claims) = build_jwt_parts(sub_arg_matches)?; diff --git a/src/cmdutil.rs b/src/cmdutil.rs index bc618a0..606b578 100644 --- a/src/cmdutil.rs +++ b/src/cmdutil.rs @@ -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") } +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> { Arg::with_name("no-pin").long("no-pin").help("No PIN") } diff --git a/src/digestutil.rs b/src/digestutil.rs index bb72978..60575ea 100644 --- a/src/digestutil.rs +++ b/src/digestutil.rs @@ -1,6 +1,25 @@ use sha1::Sha1; use sha2::{Digest, Sha256, Sha384, Sha512}; +pub enum DigestAlgorithm { + Sha256, + #[allow(dead_code)] + Sha384, +} + +impl DigestAlgorithm { + pub fn digest(&self, data: &[u8]) -> Vec { + match self { + DigestAlgorithm::Sha256 => sha256_bytes(data), + DigestAlgorithm::Sha384 => sha384_bytes(data), + } + } + + pub fn digest_str(&self, s: &str) -> Vec { + self.digest(s.as_bytes()) + } +} + pub fn sha256(input: &str) -> Vec { sha256_bytes(input.as_bytes()) } diff --git a/src/keyutil.rs b/src/keyutil.rs index f682b21..4fe2129 100644 --- a/src/keyutil.rs +++ b/src/keyutil.rs @@ -1,29 +1,50 @@ use regex::Regex; use rust_util::XResult; +use yubikey::piv::{AlgorithmId, SlotId}; +use crate::pivutil::{ToStr, FromStr}; // reference: https://git.hatter.ink/hatter/card-cli/issues/6 #[derive(Debug)] pub enum KeyUri { SecureEnclaveKey(SecureEnclaveKey), + YubikeyPivKey(YubikeyPivKey), } -// #[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, -// } -// } -// } +impl KeyUri { + pub fn as_secure_enclave_key(&self) -> XResult<&SecureEnclaveKey> { + match self { + KeyUri::SecureEnclaveKey(key) => Ok(key), + _ => simple_error!("Not a secure enclave key."), + } + } +} + +impl ToString for KeyUri { + fn to_string(&self) -> String { + let mut key_uri = String::with_capacity(64); + key_uri.push_str("key://"); + 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)] 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)] #[derive(Debug)] pub struct SecureEnclaveKey { @@ -51,64 +82,110 @@ pub struct SecureEnclaveKey { 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 { - 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) { None => return simple_error!("Invalid key uri: {}", key_uri), 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 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.") + match module { + "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, + }; + 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, - }; - - let parsed_key_uri = KeyUri::SecureEnclaveKey(SecureEnclaveKey { - host: host.to_string(), - usage: key_usage, - private_key: left_part.to_string(), - }); - - debugging!("Parsed key uri: {:?}", parsed_key_uri); - Ok(parsed_key_uri) + "piv" => { + if "" != usage { + return simple_error!("Key uri's usage must be empty."); + } + let algorithm = opt_value_result!(AlgorithmId::from_str(algorithm), "Invalid algorithm id: {}", algorithm); + let slot = opt_value_result!(SlotId::from_str(left_part), "Invalid slot id: {}", left_part); + let parsed_key_uri = KeyUri::YubikeyPivKey(YubikeyPivKey { + key_name: host_or_name.to_string(), + algorithm, + slot, + }); + debugging!("Parsed key uri: {:?}", parsed_key_uri); + Ok(parsed_key_uri) + } + _ => simple_error!("Key uri's module must be se."), + } } #[test] fn test_parse_key_uri_01() { let se_key_uri = 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 { 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); } + _ => { + panic!("Key uri not parsed") + } } } #[test] fn test_parse_key_uri_02() { 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(); + assert_eq!("key://hatter-mac-m1:se/p256:key_agreement:BASE64(dataRepresentation)", se_key_uri.to_string()); match 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!("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") + } } } diff --git a/src/pivutil.rs b/src/pivutil.rs index 4e178ba..608b5dd 100644 --- a/src/pivutil.rs +++ b/src/pivutil.rs @@ -56,6 +56,12 @@ pub trait ToStr { fn to_str(&self) -> &str; } +pub trait FromStr { + fn from_str(s: &str) -> Option + where + Self: Sized; +} + impl ToStr for PinPolicy { fn to_str(&self) -> &str { match self { @@ -78,6 +84,21 @@ impl ToStr for TouchPolicy { } } +impl FromStr for AlgorithmId { + fn from_str(s: &str) -> Option + 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 { fn to_str(&self) -> &str { match self { @@ -194,6 +215,54 @@ pub fn get_slot_id(slot: &str) -> XResult { }) } +impl FromStr for SlotId { + fn from_str(s: &str) -> Option + 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 { if never_use_pin(yk, slot_id) { None