Files
card-cli/src/keyutil.rs

405 lines
13 KiB
Rust

use crate::pivutil::{FromStr, ToStr};
use jwt::AlgorithmType;
use percent_encoding::NON_ALPHANUMERIC;
use regex::Regex;
use rust_util::XResult;
use yubikey::piv::{AlgorithmId, SlotId};
// reference: https://git.hatter.ink/hatter/card-cli/issues/6
#[derive(Debug)]
pub enum KeyUri {
SecureEnclaveKey(SecureEnclaveKey),
YubikeyPivKey(YubikeyPivKey),
YubikeyHmacEncSoftKey(YubikeyHmacEncSoftKey),
ExternalCommandKey(ExternalCommandKey),
}
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."),
}
}
pub fn get_preferred_algorithm_type(&self) -> AlgorithmType {
let algorithm_id = match &self {
KeyUri::SecureEnclaveKey(_) => return AlgorithmType::Es256,
KeyUri::YubikeyPivKey(key) => key.algorithm,
KeyUri::YubikeyHmacEncSoftKey(key) => key.algorithm,
KeyUri::ExternalCommandKey(key) => key.algorithm,
};
match algorithm_id {
KeyAlgorithmId::Rsa1024
| KeyAlgorithmId::Rsa2048
| KeyAlgorithmId::Rsa3072
| KeyAlgorithmId::Rsa4096 => AlgorithmType::Rs256,
KeyAlgorithmId::EccP256 => AlgorithmType::Es256,
KeyAlgorithmId::EccP384 => AlgorithmType::Es384,
KeyAlgorithmId::EccP521 => AlgorithmType::Es512,
}
}
}
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://-:soft/p256::hmac_enc:...
KeyUri::YubikeyHmacEncSoftKey(key) => {
key_uri.push_str(&key.key_name);
key_uri.push_str(":soft/");
key_uri.push_str(key.algorithm.to_str());
key_uri.push_str("::");
key_uri.push_str(key.hmac_enc_private_key.as_str());
}
// key://external-command-file-name:external_command/p256::parameter
KeyUri::ExternalCommandKey(key) => {
let encoded_external_command =
percent_encoding::utf8_percent_encode(&key.external_command, NON_ALPHANUMERIC)
.to_string();
key_uri.push_str(&encoded_external_command);
key_uri.push_str(":external_command/");
key_uri.push_str(key.algorithm.to_str());
key_uri.push_str("::");
key_uri.push_str(&key.parameter);
}
}
key_uri
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum KeyAlgorithmId {
Rsa1024,
Rsa2048,
Rsa3072,
Rsa4096,
EccP256,
EccP384,
EccP521,
}
impl KeyAlgorithmId {
pub fn from_algorithm_id(algorithm_id: AlgorithmId) -> Self {
match algorithm_id {
AlgorithmId::Rsa1024 => Self::Rsa1024,
AlgorithmId::Rsa2048 => Self::Rsa2048,
AlgorithmId::EccP256 => Self::EccP256,
AlgorithmId::EccP384 => Self::EccP384,
}
}
pub fn to_algorithm_id(self) -> Option<AlgorithmId> {
match self {
KeyAlgorithmId::Rsa1024 => Some(AlgorithmId::Rsa1024),
KeyAlgorithmId::Rsa2048 => Some(AlgorithmId::Rsa2048),
KeyAlgorithmId::Rsa3072 => None,
KeyAlgorithmId::Rsa4096 => None,
KeyAlgorithmId::EccP256 => Some(AlgorithmId::EccP256),
KeyAlgorithmId::EccP384 => Some(AlgorithmId::EccP384),
KeyAlgorithmId::EccP521 => None,
}
}
pub fn is_rsa(&self) -> bool {
match self {
KeyAlgorithmId::Rsa1024
| KeyAlgorithmId::Rsa2048
| KeyAlgorithmId::Rsa3072
| KeyAlgorithmId::Rsa4096 => true,
KeyAlgorithmId::EccP256 | KeyAlgorithmId::EccP384 | KeyAlgorithmId::EccP521 => false,
}
}
pub fn is_ecc(&self) -> bool {
match self {
KeyAlgorithmId::Rsa1024
| KeyAlgorithmId::Rsa2048
| KeyAlgorithmId::Rsa3072
| KeyAlgorithmId::Rsa4096 => false,
KeyAlgorithmId::EccP256 | KeyAlgorithmId::EccP384 | KeyAlgorithmId::EccP521 => true,
}
}
pub fn to_jwa_name(&self) -> &str {
match self {
KeyAlgorithmId::Rsa1024
| KeyAlgorithmId::Rsa2048
| KeyAlgorithmId::Rsa3072
| KeyAlgorithmId::Rsa4096 => "RS256",
KeyAlgorithmId::EccP256 => "ES256,",
KeyAlgorithmId::EccP384 => "ES384",
KeyAlgorithmId::EccP521 => "ES512",
}
}
}
impl FromStr for KeyAlgorithmId {
fn from_str(s: &str) -> Option<Self>
where
Self: Sized,
{
match s {
"rsa1024" => Some(KeyAlgorithmId::Rsa1024),
"rsa2048" => Some(KeyAlgorithmId::Rsa2048),
"rsa3072" => Some(KeyAlgorithmId::Rsa3072),
"rsa4096" => Some(KeyAlgorithmId::Rsa4096),
"p256" => Some(KeyAlgorithmId::EccP256),
"p384" => Some(KeyAlgorithmId::EccP384),
"p521" => Some(KeyAlgorithmId::EccP521),
_ => None,
}
}
}
impl ToStr for KeyAlgorithmId {
fn to_str(&self) -> &str {
match self {
KeyAlgorithmId::Rsa1024 => "rsa1024",
KeyAlgorithmId::Rsa2048 => "rsa2048",
KeyAlgorithmId::Rsa3072 => "rsa3072",
KeyAlgorithmId::Rsa4096 => "rsa4096",
KeyAlgorithmId::EccP256 => "p256",
KeyAlgorithmId::EccP384 => "p384",
KeyAlgorithmId::EccP521 => "p521",
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum KeyUsage {
Any,
Singing,
KeyAgreement,
}
impl KeyUsage {
pub fn from(usage: &str) -> Option<Self> {
match usage {
"signing" => Some(Self::Singing),
"key_agreement" => Some(Self::KeyAgreement),
"*" => Some(Self::Any),
_ => None,
}
}
}
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 {
pub host: String,
pub usage: KeyUsage,
pub private_key: String,
}
#[allow(dead_code)]
#[derive(Debug)]
pub struct YubikeyPivKey {
pub key_name: String,
pub algorithm: KeyAlgorithmId,
pub slot: SlotId,
}
#[allow(dead_code)]
#[derive(Debug)]
pub struct YubikeyHmacEncSoftKey {
pub key_name: String,
pub algorithm: KeyAlgorithmId,
pub hmac_enc_private_key: String,
}
#[allow(dead_code)]
#[derive(Debug)]
pub struct ExternalCommandKey {
pub external_command: String,
pub algorithm: KeyAlgorithmId,
pub parameter: String,
}
pub fn parse_key_uri(key_uri: &str) -> XResult<KeyUri> {
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_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();
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)
}
"piv" => {
if "" != usage {
return simple_error!("Key uri's usage must be empty.");
}
let algorithm = opt_value_result!(
KeyAlgorithmId::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)
}
"soft" => {
if "" != usage {
return simple_error!("Key uri's usage must be empty.");
}
let algorithm = opt_value_result!(
KeyAlgorithmId::from_str(algorithm),
"Invalid algorithm id: {}",
algorithm
);
let hmac_enc_private_key = left_part.to_string();
let parsed_key_uri = KeyUri::YubikeyHmacEncSoftKey(YubikeyHmacEncSoftKey {
key_name: host_or_name.to_string(),
algorithm,
hmac_enc_private_key,
});
debugging!("Parsed key uri: {:?}", parsed_key_uri);
Ok(parsed_key_uri)
}
"external_command" => {
if "" != usage {
return simple_error!("Key uri's usage must be empty.");
}
let external_command = opt_result!(
percent_encoding::percent_decode_str(host_or_name).decode_utf8(),
"Decode external command failed: {}"
);
let algorithm = opt_value_result!(
KeyAlgorithmId::from_str(algorithm),
"Invalid algorithm id: {}",
algorithm
);
let parameter = left_part.to_string();
let parsed_key_uri = KeyUri::ExternalCommandKey(ExternalCommandKey {
external_command: external_command.to_string(),
algorithm,
parameter,
});
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-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-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!(KeyAlgorithmId::EccP256, piv_key_uri.algorithm);
assert_eq!(SlotId::Authentication, piv_key_uri.slot);
}
_ => {
panic!("Key uri not parsed")
}
}
}