Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "sshcerts"
version = "0.15.0"
version = "0.15.1"
authors = ["Mitchell Grenier <mitchell@confurious.io>"]
edition = "2021"
license-file = "LICENSE"
Expand Down
9 changes: 8 additions & 1 deletion src/fido/generate/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::PrivateKey;
use crate::{PrivateKey, TouchRequirement};

#[cfg(any(feature = "fido-support"))]
mod ctap2_hid;
Expand Down Expand Up @@ -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()
}
}
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ pub mod error;

type Result<T> = std::result::Result<T, error::Error>;

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.
Expand Down
2 changes: 1 addition & 1 deletion src/ssh/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
55 changes: 55 additions & 0 deletions src/ssh/privkey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -125,6 +155,20 @@ pub struct Ed25519SkPrivateKey {
pub device_path: Option<String>,
}

impl EcdsaSkPrivateKey {
/// Returns the touch requirement for this hardware-backed key.
pub fn touch_requirement(&self) -> TouchRequirement {
touch_requirement_from_flags(self.flags)
}
Comment thread
timweri marked this conversation as resolved.
Comment thread
timweri marked this conversation as resolved.
}

impl Ed25519SkPrivateKey {
/// Returns the touch requirement for this hardware-backed key.
pub fn touch_requirement(&self) -> TouchRequirement {
touch_requirement_from_flags(self.flags)
}
}
Comment thread
timweri marked this conversation as resolved.
Comment thread
timweri marked this conversation as resolved.

/// A type which represents the different kinds a public key can be.
#[derive(Debug, PartialEq, Eq, Clone, Zeroize)]
pub enum PrivateKeyKind {
Expand Down Expand Up @@ -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<u8> {
let mut serializer = Writer::new();
Expand Down
83 changes: 75 additions & 8 deletions src/yubikey/piv/management.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::PublicKey;
use crate::{PublicKey, TouchRequirement};

use ring::digest;

Expand Down Expand Up @@ -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<Self> {
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 {
Expand Down Expand Up @@ -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<Option<TouchPolicy>> {
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()),
};
Comment thread
timweri marked this conversation as resolved.
Comment thread
timweri marked this conversation as resolved.

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<TouchRequirement> {
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<Vec<u8>> {
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
Expand Down Expand Up @@ -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<Vec<u8>> {
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
Expand All @@ -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
);
}
}
4 changes: 2 additions & 2 deletions src/yubikey/piv/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/// Implements KeyType trait
/// Implements KeyType trait
pub mod keytype;

/// Contains all the functions used for creating new keys, unlocking, and
Expand Down Expand Up @@ -47,7 +47,7 @@ impl std::error::Error for Error {}

type Result<T> = std::result::Result<T, Error>;

// 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;
Expand Down
50 changes: 49 additions & 1 deletion tests/sk-privkey.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use sshcerts::ssh::PrivateKey;
use sshcerts::ssh::{PrivateKey, PrivateKeyKind, TouchRequirement};
use std::io::BufWriter;

#[test]
Expand All @@ -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();
Expand All @@ -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);
}
Loading