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:
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user