From 58c11a8afad10630c040b464adb5dd97955aac08 Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Mon, 1 Jun 2026 12:16:53 -0400 Subject: [PATCH 1/9] Add public method for key.requires_touch() AI tool: Codex + gpt5.5 --- src/fido/generate/mod.rs | 7 ++++ src/ssh/privkey.rs | 27 ++++++++++++++ src/yubikey/piv/management.rs | 67 +++++++++++++++++++++++++++++++---- tests/sk-privkey.rs | 50 +++++++++++++++++++++++++- 4 files changed, 143 insertions(+), 8 deletions(-) diff --git a/src/fido/generate/mod.rs b/src/fido/generate/mod.rs index 48cca49..5ac154d 100644 --- a/src/fido/generate/mod.rs +++ b/src/fido/generate/mod.rs @@ -50,3 +50,10 @@ impl U2FAttestation { ) } } + +impl FIDOSSHKey { + /// Returns whether this generated FIDO key requires touch for signing. + pub fn requires_touch(&self) -> bool { + self.private_key.requires_touch() + } +} diff --git a/src/ssh/privkey.rs b/src/ssh/privkey.rs index 26c28f5..6861e88 100644 --- a/src/ssh/privkey.rs +++ b/src/ssh/privkey.rs @@ -36,6 +36,8 @@ use aes::{ #[cfg(feature = "encrypted-keys")] use bcrypt_pbkdf::bcrypt_pbkdf; +const SSH_SK_USER_PRESENCE_REQD: u8 = 0x01; + /// RSA private key. #[derive(Debug, PartialEq, Eq, Clone, Zeroize)] pub struct RsaPrivateKey { @@ -125,6 +127,20 @@ pub struct Ed25519SkPrivateKey { pub device_path: Option, } +impl EcdsaSkPrivateKey { + /// Returns whether this hardware-backed key requests user presence for signing. + pub fn requires_touch(&self) -> bool { + self.flags & SSH_SK_USER_PRESENCE_REQD > 0 + } +} + +impl Ed25519SkPrivateKey { + /// Returns whether this hardware-backed key requests user presence for signing. + pub fn requires_touch(&self) -> bool { + self.flags & SSH_SK_USER_PRESENCE_REQD > 0 + } +} + /// A type which represents the different kinds a public key can be. #[derive(Debug, PartialEq, Eq, Clone, Zeroize)] pub enum PrivateKeyKind { @@ -646,6 +662,17 @@ impl PrivateKey { }; } + /// Returns whether this private key requires touch for signing. + /// + /// Non-hardware-backed keys return false. + pub fn requires_touch(&self) -> bool { + match &self.kind { + PrivateKeyKind::EcdsaSk(key) => key.requires_touch(), + PrivateKeyKind::Ed25519Sk(key) => key.requires_touch(), + _ => false, + } + } + /// 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..2fbd06d 100644 --- a/src/yubikey/piv/management.rs +++ b/src/yubikey/piv/management.rs @@ -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_requires_touch(touch_policy: TouchPolicy) -> Option { + match touch_policy { + TouchPolicy::Always | TouchPolicy::Cached => Some(true), + TouchPolicy::Never => Some(false), + TouchPolicy::Default => None, + } +} + 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,36 @@ 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 whether a YubiKey PIV slot requires touch for signing. + /// + /// Returns `None` when the key uses the device default or the device does not + /// expose policy metadata. + pub fn requires_touch(&mut self, slot: &SlotId) -> Result> { + Ok(self + .fetch_touch_policy(slot)? + .and_then(touch_policy_requires_touch)) + } + /// 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 +370,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 +397,16 @@ impl super::Yubikey { Ok(signature.to_vec()) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn touch_policy_requires_touch_maps_known_policies() { + assert_eq!(touch_policy_requires_touch(TouchPolicy::Always), Some(true)); + assert_eq!(touch_policy_requires_touch(TouchPolicy::Cached), Some(true)); + assert_eq!(touch_policy_requires_touch(TouchPolicy::Never), Some(false)); + assert_eq!(touch_policy_requires_touch(TouchPolicy::Default), None); + } +} diff --git a/tests/sk-privkey.rs b/tests/sk-privkey.rs index 80f6f9c..98ae764 100644 --- a/tests/sk-privkey.rs +++ b/tests/sk-privkey.rs @@ -1,4 +1,4 @@ -use sshcerts::ssh::PrivateKey; +use sshcerts::ssh::{PrivateKey, PrivateKeyKind}; use std::io::BufWriter; #[test] @@ -10,6 +10,7 @@ fn parse_sk_ed25519_private_key() { privkey.pubkey.fingerprint().hash, "GlvFAEnledYF0XG1guJ7dT2d0Mk88GmPAiHk8+zCBlA" ); + assert!(privkey.requires_touch()); 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!(privkey.requires_touch()); 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_requires_touch_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!(!key.requires_touch()); + } else { + panic!("expected ed25519-sk private key"); + } + assert!(!privkey.requires_touch()); + + if let PrivateKeyKind::Ed25519Sk(key) = &mut privkey.kind { + key.flags = 0x01; + assert!(key.requires_touch()); + } else { + panic!("expected ed25519-sk private key"); + } + assert!(privkey.requires_touch()); + + let mut privkey = PrivateKey::from_string(include_str!("keys/sk/ecdsa")).unwrap(); + + if let PrivateKeyKind::EcdsaSk(key) = &mut privkey.kind { + key.flags = 0x00; + assert!(!key.requires_touch()); + } else { + panic!("expected ecdsa-sk private key"); + } + assert!(!privkey.requires_touch()); + + if let PrivateKeyKind::EcdsaSk(key) = &mut privkey.kind { + key.flags = 0x01; + assert!(key.requires_touch()); + } else { + panic!("expected ecdsa-sk private key"); + } + assert!(privkey.requires_touch()); +} + +#[test] +fn non_sk_private_key_does_not_require_touch() { + let privkey = PrivateKey::from_string(include_str!("keys/unencrypted/ed25519_1")).unwrap(); + + assert!(!privkey.requires_touch()); +} From 275abea2dbf37fa4b26a0a2aacf4b796bb7c9fe5 Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Mon, 1 Jun 2026 12:22:07 -0400 Subject: [PATCH 2/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/ssh/privkey.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ssh/privkey.rs b/src/ssh/privkey.rs index 6861e88..d7e469a 100644 --- a/src/ssh/privkey.rs +++ b/src/ssh/privkey.rs @@ -137,7 +137,7 @@ impl EcdsaSkPrivateKey { impl Ed25519SkPrivateKey { /// Returns whether this hardware-backed key requests user presence for signing. pub fn requires_touch(&self) -> bool { - self.flags & SSH_SK_USER_PRESENCE_REQD > 0 + (self.flags & SSH_SK_USER_PRESENCE_REQD) != 0 } } From dfb6c9b39235c9c0ae16c2f1a17da03aa1e35312 Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Mon, 1 Jun 2026 12:22:21 -0400 Subject: [PATCH 3/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/ssh/privkey.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ssh/privkey.rs b/src/ssh/privkey.rs index d7e469a..6c39d1b 100644 --- a/src/ssh/privkey.rs +++ b/src/ssh/privkey.rs @@ -130,9 +130,10 @@ pub struct Ed25519SkPrivateKey { impl EcdsaSkPrivateKey { /// Returns whether this hardware-backed key requests user presence for signing. pub fn requires_touch(&self) -> bool { - self.flags & SSH_SK_USER_PRESENCE_REQD > 0 + (self.flags & SSH_SK_USER_PRESENCE_REQD) != 0 } } +} impl Ed25519SkPrivateKey { /// Returns whether this hardware-backed key requests user presence for signing. From 19cf76384e4dbf1621da9ae20c5089e299a871b2 Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Mon, 1 Jun 2026 13:05:19 -0400 Subject: [PATCH 4/9] Remove requires touch --- src/fido/generate/mod.rs | 6 ++--- src/lib.rs | 2 +- src/ssh/mod.rs | 4 +-- src/ssh/privkey.rs | 49 ++++++++++++++++++++++++++++------- src/yubikey/piv/management.rs | 41 +++++++++++++++++++---------- src/yubikey/piv/mod.rs | 5 ++-- tests/sk-privkey.rs | 28 ++++++++++---------- 7 files changed, 89 insertions(+), 46 deletions(-) diff --git a/src/fido/generate/mod.rs b/src/fido/generate/mod.rs index 5ac154d..2d6e908 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; @@ -53,7 +53,7 @@ impl U2FAttestation { impl FIDOSSHKey { /// Returns whether this generated FIDO key requires touch for signing. - pub fn requires_touch(&self) -> bool { - self.private_key.requires_touch() + 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..289af82 100644 --- a/src/ssh/mod.rs +++ b/src/ssh/mod.rs @@ -24,12 +24,12 @@ pub trait SSHCertificateSigner { fn sign(&self, buffer: &[u8]) -> Option>; } -pub use self::allowed_signer::{AllowedSigner, AllowedSigners, AllowedSignerParsingError}; +pub use self::allowed_signer::{AllowedSigner, AllowedSignerParsingError, AllowedSigners}; 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 6c39d1b..9d0a224 100644 --- a/src/ssh/privkey.rs +++ b/src/ssh/privkey.rs @@ -38,6 +38,36 @@ 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 whether signing is known to require touch. + pub fn is_required(self) -> bool { + matches!(self, TouchRequirement::Required) + } +} + +impl From for TouchRequirement { + fn from(required: bool) -> Self { + if required { + TouchRequirement::Required + } else { + TouchRequirement::NotRequired + } + } +} + /// RSA private key. #[derive(Debug, PartialEq, Eq, Clone, Zeroize)] pub struct RsaPrivateKey { @@ -129,16 +159,15 @@ pub struct Ed25519SkPrivateKey { impl EcdsaSkPrivateKey { /// Returns whether this hardware-backed key requests user presence for signing. - pub fn requires_touch(&self) -> bool { - (self.flags & SSH_SK_USER_PRESENCE_REQD) != 0 + pub fn touch_requirement(&self) -> TouchRequirement { + TouchRequirement::from((self.flags & SSH_SK_USER_PRESENCE_REQD) != 0) } } -} impl Ed25519SkPrivateKey { /// Returns whether this hardware-backed key requests user presence for signing. - pub fn requires_touch(&self) -> bool { - (self.flags & SSH_SK_USER_PRESENCE_REQD) != 0 + pub fn touch_requirement(&self) -> TouchRequirement { + TouchRequirement::from((self.flags & SSH_SK_USER_PRESENCE_REQD) != 0) } } @@ -665,12 +694,12 @@ impl PrivateKey { /// Returns whether this private key requires touch for signing. /// - /// Non-hardware-backed keys return false. - pub fn requires_touch(&self) -> bool { + /// Non-hardware-backed keys return `TouchRequirement::NotRequired`. + pub fn touch_requirement(&self) -> TouchRequirement { match &self.kind { - PrivateKeyKind::EcdsaSk(key) => key.requires_touch(), - PrivateKeyKind::Ed25519Sk(key) => key.requires_touch(), - _ => false, + PrivateKeyKind::EcdsaSk(key) => key.touch_requirement(), + PrivateKeyKind::Ed25519Sk(key) => key.touch_requirement(), + _ => TouchRequirement::NotRequired, } } diff --git a/src/yubikey/piv/management.rs b/src/yubikey/piv/management.rs index 2fbd06d..8015549 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,11 +47,11 @@ 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_requires_touch(touch_policy: TouchPolicy) -> Option { +fn touch_policy_requirement(touch_policy: TouchPolicy) -> TouchRequirement { match touch_policy { - TouchPolicy::Always | TouchPolicy::Cached => Some(true), - TouchPolicy::Never => Some(false), - TouchPolicy::Default => None, + TouchPolicy::Always | TouchPolicy::Cached => TouchRequirement::Required, + TouchPolicy::Never => TouchRequirement::NotRequired, + TouchPolicy::Default => TouchRequirement::Unknown, } } @@ -267,12 +267,13 @@ impl super::Yubikey { /// Returns whether a YubiKey PIV slot requires touch for signing. /// - /// Returns `None` when the key uses the device default or the device does not - /// expose policy metadata. - pub fn requires_touch(&mut self, slot: &SlotId) -> Result> { + /// Returns `TouchRequirement::Unknown` when the key uses the device default + /// or the device does not expose policy metadata. + pub fn touch_requirement(&mut self, slot: &SlotId) -> Result { Ok(self .fetch_touch_policy(slot)? - .and_then(touch_policy_requires_touch)) + .map(touch_policy_requirement) + .unwrap_or(TouchRequirement::Unknown)) } /// Generate CSR for slot @@ -403,10 +404,22 @@ mod tests { use super::*; #[test] - fn touch_policy_requires_touch_maps_known_policies() { - assert_eq!(touch_policy_requires_touch(TouchPolicy::Always), Some(true)); - assert_eq!(touch_policy_requires_touch(TouchPolicy::Cached), Some(true)); - assert_eq!(touch_policy_requires_touch(TouchPolicy::Never), Some(false)); - assert_eq!(touch_policy_requires_touch(TouchPolicy::Default), None); + 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::Unknown + ); } } diff --git a/src/yubikey/piv/mod.rs b/src/yubikey/piv/mod.rs index 83e1481..3ebca71 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 @@ -48,9 +48,10 @@ 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; +pub use yubikey::piv::{AlgorithmId, RetiredSlotId, SlotId}; pub use yubikey::{PinPolicy, TouchPolicy}; /// Structure to wrap a yubikey and abstract actions diff --git a/tests/sk-privkey.rs b/tests/sk-privkey.rs index 98ae764..ea2f1ac 100644 --- a/tests/sk-privkey.rs +++ b/tests/sk-privkey.rs @@ -1,4 +1,4 @@ -use sshcerts::ssh::{PrivateKey, PrivateKeyKind}; +use sshcerts::ssh::{PrivateKey, PrivateKeyKind, TouchRequirement}; use std::io::BufWriter; #[test] @@ -10,7 +10,7 @@ fn parse_sk_ed25519_private_key() { privkey.pubkey.fingerprint().hash, "GlvFAEnledYF0XG1guJ7dT2d0Mk88GmPAiHk8+zCBlA" ); - assert!(privkey.requires_touch()); + assert_eq!(privkey.touch_requirement(), TouchRequirement::Required); let mut buf = BufWriter::new(Vec::new()); privkey.write(&mut buf).unwrap(); @@ -27,7 +27,7 @@ fn parse_sk_ecdsa_256_private_key() { privkey.pubkey.fingerprint().hash, "Ylfgx0U2M9/IVN0+b5/IxdNeVCotsdrRZ5lu5FG2ouc" ); - assert!(privkey.requires_touch()); + assert_eq!(privkey.touch_requirement(), TouchRequirement::Required); let mut buf = BufWriter::new(Vec::new()); privkey.write(&mut buf).unwrap(); @@ -36,47 +36,47 @@ fn parse_sk_ecdsa_256_private_key() { } #[test] -fn sk_private_key_requires_touch_follows_flags() { +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!(!key.requires_touch()); + assert_eq!(key.touch_requirement(), TouchRequirement::NotRequired); } else { panic!("expected ed25519-sk private key"); } - assert!(!privkey.requires_touch()); + assert_eq!(privkey.touch_requirement(), TouchRequirement::NotRequired); if let PrivateKeyKind::Ed25519Sk(key) = &mut privkey.kind { key.flags = 0x01; - assert!(key.requires_touch()); + assert_eq!(key.touch_requirement(), TouchRequirement::Required); } else { panic!("expected ed25519-sk private key"); } - assert!(privkey.requires_touch()); + 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!(!key.requires_touch()); + assert_eq!(key.touch_requirement(), TouchRequirement::NotRequired); } else { panic!("expected ecdsa-sk private key"); } - assert!(!privkey.requires_touch()); + assert_eq!(privkey.touch_requirement(), TouchRequirement::NotRequired); if let PrivateKeyKind::EcdsaSk(key) = &mut privkey.kind { key.flags = 0x01; - assert!(key.requires_touch()); + assert_eq!(key.touch_requirement(), TouchRequirement::Required); } else { panic!("expected ecdsa-sk private key"); } - assert!(privkey.requires_touch()); + assert_eq!(privkey.touch_requirement(), TouchRequirement::Required); } #[test] -fn non_sk_private_key_does_not_require_touch() { +fn non_sk_private_key_touch_requirement_is_not_required() { let privkey = PrivateKey::from_string(include_str!("keys/unencrypted/ed25519_1")).unwrap(); - assert!(!privkey.requires_touch()); + assert_eq!(privkey.touch_requirement(), TouchRequirement::NotRequired); } From dee811bd5beb1b810f57b470adcf6c1667a3df2a Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Mon, 1 Jun 2026 13:18:15 -0400 Subject: [PATCH 5/9] Simplify --- src/fido/generate/mod.rs | 2 +- src/ssh/mod.rs | 2 +- src/ssh/privkey.rs | 25 ++++++++++++------------- src/yubikey/piv/management.rs | 2 +- src/yubikey/piv/mod.rs | 3 +-- 5 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/fido/generate/mod.rs b/src/fido/generate/mod.rs index 2d6e908..fbd2521 100644 --- a/src/fido/generate/mod.rs +++ b/src/fido/generate/mod.rs @@ -52,7 +52,7 @@ impl U2FAttestation { } impl FIDOSSHKey { - /// Returns whether this generated FIDO key requires touch for signing. + /// Returns the touch requirement for this generated FIDO key. pub fn touch_requirement(&self) -> TouchRequirement { self.private_key.touch_requirement() } diff --git a/src/ssh/mod.rs b/src/ssh/mod.rs index 289af82..7006f87 100644 --- a/src/ssh/mod.rs +++ b/src/ssh/mod.rs @@ -24,7 +24,7 @@ pub trait SSHCertificateSigner { fn sign(&self, buffer: &[u8]) -> Option>; } -pub use self::allowed_signer::{AllowedSigner, AllowedSignerParsingError, AllowedSigners}; +pub use self::allowed_signer::{AllowedSigner, AllowedSigners, AllowedSignerParsingError}; pub use self::cert::{CertType, Certificate}; pub use self::keytype::{Curve, CurveKind, KeyType, KeyTypeKind}; pub use self::privkey::{ diff --git a/src/ssh/privkey.rs b/src/ssh/privkey.rs index 9d0a224..05e34a9 100644 --- a/src/ssh/privkey.rs +++ b/src/ssh/privkey.rs @@ -39,6 +39,7 @@ use bcrypt_pbkdf::bcrypt_pbkdf; const SSH_SK_USER_PRESENCE_REQD: u8 = 0x01; /// Whether a hardware-backed key requires a user touch for signing. +#[non_exhaustive] #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum TouchRequirement { /// Signing requires a user touch. @@ -52,19 +53,17 @@ pub enum TouchRequirement { } impl TouchRequirement { - /// Returns whether signing is known to require touch. + /// Returns true when signing is known to require touch. pub fn is_required(self) -> bool { matches!(self, TouchRequirement::Required) } } -impl From for TouchRequirement { - fn from(required: bool) -> Self { - if required { - TouchRequirement::Required - } else { - TouchRequirement::NotRequired - } +fn touch_requirement_from_flags(flags: u8) -> TouchRequirement { + if (flags & SSH_SK_USER_PRESENCE_REQD) != 0 { + TouchRequirement::Required + } else { + TouchRequirement::NotRequired } } @@ -158,16 +157,16 @@ pub struct Ed25519SkPrivateKey { } impl EcdsaSkPrivateKey { - /// Returns whether this hardware-backed key requests user presence for signing. + /// Returns the touch requirement for this hardware-backed key. pub fn touch_requirement(&self) -> TouchRequirement { - TouchRequirement::from((self.flags & SSH_SK_USER_PRESENCE_REQD) != 0) + touch_requirement_from_flags(self.flags) } } impl Ed25519SkPrivateKey { - /// Returns whether this hardware-backed key requests user presence for signing. + /// Returns the touch requirement for this hardware-backed key. pub fn touch_requirement(&self) -> TouchRequirement { - TouchRequirement::from((self.flags & SSH_SK_USER_PRESENCE_REQD) != 0) + touch_requirement_from_flags(self.flags) } } @@ -692,7 +691,7 @@ impl PrivateKey { }; } - /// Returns whether this private key requires touch for signing. + /// Returns the touch requirement for this private key. /// /// Non-hardware-backed keys return `TouchRequirement::NotRequired`. pub fn touch_requirement(&self) -> TouchRequirement { diff --git a/src/yubikey/piv/management.rs b/src/yubikey/piv/management.rs index 8015549..f0afd69 100644 --- a/src/yubikey/piv/management.rs +++ b/src/yubikey/piv/management.rs @@ -265,7 +265,7 @@ impl super::Yubikey { Ok(metadata.policy.map(|(_, touch_policy)| touch_policy)) } - /// Returns whether a YubiKey PIV slot requires touch for signing. + /// Returns the touch requirement for a YubiKey PIV slot. /// /// Returns `TouchRequirement::Unknown` when the key uses the device default /// or the device does not expose policy metadata. diff --git a/src/yubikey/piv/mod.rs b/src/yubikey/piv/mod.rs index 3ebca71..9a52d70 100644 --- a/src/yubikey/piv/mod.rs +++ b/src/yubikey/piv/mod.rs @@ -47,11 +47,10 @@ 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 management::ManagementKeyAlgorithm; pub use yubikey::piv::{AlgorithmId, RetiredSlotId, SlotId}; +pub use management::ManagementKeyAlgorithm; pub use yubikey::{PinPolicy, TouchPolicy}; /// Structure to wrap a yubikey and abstract actions From 9050c549cbbc7943a751776eb8986c31d1ae426f Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Mon, 1 Jun 2026 13:34:52 -0400 Subject: [PATCH 6/9] Update default --- src/yubikey/piv/management.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/yubikey/piv/management.rs b/src/yubikey/piv/management.rs index f0afd69..2bd07b3 100644 --- a/src/yubikey/piv/management.rs +++ b/src/yubikey/piv/management.rs @@ -50,8 +50,8 @@ pub const SECP384_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.132. fn touch_policy_requirement(touch_policy: TouchPolicy) -> TouchRequirement { match touch_policy { TouchPolicy::Always | TouchPolicy::Cached => TouchRequirement::Required, - TouchPolicy::Never => TouchRequirement::NotRequired, - TouchPolicy::Default => TouchRequirement::Unknown, + // YubiKey PIV defaults to no touch; Unknown is only for missing metadata. + TouchPolicy::Never | TouchPolicy::Default => TouchRequirement::NotRequired, } } @@ -419,7 +419,7 @@ mod tests { ); assert_eq!( touch_policy_requirement(TouchPolicy::Default), - TouchRequirement::Unknown + TouchRequirement::NotRequired ); } } From 78439fe9ef8da8038b636d91c7b1459a36d7b75d Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Sat, 27 Jun 2026 16:52:26 +0100 Subject: [PATCH 7/9] Update docs --- src/yubikey/piv/management.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/yubikey/piv/management.rs b/src/yubikey/piv/management.rs index 2bd07b3..3bc6ca5 100644 --- a/src/yubikey/piv/management.rs +++ b/src/yubikey/piv/management.rs @@ -267,8 +267,9 @@ impl super::Yubikey { /// Returns the touch requirement for a YubiKey PIV slot. /// - /// Returns `TouchRequirement::Unknown` when the key uses the device default - /// or the device does not expose policy metadata. + /// 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)? From bf81845525f3367a0e731803d4e5bdbdb94026b3 Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Mon, 29 Jun 2026 23:00:42 +0100 Subject: [PATCH 8/9] Remove nonexhaustive --- src/ssh/privkey.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ssh/privkey.rs b/src/ssh/privkey.rs index 05e34a9..b790211 100644 --- a/src/ssh/privkey.rs +++ b/src/ssh/privkey.rs @@ -39,7 +39,6 @@ use bcrypt_pbkdf::bcrypt_pbkdf; const SSH_SK_USER_PRESENCE_REQD: u8 = 0x01; /// Whether a hardware-backed key requires a user touch for signing. -#[non_exhaustive] #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum TouchRequirement { /// Signing requires a user touch. From 2daf1ebcd3a4dc0579b90fbe1a3595428edcd8e5 Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Mon, 29 Jun 2026 23:06:07 +0100 Subject: [PATCH 9/9] Bump 0.15.1 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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"