Skip to content
Open
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
13 changes: 8 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "sshcerts"
version = "0.13.2"
version = "0.14.0"
authors = ["Mitchell Grenier <mitchell@confurious.io>"]
edition = "2021"
license-file = "LICENSE"
Expand Down Expand Up @@ -48,7 +48,7 @@ fido-lite = ["minicbor", "x509-parser"]
rsa-signing = ["simple_asn1", "num-bigint"]
x509-support = ["der-parser", "x509", "x509-parser"]
yubikey-support = ["rcgen", "yubikey", "yubikey-lite"]
yubikey-lite = ["x509-support"]
yubikey-lite = ["x509-support", "der", "x509-cert"]

[dependencies]
base64 = "0.13"
Expand All @@ -61,12 +61,14 @@ simple_asn1 = { version = "0.5", optional = true }
num-bigint = { version = "0.4", optional = true }

# Dependencies for yubikey-support
yubikey = { version = "0.7", features = ["untested"], optional = true }
yubikey = { version = "0.8", features = ["untested"], optional = true }
lexical-core = { version = ">0.7.4", optional = true }
rcgen = { version = "0.11", optional = true }
x509 = { version = "0.2", optional = true }
x509-parser = { version = "0.15", features = ["verify"], optional = true }
der-parser = { version = "5", optional = true }
der = { version = "0.7", optional = true }
x509-cert = { version = "0.2", optional = true }

# Dependencies for encrypted-keys
aes = { version = "0.7", features = ["ctr"], optional = true }
Expand All @@ -77,14 +79,13 @@ ctr = { version = "0.8", optional = true }
minicbor = { version = "0.13", optional = true }

# Dependencies for fido-support-mozilla
authenticator = { version = "0.4.0-alpha.24", default-features = false, features = [
authenticator = { version = "0.4.1", default-features = false, features = [
"crypto_openssl",
], optional = true }
# authenticator = { path = "../authenticator-rs", default-features = false, features = [
# "crypto_openssl",
# ], optional = true }


# Dependencies for fido-support
ctap-hid-fido2 = { version = "3", optional = true }
#ctap-hid-fido2 = {git = "https://github.com/gebogebogebo/ctap-hid-fido2", branch="master", optional = true}
Expand All @@ -96,6 +97,8 @@ env_logger = "0.8.2"
hex = "0.4.2"
clap = "3.0.5"
criterion = "0.3"
p256 = "*"
p384 = "*"

[[bench]]
name = "certs_per_second"
Expand Down
4 changes: 2 additions & 2 deletions benches/certs_per_second.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use criterion::{criterion_group, criterion_main, Criterion};

use sshcerts::yubikey::{RetiredSlotId, SlotId, Yubikey};
use sshcerts::yubikey::piv::Yubikey;
use yubikey::piv::{RetiredSlotId, SlotId};

fn generate_certs(n: u64) -> () {
let data = [0; 32];
Expand Down
35 changes: 33 additions & 2 deletions examples/sign-cert-with-yubikey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,22 @@ fn slot_validator(slot: &str) -> Result<(), String> {

struct YubikeySigner {
slot: SlotId,
pin: String,
mgm_key: Vec<u8>,
}

impl SSHCertificateSigner for YubikeySigner {
fn sign(&self, buffer: &[u8]) -> Option<Vec<u8>> {
let mut yk = Yubikey::new().unwrap();
yk.unlock(self.pin.as_bytes(), &self.mgm_key).unwrap();
println!("Unlocking Successful");

match yk.ssh_cert_signer(buffer, &self.slot) {
Ok(sig) => Some(sig),
Err(_) => None,
Err(e) => {
println!("Error signing: {:?}", e);
None
}
}
}
}
Expand Down Expand Up @@ -79,14 +87,37 @@ fn main() {
.required(true)
.takes_value(true),
)
.arg(
Arg::new("pin")
.help("Provision this slot with a new private key. The pin number must be passed as parameter here")
.default_value("123456")
.long("pin")
.short('p')
.takes_value(true),
)
.arg(
Arg::new("management-key")
.help("Provision this slot with a new private key. The pin number must be passed as parameter here")
.default_value("010203040506070801020304050607080102030405060708")
.long("mgmkey")
.short('m')
.takes_value(true),
)
.get_matches();

let slot = slot_parser(matches.value_of("slot").unwrap()).unwrap();
let mut yk = Yubikey::new().unwrap();

let yk_pubkey = yk.ssh_cert_fetch_pubkey(&slot).unwrap();

let ssh_pubkey = PublicKey::from_path(matches.value_of("key").unwrap()).unwrap();
println!("Signing {ssh_pubkey} with {yk_pubkey}");

let yk_signer = YubikeySigner { slot };
let yk_signer = YubikeySigner {
slot,
pin: matches.value_of("pin").unwrap().to_string(),
mgm_key: hex::decode(matches.value_of("management-key").unwrap()).unwrap(),
};

let user_cert = Certificate::builder(&ssh_pubkey, CertType::User, &yk_pubkey)
.unwrap()
Expand Down
18 changes: 9 additions & 9 deletions examples/yk-provision.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::env;
use clap::{Arg, Command};

use sshcerts::yubikey::piv::Yubikey;
use sshcerts::yubikey::piv::{AlgorithmId, PinPolicy, RetiredSlotId, SlotId, TouchPolicy};
use sshcerts::yubikey::piv::{PinPolicy, RetiredSlotId, SlotId, TouchPolicy};

use std::convert::TryFrom;

Expand All @@ -15,11 +15,6 @@ fn provision_new_key(
alg: &str,
secure: bool,
) {
let alg = match alg {
"p256" => AlgorithmId::EccP256,
_ => AlgorithmId::EccP384,
};

println!(
"Provisioning new {:?} key called [{}] in slot: {:?}",
alg, subject, slot
Expand All @@ -34,10 +29,15 @@ fn provision_new_key(

let mut yk = Yubikey::new().unwrap();
yk.unlock(pin.as_bytes(), mgm_key).unwrap();
match yk.provision(&slot, subject, alg, policy, PinPolicy::Never) {
Ok(pk) => {
println!("New hardware backed SSH Public Key: {}", pk);
let result = match alg {
"p256" => yk.provision::<p256::NistP256>(&slot, subject, policy, PinPolicy::Never),
_ => {
println!("Using P384");
yk.provision::<p384::NistP384>(&slot, subject, policy, PinPolicy::Never)
}
};
match result {
Ok(pk) => println!("New hardware backed SSH Public Key: {}", pk),
Err(e) => panic!("Could not provision device with new key: {:?}", e),
}
}
Expand Down
121 changes: 89 additions & 32 deletions src/yubikey/piv/management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,23 @@ use crate::PublicKey;

use ring::digest;

use yubikey::certificate::{Certificate, PublicKeyInfo};
use yubikey::certificate::Certificate;
use yubikey::piv::{attest, sign_data as yk_sign_data, AlgorithmId, SlotId};
use yubikey::{MgmKey, YubiKey};
use yubikey::{PinPolicy, TouchPolicy};

use x509::RelativeDistinguishedName;

use super::{Error, Result};

use x509_cert::{
der::{oid::ObjectIdentifier, Encode},
name::Name,
serial_number::SerialNumber,
time::Validity,
};

use std::str::FromStr;
use yubikey::certificate::yubikey_signer;

#[derive(Debug)]
/// A struct that allows the generation of CSRs via the rcgen library. This is
/// only used when calling the `generate_csr` function.
Expand All @@ -21,16 +29,44 @@ pub struct CSRSigner {
algorithm: AlgorithmId,
}

const NISTP256_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.3.1.7");
const SECP384_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.132.0.34");

impl CSRSigner {
/// Create a new certificate signer based on a Yubikey serial
/// and slot
pub fn new(serial: u32, slot: SlotId) -> Self {
let mut yk = super::Yubikey::open(serial).unwrap();
let pki = yk.configured(&slot).unwrap();
let (public_key, algorithm) = match pki {
PublicKeyInfo::Rsa { pubkey: _, .. } => panic!("RSA keys not supported"),
PublicKeyInfo::EcP256(pubkey) => (pubkey.as_bytes().to_vec(), AlgorithmId::EccP256),
PublicKeyInfo::EcP384(pubkey) => (pubkey.as_bytes().to_vec(), AlgorithmId::EccP384),
let cert = yk.configured(&slot).unwrap();
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hasn't been tested

let pki = cert.subject_pki();
let oid_alg = pki.algorithm.parameters_oid().unwrap();

let (public_key, algorithm) = match oid_alg {
NISTP256_OID => {
// This is the OID for ECDSA with SHA256
(
pki.subject_public_key
.raw_bytes()
.to_vec()
.to_der()
.unwrap(),
AlgorithmId::EccP256,
)
}
SECP384_OID => {
// This is the OID for ECDSA with SHA384
(
pki.subject_public_key
.raw_bytes()
.to_vec()
.to_der()
.unwrap(),
AlgorithmId::EccP384,
)
}
_ => {
panic!("Unsupported algorithm");
}
};

Self {
Expand All @@ -54,7 +90,8 @@ impl rcgen::RemoteKeyPair for CSRSigner {
return Err(rcgen::RcgenError::RemoteKeyError);
};

yk.sign_data(message, self.algorithm, &self.slot).map_err(|_| rcgen::RcgenError::RemoteKeyError)
yk.sign_data(message, self.algorithm, &self.slot)
.map_err(|_| rcgen::RcgenError::RemoteKeyError)
}

fn algorithm(&self) -> &'static rcgen::SignatureAlgorithm {
Expand Down Expand Up @@ -108,9 +145,9 @@ impl super::Yubikey {
}

/// Check to see that a provided Yubikey and slot is configured for signing
pub fn configured(&mut self, slot: &SlotId) -> Result<PublicKeyInfo> {
pub fn configured(&mut self, slot: &SlotId) -> Result<Certificate> {
let cert = Certificate::read(&mut self.yk, *slot)?;
Ok(cert.subject_pki().clone())
Ok(cert)
}

/// Check to see that a provided Yubikey and slot is configured for signing
Expand All @@ -122,7 +159,9 @@ impl super::Yubikey {
/// Fetch the certificate from a given Yubikey slot.
pub fn fetch_certificate(&mut self, slot: &SlotId) -> Result<Vec<u8>> {
let cert = Certificate::read(&mut self.yk, *slot)?;
Ok(cert.as_ref().to_vec())
Ok(cert.cert.to_der().map_err(|e| {
Error::InternalYubiKeyError(format!("Failed to encode certificate: {}", e))
})?)
}

/// Write the certificate from a given Yubikey slot.
Expand All @@ -142,45 +181,57 @@ impl super::Yubikey {
/// 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![]);
params.alg = match self.configured(slot)? {
PublicKeyInfo::EcP256(_) => &rcgen::PKCS_ECDSA_P256_SHA256,
PublicKeyInfo::EcP384(_) => &rcgen::PKCS_ECDSA_P384_SHA384,
let cert = self.configured(&slot).unwrap();
let pki = cert.subject_pki();
let oid_alg = pki
.algorithm
.parameters_oid()
.map_err(|_| Error::Unsupported)?;

params.alg = match oid_alg {
NISTP256_OID => &rcgen::PKCS_ECDSA_P256_SHA256,
SECP384_OID => &rcgen::PKCS_ECDSA_P384_SHA384,
_ => return Err(Error::Unsupported),
};
params
.distinguished_name
.push(rcgen::DnType::CommonName, common_name.to_string());

let csr_signer = CSRSigner::new(self.yk.serial().into(), *slot);
params.key_pair = Some(rcgen::KeyPair::from_remote(Box::new(csr_signer)).map_err(|e| Error::InternalYubiKeyError(format!("{}", e)))?);
params.key_pair = Some(
rcgen::KeyPair::from_remote(Box::new(csr_signer))
.map_err(|e| Error::InternalYubiKeyError(format!("{}", e)))?,
);

let csr = rcgen::Certificate::from_params(params).map_err(|e| Error::InternalYubiKeyError(format!("{}", e)))?;
let csr = csr.serialize_request_der().map_err(|e| Error::InternalYubiKeyError(format!("{}", e)))?;
let csr = rcgen::Certificate::from_params(params)
.map_err(|e| Error::InternalYubiKeyError(format!("{}", e)))?;
let csr = csr
.serialize_request_der()
.map_err(|e| Error::InternalYubiKeyError(format!("{}", e)))?;

Ok(csr)
}

/// Provisions the YubiKey with a new certificate generated on the device.
/// Only keys that are generated this way can use the attestation functionality.
pub fn provision(
pub fn provision<KT: yubikey_signer::KeyType>(
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we hide this away and just provide two non-generic functions for p256 and p384

&mut self,
slot: &SlotId,
common_name: &str,
alg: AlgorithmId,
touch_policy: TouchPolicy,
pin_policy: PinPolicy,
) -> Result<PublicKey> {
let key_info = yubikey::piv::generate(&mut self.yk, *slot, alg, pin_policy, touch_policy)?;
let extensions: &[x509::Extension<'_, &[u64]>] = &[];
let key_info =
yubikey::piv::generate(&mut self.yk, *slot, KT::ALGORITHM, pin_policy, touch_policy)?;
// Generate a self-signed certificate for the new key.
Certificate::generate_self_signed(
Certificate::generate_self_signed::<_, KT>(
&mut self.yk,
*slot,
[0u8; 20],
None,
&[RelativeDistinguishedName::common_name(common_name)],
SerialNumber::new(&[0; 20]).unwrap(),
Validity::from_now(std::time::Duration::new(3600 * 24 * 3650, 0)).unwrap(),
Name::from_str(&format!("CN={}", common_name)).unwrap(),
key_info,
extensions,
|_builder| Ok(()),
)?;

self.ssh_cert_fetch_pubkey(slot)
Expand All @@ -191,11 +242,17 @@ 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 (slot_alg, hash_alg) = match self.configured(slot) {
Ok(PublicKeyInfo::EcP256(_)) => (AlgorithmId::EccP256, &digest::SHA256),
Ok(PublicKeyInfo::EcP384(_)) => (AlgorithmId::EccP384, &digest::SHA384),
Ok(_) => (AlgorithmId::Rsa2048, &digest::SHA256), // RSAish
Err(_) => return Err(Error::Unprovisioned),
let cert = self.configured(&slot).unwrap();
let pki = cert.subject_pki();
let oid_alg = pki
.algorithm
.parameters_oid()
.map_err(|_| Error::Unprovisioned)?;

let (slot_alg, hash_alg) = match oid_alg {
NISTP256_OID => (AlgorithmId::EccP256, &digest::SHA256),
SECP384_OID => (AlgorithmId::EccP384, &digest::SHA384),
_ => return Err(Error::Unprovisioned),
};

if slot_alg != alg {
Expand Down