diff --git a/Cargo.lock b/Cargo.lock index fabb2bf..099ddc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2277,7 +2277,7 @@ dependencies = [ [[package]] name = "sshcerts" -version = "0.15.0" +version = "0.15.1" dependencies = [ "aes 0.7.5", "authenticator", diff --git a/Cargo.toml b/Cargo.toml index 78cf4d2..6577e4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sshcerts" -version = "0.15.0" +version = "0.15.1" authors = ["Mitchell Grenier "] edition = "2021" license-file = "LICENSE" diff --git a/src/fido/generate/mod.rs b/src/fido/generate/mod.rs index 48cca49..fbd2521 100644 --- a/src/fido/generate/mod.rs +++ b/src/fido/generate/mod.rs @@ -1,4 +1,4 @@ -use crate::PrivateKey; +use crate::{PrivateKey, TouchRequirement}; #[cfg(any(feature = "fido-support"))] mod ctap2_hid; @@ -50,3 +50,10 @@ impl U2FAttestation { ) } } + +impl FIDOSSHKey { + /// Returns the touch requirement for this generated FIDO key. + pub fn touch_requirement(&self) -> TouchRequirement { + self.private_key.touch_requirement() + } +} diff --git a/src/lib.rs b/src/lib.rs index 5879e5b..61227e8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,7 +45,7 @@ pub mod error; type Result = std::result::Result; -pub use ssh::{CertType, Certificate, PrivateKey, PublicKey}; +pub use ssh::{CertType, Certificate, PrivateKey, PublicKey, TouchRequirement}; /// Functions or structs for dealing with SSH Certificates. /// Parsing, and creating certs happens here. diff --git a/src/ssh/mod.rs b/src/ssh/mod.rs index ef2283a..7006f87 100644 --- a/src/ssh/mod.rs +++ b/src/ssh/mod.rs @@ -29,7 +29,7 @@ pub use self::cert::{CertType, Certificate}; pub use self::keytype::{Curve, CurveKind, KeyType, KeyTypeKind}; pub use self::privkey::{ EcdsaPrivateKey, EcdsaSkPrivateKey, Ed25519PrivateKey, Ed25519SkPrivateKey, PrivateKey, - PrivateKeyKind, RsaPrivateKey, + PrivateKeyKind, RsaPrivateKey, TouchRequirement, }; pub use self::pubkey::{ EcdsaPublicKey, Ed25519PublicKey, Fingerprint, FingerprintKind, PublicKey, PublicKeyKind, diff --git a/src/ssh/privkey.rs b/src/ssh/privkey.rs index 26c28f5..b790211 100644 --- a/src/ssh/privkey.rs +++ b/src/ssh/privkey.rs @@ -36,6 +36,36 @@ use aes::{ #[cfg(feature = "encrypted-keys")] use bcrypt_pbkdf::bcrypt_pbkdf; +const SSH_SK_USER_PRESENCE_REQD: u8 = 0x01; + +/// Whether a hardware-backed key requires a user touch for signing. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum TouchRequirement { + /// Signing requires a user touch. + Required, + + /// Signing does not require a user touch. + NotRequired, + + /// The touch requirement could not be determined. + Unknown, +} + +impl TouchRequirement { + /// Returns true when signing is known to require touch. + pub fn is_required(self) -> bool { + matches!(self, TouchRequirement::Required) + } +} + +fn touch_requirement_from_flags(flags: u8) -> TouchRequirement { + if (flags & SSH_SK_USER_PRESENCE_REQD) != 0 { + TouchRequirement::Required + } else { + TouchRequirement::NotRequired + } +} + /// RSA private key. #[derive(Debug, PartialEq, Eq, Clone, Zeroize)] pub struct RsaPrivateKey { @@ -125,6 +155,20 @@ pub struct Ed25519SkPrivateKey { pub device_path: Option, } +impl EcdsaSkPrivateKey { + /// Returns the touch requirement for this hardware-backed key. + pub fn touch_requirement(&self) -> TouchRequirement { + touch_requirement_from_flags(self.flags) + } +} + +impl Ed25519SkPrivateKey { + /// Returns the touch requirement for this hardware-backed key. + pub fn touch_requirement(&self) -> TouchRequirement { + touch_requirement_from_flags(self.flags) + } +} + /// A type which represents the different kinds a public key can be. #[derive(Debug, PartialEq, Eq, Clone, Zeroize)] pub enum PrivateKeyKind { @@ -646,6 +690,17 @@ impl PrivateKey { }; } + /// Returns the touch requirement for this private key. + /// + /// Non-hardware-backed keys return `TouchRequirement::NotRequired`. + pub fn touch_requirement(&self) -> TouchRequirement { + match &self.kind { + PrivateKeyKind::EcdsaSk(key) => key.touch_requirement(), + PrivateKeyKind::Ed25519Sk(key) => key.touch_requirement(), + _ => TouchRequirement::NotRequired, + } + } + /// Encode the PrivateKey into a bytes representation pub fn encode(&self) -> Vec { let mut serializer = Writer::new(); diff --git a/src/yubikey/piv/management.rs b/src/yubikey/piv/management.rs index c497f95..3bc6ca5 100644 --- a/src/yubikey/piv/management.rs +++ b/src/yubikey/piv/management.rs @@ -1,4 +1,4 @@ -use crate::PublicKey; +use crate::{PublicKey, TouchRequirement}; use ring::digest; @@ -47,15 +47,29 @@ pub const NISTP256_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840 /// OID for secp384r1 (NIST P-384) pub const SECP384_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.132.0.34"); +fn touch_policy_requirement(touch_policy: TouchPolicy) -> TouchRequirement { + match touch_policy { + TouchPolicy::Always | TouchPolicy::Cached => TouchRequirement::Required, + // YubiKey PIV defaults to no touch; Unknown is only for missing metadata. + TouchPolicy::Never | TouchPolicy::Default => TouchRequirement::NotRequired, + } +} + impl CSRSigner { /// Create a new certificate signer based on a Yubikey serial /// and slot pub fn new(serial: u32, slot: SlotId) -> Result { let mut yk = super::Yubikey::open(serial)?; - let cert = yk.configured(&slot) - .map_err(|e| Error::InternalYubiKeyError(format!("failed to read certificate for CSR generation: {}", e)))?; + let cert = yk.configured(&slot).map_err(|e| { + Error::InternalYubiKeyError(format!( + "failed to read certificate for CSR generation: {}", + e + )) + })?; let pki = cert.subject_pki(); - let oid_alg = pki.algorithm.parameters_oid() + let oid_alg = pki + .algorithm + .parameters_oid() .map_err(|_| Error::OIDError)?; let (public_key, algorithm) = match oid_alg { @@ -240,11 +254,38 @@ impl super::Yubikey { Ok(attest(&mut self.yk, *slot)?.to_vec()) } + /// Fetch the touch policy configured for a YubiKey PIV slot. + pub fn fetch_touch_policy(&mut self, slot: &SlotId) -> Result> { + let metadata = match yubikey::piv::metadata(&mut self.yk, *slot) { + Ok(metadata) => metadata, + Err(yubikey::Error::NotFound) => return Err(Error::Unprovisioned), + Err(e) => return Err(e.into()), + }; + + Ok(metadata.policy.map(|(_, touch_policy)| touch_policy)) + } + + /// Returns the touch requirement for a YubiKey PIV slot. + /// + /// Returns `TouchRequirement::Unknown` only when the device does not expose + /// policy metadata. A slot's default touch policy is `Never`, so it resolves + /// to `NotRequired`. + pub fn touch_requirement(&mut self, slot: &SlotId) -> Result { + Ok(self + .fetch_touch_policy(slot)? + .map(touch_policy_requirement) + .unwrap_or(TouchRequirement::Unknown)) + } + /// Generate CSR for slot pub fn generate_csr(&mut self, slot: &SlotId, common_name: &str) -> Result> { let mut params = rcgen::CertificateParams::new(vec![]); - let cert = self.configured(&slot) - .map_err(|e| Error::InternalYubiKeyError(format!("failed to read certificate for CSR generation: {}", e)))?; + let cert = self.configured(&slot).map_err(|e| { + Error::InternalYubiKeyError(format!( + "failed to read certificate for CSR generation: {}", + e + )) + })?; let pki = cert.subject_pki(); let oid_alg = pki .algorithm @@ -331,8 +372,9 @@ impl super::Yubikey { /// If the requested algorithm doesn't match the key in the slot (or the slot /// is empty) this will error. pub fn sign_data(&mut self, data: &[u8], alg: AlgorithmId, slot: &SlotId) -> Result> { - let cert = self.configured(&slot) - .map_err(|e| Error::InternalYubiKeyError(format!("failed to read slot for signing: {}", e)))?; + let cert = self.configured(&slot).map_err(|e| { + Error::InternalYubiKeyError(format!("failed to read slot for signing: {}", e)) + })?; let pki = cert.subject_pki(); let oid_alg = pki .algorithm @@ -357,3 +399,28 @@ impl super::Yubikey { Ok(signature.to_vec()) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn touch_policy_requirement_maps_known_policies() { + assert_eq!( + touch_policy_requirement(TouchPolicy::Always), + TouchRequirement::Required + ); + assert_eq!( + touch_policy_requirement(TouchPolicy::Cached), + TouchRequirement::Required + ); + assert_eq!( + touch_policy_requirement(TouchPolicy::Never), + TouchRequirement::NotRequired + ); + assert_eq!( + touch_policy_requirement(TouchPolicy::Default), + TouchRequirement::NotRequired + ); + } +} diff --git a/src/yubikey/piv/mod.rs b/src/yubikey/piv/mod.rs index 83e1481..9a52d70 100644 --- a/src/yubikey/piv/mod.rs +++ b/src/yubikey/piv/mod.rs @@ -1,4 +1,4 @@ -/// Implements KeyType trait +/// Implements KeyType trait pub mod keytype; /// Contains all the functions used for creating new keys, unlocking, and @@ -47,7 +47,7 @@ impl std::error::Error for Error {} type Result = std::result::Result; -// Re-export because it's used as a parameter in `sign_data` +pub use crate::ssh::TouchRequirement; pub use keytype::{NistP256, NistP384}; pub use yubikey::piv::{AlgorithmId, RetiredSlotId, SlotId}; pub use management::ManagementKeyAlgorithm; diff --git a/tests/sk-privkey.rs b/tests/sk-privkey.rs index 80f6f9c..ea2f1ac 100644 --- a/tests/sk-privkey.rs +++ b/tests/sk-privkey.rs @@ -1,4 +1,4 @@ -use sshcerts::ssh::PrivateKey; +use sshcerts::ssh::{PrivateKey, PrivateKeyKind, TouchRequirement}; use std::io::BufWriter; #[test] @@ -10,6 +10,7 @@ fn parse_sk_ed25519_private_key() { privkey.pubkey.fingerprint().hash, "GlvFAEnledYF0XG1guJ7dT2d0Mk88GmPAiHk8+zCBlA" ); + assert_eq!(privkey.touch_requirement(), TouchRequirement::Required); let mut buf = BufWriter::new(Vec::new()); privkey.write(&mut buf).unwrap(); @@ -26,9 +27,56 @@ fn parse_sk_ecdsa_256_private_key() { privkey.pubkey.fingerprint().hash, "Ylfgx0U2M9/IVN0+b5/IxdNeVCotsdrRZ5lu5FG2ouc" ); + assert_eq!(privkey.touch_requirement(), TouchRequirement::Required); let mut buf = BufWriter::new(Vec::new()); privkey.write(&mut buf).unwrap(); let serialized = String::from_utf8(buf.into_inner().unwrap()).unwrap(); assert_eq!(include_str!("keys/sk/ecdsa"), serialized); } + +#[test] +fn sk_private_key_touch_requirement_follows_flags() { + let mut privkey = PrivateKey::from_string(include_str!("keys/sk/ed25519")).unwrap(); + + if let PrivateKeyKind::Ed25519Sk(key) = &mut privkey.kind { + key.flags = 0x00; + assert_eq!(key.touch_requirement(), TouchRequirement::NotRequired); + } else { + panic!("expected ed25519-sk private key"); + } + assert_eq!(privkey.touch_requirement(), TouchRequirement::NotRequired); + + if let PrivateKeyKind::Ed25519Sk(key) = &mut privkey.kind { + key.flags = 0x01; + assert_eq!(key.touch_requirement(), TouchRequirement::Required); + } else { + panic!("expected ed25519-sk private key"); + } + assert_eq!(privkey.touch_requirement(), TouchRequirement::Required); + + let mut privkey = PrivateKey::from_string(include_str!("keys/sk/ecdsa")).unwrap(); + + if let PrivateKeyKind::EcdsaSk(key) = &mut privkey.kind { + key.flags = 0x00; + assert_eq!(key.touch_requirement(), TouchRequirement::NotRequired); + } else { + panic!("expected ecdsa-sk private key"); + } + assert_eq!(privkey.touch_requirement(), TouchRequirement::NotRequired); + + if let PrivateKeyKind::EcdsaSk(key) = &mut privkey.kind { + key.flags = 0x01; + assert_eq!(key.touch_requirement(), TouchRequirement::Required); + } else { + panic!("expected ecdsa-sk private key"); + } + assert_eq!(privkey.touch_requirement(), TouchRequirement::Required); +} + +#[test] +fn non_sk_private_key_touch_requirement_is_not_required() { + let privkey = PrivateKey::from_string(include_str!("keys/unencrypted/ed25519_1")).unwrap(); + + assert_eq!(privkey.touch_requirement(), TouchRequirement::NotRequired); +}