Implement --list and --list-all commands

Requires a MSRV of 1.44 due to the transitive dependency on bitvec 0.19.
This commit is contained in:
Jack Grigg
2021-01-01 03:23:37 +00:00
parent 3ee6d59dcd
commit babe64da42
7 changed files with 890 additions and 12 deletions
+19
View File
@@ -4,6 +4,7 @@ use std::io;
pub enum Error {
Io(io::Error),
MultipleCommands,
YubiKey(yubikey_piv::Error),
}
impl From<io::Error> for Error {
@@ -12,6 +13,12 @@ impl From<io::Error> for Error {
}
}
impl From<yubikey_piv::error::Error> for Error {
fn from(e: yubikey_piv::error::Error) -> Self {
Error::YubiKey(e)
}
}
// Rust only supports `fn main() -> Result<(), E: Debug>`, so we implement `Debug`
// manually to provide the error output we want.
impl fmt::Debug for Error {
@@ -22,6 +29,18 @@ impl fmt::Debug for Error {
f,
"Only one of --generate, --identity, --list, --list-all can be specified."
)?,
Error::YubiKey(e) => match e {
yubikey_piv::error::Error::NotFound => {
writeln!(f, "Please insert the YubiKey you want to set up")?
}
e => {
writeln!(f, "Error while communicating with YubiKey: {}", e)?;
use std::error::Error;
if let Some(inner) = e.source() {
writeln!(f, "Cause: {}", inner)?;
}
}
},
}
writeln!(f)?;
writeln!(
+90 -2
View File
@@ -1,11 +1,44 @@
use age_plugin::run_state_machine;
use gumdrop::Options;
use yubikey_piv::{
certificate::PublicKeyInfo,
key::{RetiredSlotId, SlotId},
Key, Readers,
};
mod error;
mod p256;
mod plugin;
mod util;
use error::Error;
const PLUGIN_NAME: &str = "age-plugin-yubikey";
const RECIPIENT_PREFIX: &str = "age1yubikey";
const USABLE_SLOTS: [RetiredSlotId; 20] = [
RetiredSlotId::R1,
RetiredSlotId::R2,
RetiredSlotId::R3,
RetiredSlotId::R4,
RetiredSlotId::R5,
RetiredSlotId::R6,
RetiredSlotId::R7,
RetiredSlotId::R8,
RetiredSlotId::R9,
RetiredSlotId::R10,
RetiredSlotId::R11,
RetiredSlotId::R12,
RetiredSlotId::R13,
RetiredSlotId::R14,
RetiredSlotId::R15,
RetiredSlotId::R16,
RetiredSlotId::R17,
RetiredSlotId::R18,
RetiredSlotId::R19,
RetiredSlotId::R20,
];
#[derive(Debug, Options)]
struct PluginOptions {
#[options(help = "Print this help message and exit.")]
@@ -31,6 +64,61 @@ struct PluginOptions {
list_all: bool,
}
fn list(all: bool) -> Result<(), Error> {
let mut readers = Readers::open()?;
for reader in readers.iter()? {
let mut yubikey = reader.open()?;
for key in Key::list(&mut yubikey)? {
// We only use the retired slots.
let slot = match key.slot() {
SlotId::Retired(slot) => slot,
_ => continue,
};
// Only P-256 keys are compatible with us.
let recipient = match key.certificate().subject_pki() {
PublicKeyInfo::EcP256(pubkey) => match p256::Recipient::from_pubkey(*pubkey) {
Some(recipient) => recipient,
None => continue,
},
_ => continue,
};
let ((name, pin_policy, touch_policy), created) =
match x509_parser::parse_x509_certificate(key.certificate().as_ref())
.ok()
.and_then(|(_, cert)| {
util::extract_name_and_policies(&mut yubikey, &key, &cert, all)
.map(|res| (res, cert.validity().not_before.to_rfc2822()))
}) {
Some(res) => res,
None => continue,
};
println!(
"# Serial: {}, Slot: {}",
yubikey.serial(),
// Use 1-indexing in the UI for niceness
USABLE_SLOTS.iter().position(|s| s == &slot).unwrap() + 1,
);
println!("# Name: {}", name);
println!("# Created: {}", created);
println!("# PIN policy: {}", util::pin_policy_to_str(pin_policy));
println!(
"# Touch policy: {}",
util::touch_policy_to_str(touch_policy)
);
println!("{}", recipient.to_string());
println!();
}
println!();
}
Ok(())
}
fn main() -> Result<(), Error> {
let opts = PluginOptions::parse_args_default_or_exit();
@@ -55,9 +143,9 @@ fn main() -> Result<(), Error> {
} else if opts.identity {
todo!()
} else if opts.list {
todo!()
list(false)
} else if opts.list_all {
todo!()
list(true)
} else {
// TODO: CLI identity generation
Ok(())
+46
View File
@@ -0,0 +1,46 @@
use bech32::ToBase32;
use elliptic_curve::sec1::EncodedPoint;
use p256::NistP256;
use std::fmt;
use crate::RECIPIENT_PREFIX;
/// Wrapper around a compressed secp256r1 curve point.
#[derive(Clone)]
pub struct Recipient(EncodedPoint<NistP256>);
impl fmt::Debug for Recipient {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Recipient({:?})", self.as_bytes())
}
}
impl fmt::Display for Recipient {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(
bech32::encode(RECIPIENT_PREFIX, self.as_bytes().to_base32())
.expect("HRP is valid")
.as_str(),
)
}
}
impl Recipient {
/// Attempts to parse a valid secp256r1 public key from its SEC-1 encoding.
pub(crate) fn from_pubkey(pubkey: EncodedPoint<NistP256>) -> Option<Self> {
if pubkey.is_compressed() {
if pubkey.decompress().is_some().into() {
Some(Recipient(pubkey))
} else {
None
}
} else {
Some(Recipient(pubkey.compress()))
}
}
/// Returns the compressed SEC-1 encoding of this public key.
pub(crate) fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
}
+113
View File
@@ -0,0 +1,113 @@
use x509_parser::{certificate::X509Certificate, der_parser::oid::Oid};
use yubikey_piv::{
policy::{PinPolicy, TouchPolicy},
Key, YubiKey,
};
use crate::PLUGIN_NAME;
const POLICY_EXTENSION_OID: &[u64] = &[1, 3, 6, 1, 4, 1, 41482, 3, 8];
pub(crate) fn pin_policy_to_str(policy: Option<PinPolicy>) -> &'static str {
match policy {
Some(PinPolicy::Always) => "Always (A PIN is required for every decryption, if set)",
Some(PinPolicy::Once) => "Once (A PIN is required once per session, if set)",
Some(PinPolicy::Never) => "Never (A PIN is NOT required to decrypt)",
_ => "Unknown",
}
}
pub(crate) fn touch_policy_to_str(policy: Option<TouchPolicy>) -> &'static str {
match policy {
Some(TouchPolicy::Always) => "Always (A physical touch is required for every decryption)",
Some(TouchPolicy::Cached) => {
"Cached (A physical touch is required for decryption, and is cached for 15 seconds)"
}
Some(TouchPolicy::Never) => "Never (A physical touch is NOT required to decrypt)",
_ => "Unknown",
}
}
pub(crate) fn extract_name(cert: &X509Certificate, all: bool) -> Option<(String, bool)> {
// Look at Subject Organization to determine if we created this.
match cert.subject().iter_organization().next() {
Some(org) if org.as_str() == Ok(PLUGIN_NAME) => {
// We store the identity name as a Common Name attribute.
let name = cert
.subject()
.iter_common_name()
.next()
.and_then(|cn| cn.as_str().ok())
.map(|s| s.to_owned())
.unwrap_or_default(); // TODO: This should always be present.
Some((name, true))
}
_ => {
// Not one of ours, but we've already filtered for compatibility.
if !all {
return None;
}
// Display the entire subject.
let name = cert.subject().to_string();
Some((name, false))
}
}
}
pub(crate) fn extract_name_and_policies(
yubikey: &mut YubiKey,
key: &Key,
cert: &X509Certificate,
all: bool,
) -> Option<(String, Option<PinPolicy>, Option<TouchPolicy>)> {
// We store the PIN and touch policies for identities in their certificates
// using the same certificate extension as PIV attestations.
// https://developers.yubico.com/PIV/Introduction/PIV_attestation.html
let policies = |c: &X509Certificate| {
c.extensions()
.get(&Oid::from(POLICY_EXTENSION_OID).unwrap())
// If the encoded extension doesn't have 2 bytes, we assume it is invalid.
.filter(|policy| policy.value.len() >= 2)
.map(|policy| {
// We should only ever see one of three values for either policy, but
// handle unknown values just in case.
let pin_policy = match policy.value[0] {
0x01 => Some(PinPolicy::Never),
0x02 => Some(PinPolicy::Once),
0x03 => Some(PinPolicy::Always),
_ => None,
};
let touch_policy = match policy.value[1] {
0x01 => Some(TouchPolicy::Never),
0x02 => Some(TouchPolicy::Always),
0x03 => Some(TouchPolicy::Cached),
_ => None,
};
(pin_policy, touch_policy)
})
.unwrap_or((None, None))
};
extract_name(cert, all).map(|(name, ours)| {
if ours {
let (pin_policy, touch_policy) = policies(&cert);
(name, pin_policy, touch_policy)
} else {
// We can extract the PIN and touch policies via an attestation. This
// is slow, but the user has asked for all compatible keys, so...
let (pin_policy, touch_policy) = yubikey_piv::key::attest(yubikey, key.slot())
.ok()
.and_then(|buf| {
x509_parser::parse_x509_certificate(&buf)
.map(|(_, c)| policies(&c))
.ok()
})
.unwrap_or((None, None));
(name, pin_policy, touch_policy)
}
})
}