feat: v1.12.0
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "card-cli"
|
||||
version = "1.11.17"
|
||||
version = "1.12.0"
|
||||
authors = ["Hatter Jiang <jht5945@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
|
||||
@@ -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<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>> {
|
||||
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);
|
||||
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 {
|
||||
|
||||
@@ -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<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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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<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> {
|
||||
sha256_bytes(input.as_bytes())
|
||||
}
|
||||
|
||||
159
src/keyutil.rs
159
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<Self> {
|
||||
// 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<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) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,12 @@ pub trait ToStr {
|
||||
fn to_str(&self) -> &str;
|
||||
}
|
||||
|
||||
pub trait FromStr {
|
||||
fn from_str(s: &str) -> Option<Self>
|
||||
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<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 {
|
||||
fn to_str(&self) -> &str {
|
||||
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> {
|
||||
if never_use_pin(yk, slot_id) {
|
||||
None
|
||||
|
||||
Reference in New Issue
Block a user