Implement --identity command

This commit is contained in:
Jack Grigg
2021-01-01 21:12:52 +00:00
parent babe64da42
commit 7a527b2be6
7 changed files with 287 additions and 2 deletions
+37
View File
@@ -1,9 +1,19 @@
use std::fmt;
use std::io;
use yubikey_piv::{key::RetiredSlotId, Serial};
use crate::USABLE_SLOTS;
pub enum Error {
InvalidSlot(u8),
Io(io::Error),
MultipleCommands,
MultipleIdentities,
MultipleYubiKeys,
NoIdentities,
NoMatchingSerial(Serial),
SlotHasNoIdentity(RetiredSlotId),
TimedOut,
YubiKey(yubikey_piv::Error),
}
@@ -24,11 +34,38 @@ impl From<yubikey_piv::error::Error> for Error {
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::InvalidSlot(slot) => writeln!(
f,
"Invalid slot '{}' (expected number between 1 and 20).",
slot
)?,
Error::Io(e) => writeln!(f, "Failed to set up YubiKey: {}", e)?,
Error::MultipleCommands => writeln!(
f,
"Only one of --generate, --identity, --list, --list-all can be specified."
)?,
Error::MultipleIdentities => writeln!(
f,
"This YubiKey has multiple age identities. Use --slot to select a single identity."
)?,
Error::MultipleYubiKeys => writeln!(
f,
"Multiple YubiKeys are plugged in. Use --serial to select a single YubiKey."
)?,
Error::NoIdentities => {
writeln!(f, "This YubiKey does not contain any age identities.")?
}
Error::NoMatchingSerial(serial) => {
writeln!(f, "Could not find YubiKey with serial {}.", serial)?
}
Error::SlotHasNoIdentity(slot) => writeln!(
f,
"Slot {} does not contain an age identity or compatible key.",
USABLE_SLOTS.iter().position(|s| s == slot).unwrap() + 1
)?,
Error::TimedOut => {
writeln!(f, "Timed out while waiting for a YubiKey to be inserted.")?
}
Error::YubiKey(e) => match e {
yubikey_piv::error::Error::NotFound => {
writeln!(f, "Please insert the YubiKey you want to set up")?
+75 -1
View File
@@ -10,11 +10,13 @@ mod error;
mod p256;
mod plugin;
mod util;
mod yubikey;
use error::Error;
const PLUGIN_NAME: &str = "age-plugin-yubikey";
const RECIPIENT_PREFIX: &str = "age1yubikey";
const IDENTITY_PREFIX: &str = "age-plugin-yubikey-";
const USABLE_SLOTS: [RetiredSlotId; 20] = [
RetiredSlotId::R1,
@@ -62,6 +64,78 @@ struct PluginOptions {
#[options(help = "List all YubiKey keys that are compatible with age.", no_short)]
list_all: bool,
#[options(
help = "Specify which YubiKey to use, if more than one is plugged in.",
no_short
)]
serial: Option<u32>,
#[options(
help = "Specify which slot to use. Defaults to first usable slot.",
no_short
)]
slot: Option<u8>,
}
fn identity(opts: PluginOptions) -> Result<(), Error> {
let serial = opts.serial.map(|s| s.into());
let slot = opts
.slot
.map(|slot| {
USABLE_SLOTS
.get(slot as usize - 1)
.cloned()
.ok_or(Error::InvalidSlot(slot))
})
.transpose()?;
let mut yubikey = yubikey::open(serial)?;
let mut keys = Key::list(&mut yubikey)?.into_iter().filter_map(|key| {
// - We only use the retired slots.
// - Only P-256 keys are compatible with us.
match (key.slot(), key.certificate().subject_pki()) {
(SlotId::Retired(slot), PublicKeyInfo::EcP256(pubkey)) => {
p256::Recipient::from_pubkey(*pubkey).map(|r| (key, slot, r))
}
_ => None,
}
});
let (key, slot, recipient) = if let Some(slot) = slot {
keys.find(|(_, s, _)| s == &slot)
.ok_or(Error::SlotHasNoIdentity(slot))
} else {
let mut keys = keys.filter(|(key, _, _)| {
let cert = x509_parser::parse_x509_certificate(key.certificate().as_ref())
.map(|(_, cert)| cert)
.ok();
match cert
.as_ref()
.and_then(|cert| cert.subject().iter_organization().next())
{
Some(org) => org.as_str() == Ok(PLUGIN_NAME),
_ => false,
}
});
match (keys.next(), keys.next()) {
(None, None) => Err(Error::NoIdentities),
(Some(key), None) => Ok(key),
(Some(_), Some(_)) => Err(Error::MultipleIdentities),
(None, Some(_)) => unreachable!(),
}
}?;
let stub = yubikey::Stub::new(yubikey.serial(), slot, &recipient);
let created = x509_parser::parse_x509_certificate(key.certificate().as_ref())
.ok()
.map(|(_, cert)| cert.validity().not_before.to_rfc2822())
.unwrap_or_else(|| "Unknown".to_owned());
util::print_identity(stub, recipient, &created);
Ok(())
}
fn list(all: bool) -> Result<(), Error> {
@@ -141,7 +215,7 @@ fn main() -> Result<(), Error> {
} else if opts.generate {
todo!()
} else if opts.identity {
todo!()
identity(opts)
} else if opts.list {
list(false)
} else if opts.list_all {
+9
View File
@@ -1,10 +1,14 @@
use bech32::ToBase32;
use elliptic_curve::sec1::EncodedPoint;
use p256::NistP256;
use sha2::{Digest, Sha256};
use std::convert::TryInto;
use std::fmt;
use crate::RECIPIENT_PREFIX;
pub(crate) const TAG_BYTES: usize = 4;
/// Wrapper around a compressed secp256r1 curve point.
#[derive(Clone)]
pub struct Recipient(EncodedPoint<NistP256>);
@@ -43,4 +47,9 @@ impl Recipient {
pub(crate) fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
pub(crate) fn tag(&self) -> [u8; TAG_BYTES] {
let tag = Sha256::digest(self.to_string().as_bytes());
(&tag[0..TAG_BYTES]).try_into().expect("length is correct")
}
}
+12 -1
View File
@@ -4,7 +4,7 @@ use yubikey_piv::{
Key, YubiKey,
};
use crate::PLUGIN_NAME;
use crate::{p256::Recipient, yubikey::Stub, PLUGIN_NAME};
const POLICY_EXTENSION_OID: &[u64] = &[1, 3, 6, 1, 4, 1, 41482, 3, 8];
@@ -111,3 +111,14 @@ pub(crate) fn extract_name_and_policies(
}
})
}
pub(crate) fn print_identity(stub: Stub, recipient: Recipient, created: &str) {
let recipient = recipient.to_string();
if !console::user_attended() {
eprintln!("Recipient: {}", recipient);
}
println!("# created: {}", created);
println!("# recipient: {}", recipient);
println!("{}", stub.to_string());
}
+113
View File
@@ -0,0 +1,113 @@
//! Structs for handling YubiKeys.
use bech32::ToBase32;
use std::fmt;
use std::thread::sleep;
use std::time::{Duration, SystemTime};
use yubikey_piv::{key::RetiredSlotId, yubikey::Serial, Readers, YubiKey};
use crate::{
error::Error,
p256::{Recipient, TAG_BYTES},
IDENTITY_PREFIX,
};
const ONE_SECOND: Duration = Duration::from_secs(1);
const FIFTEEN_SECONDS: Duration = Duration::from_secs(15);
pub(crate) fn wait_for_readers() -> Result<Readers, Error> {
// Start a 15-second timer waiting for a YubiKey to be inserted (if necessary).
let start = SystemTime::now();
loop {
let mut readers = Readers::open()?;
if readers.iter()?.len() > 0 {
break Ok(readers);
}
match SystemTime::now().duration_since(start) {
Ok(end) if end >= FIFTEEN_SECONDS => return Err(Error::TimedOut),
_ => sleep(ONE_SECOND),
}
}
}
pub(crate) fn open(serial: Option<Serial>) -> Result<YubiKey, Error> {
if Readers::open()?.iter()?.len() == 0 {
if let Some(serial) = serial {
eprintln!("⏳ Please insert the YubiKey with serial {}.", serial);
} else {
eprintln!("⏳ Please insert the YubiKey.");
}
}
let mut readers = wait_for_readers()?;
let mut readers_iter = readers.iter()?;
// --serial selects the YubiKey to use. If not provided, and more than one YubiKey is
// connected, an error is returned.
let yubikey = match (readers_iter.len(), serial) {
(0, _) => unreachable!(),
(1, None) => readers_iter.next().unwrap().open()?,
(1, Some(serial)) => {
let yubikey = readers_iter.next().unwrap().open()?;
if yubikey.serial() != serial {
return Err(Error::NoMatchingSerial(serial));
}
yubikey
}
(_, Some(serial)) => {
let reader = readers_iter
.find(|reader| match reader.open() {
Ok(yk) => yk.serial() == serial,
_ => false,
})
.ok_or(Error::NoMatchingSerial(serial))?;
reader.open()?
}
(_, None) => return Err(Error::MultipleYubiKeys),
};
Ok(yubikey)
}
/// A reference to an age key stored in a YubiKey.
#[derive(Debug)]
pub struct Stub {
pub(crate) serial: Serial,
pub(crate) slot: RetiredSlotId,
pub(crate) tag: [u8; TAG_BYTES],
identity_index: usize,
}
impl fmt::Display for Stub {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(
bech32::encode(IDENTITY_PREFIX, self.to_bytes().to_base32())
.expect("HRP is valid")
.to_uppercase()
.as_str(),
)
}
}
impl Stub {
/// Returns a key stub and recipient for this `(Serial, SlotId, PublicKey)` tuple.
///
/// Does not check that the `PublicKey` matches the given `(Serial, SlotId)` tuple;
/// this is checked at decryption time.
pub(crate) fn new(serial: Serial, slot: RetiredSlotId, recipient: &Recipient) -> Self {
Stub {
serial,
slot,
tag: recipient.tag(),
identity_index: 0,
}
}
fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(9);
bytes.extend_from_slice(&self.serial.0.to_le_bytes());
bytes.push(self.slot.into());
bytes.extend_from_slice(&self.tag);
bytes
}
}