feat: v1.12.0
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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") {
|
||||||
|
|||||||
@@ -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)?;
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)?;
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
159
src/keyutil.rs
159
src/keyutil.rs
@@ -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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user