Implement --identity command
This commit is contained in:
Generated
+39
@@ -43,9 +43,11 @@ dependencies = [
|
||||
"age-core",
|
||||
"age-plugin",
|
||||
"bech32",
|
||||
"console",
|
||||
"elliptic-curve",
|
||||
"gumdrop",
|
||||
"p256",
|
||||
"sha2",
|
||||
"x509-parser 0.9.0",
|
||||
"yubikey-piv",
|
||||
]
|
||||
@@ -186,6 +188,21 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cc80946b3480f421c2f17ed1cb841753a371c7c5104f51d507e13f532c856aa"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"regex",
|
||||
"terminal_size",
|
||||
"unicode-width",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.1.0"
|
||||
@@ -313,6 +330,12 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
||||
|
||||
[[package]]
|
||||
name = "ff"
|
||||
version = "0.8.0"
|
||||
@@ -909,6 +932,16 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36474e732d1affd3a6ed582781b3683df3d0563714c59c39591e8ff707cf078e"
|
||||
|
||||
[[package]]
|
||||
name = "terminal_size"
|
||||
version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bd2d183bd3fac5f5fe38ddbeb4dc9aec4a39a9d7d59e7491d900302da01cbe1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.23"
|
||||
@@ -946,6 +979,12 @@ version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.1"
|
||||
|
||||
@@ -14,9 +14,11 @@ edition = "2018"
|
||||
age-core = "0.5"
|
||||
age-plugin = "0.0"
|
||||
bech32 = "0.7.2"
|
||||
console = "0.14"
|
||||
elliptic-curve = "0.6"
|
||||
gumdrop = "0.8"
|
||||
p256 = "0.5"
|
||||
sha2 = "0.9"
|
||||
x509-parser = "0.9"
|
||||
yubikey-piv = { version = "0.1", features = ["untested"] }
|
||||
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user