Implement --identity command
This commit is contained in:
Generated
+39
@@ -43,9 +43,11 @@ dependencies = [
|
|||||||
"age-core",
|
"age-core",
|
||||||
"age-plugin",
|
"age-plugin",
|
||||||
"bech32",
|
"bech32",
|
||||||
|
"console",
|
||||||
"elliptic-curve",
|
"elliptic-curve",
|
||||||
"gumdrop",
|
"gumdrop",
|
||||||
"p256",
|
"p256",
|
||||||
|
"sha2",
|
||||||
"x509-parser 0.9.0",
|
"x509-parser 0.9.0",
|
||||||
"yubikey-piv",
|
"yubikey-piv",
|
||||||
]
|
]
|
||||||
@@ -186,6 +188,21 @@ dependencies = [
|
|||||||
"generic-array",
|
"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]]
|
[[package]]
|
||||||
name = "const-oid"
|
name = "const-oid"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -313,6 +330,12 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encode_unicode"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ff"
|
name = "ff"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@@ -909,6 +932,16 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "36474e732d1affd3a6ed582781b3683df3d0563714c59c39591e8ff707cf078e"
|
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]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.23"
|
version = "1.0.23"
|
||||||
@@ -946,6 +979,12 @@ version = "1.12.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33"
|
checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.1.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-xid"
|
name = "unicode-xid"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ edition = "2018"
|
|||||||
age-core = "0.5"
|
age-core = "0.5"
|
||||||
age-plugin = "0.0"
|
age-plugin = "0.0"
|
||||||
bech32 = "0.7.2"
|
bech32 = "0.7.2"
|
||||||
|
console = "0.14"
|
||||||
elliptic-curve = "0.6"
|
elliptic-curve = "0.6"
|
||||||
gumdrop = "0.8"
|
gumdrop = "0.8"
|
||||||
p256 = "0.5"
|
p256 = "0.5"
|
||||||
|
sha2 = "0.9"
|
||||||
x509-parser = "0.9"
|
x509-parser = "0.9"
|
||||||
yubikey-piv = { version = "0.1", features = ["untested"] }
|
yubikey-piv = { version = "0.1", features = ["untested"] }
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::io;
|
use std::io;
|
||||||
|
use yubikey_piv::{key::RetiredSlotId, Serial};
|
||||||
|
|
||||||
|
use crate::USABLE_SLOTS;
|
||||||
|
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
InvalidSlot(u8),
|
||||||
Io(io::Error),
|
Io(io::Error),
|
||||||
MultipleCommands,
|
MultipleCommands,
|
||||||
|
MultipleIdentities,
|
||||||
|
MultipleYubiKeys,
|
||||||
|
NoIdentities,
|
||||||
|
NoMatchingSerial(Serial),
|
||||||
|
SlotHasNoIdentity(RetiredSlotId),
|
||||||
|
TimedOut,
|
||||||
YubiKey(yubikey_piv::Error),
|
YubiKey(yubikey_piv::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,11 +34,38 @@ impl From<yubikey_piv::error::Error> for Error {
|
|||||||
impl fmt::Debug for Error {
|
impl fmt::Debug for Error {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
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::Io(e) => writeln!(f, "Failed to set up YubiKey: {}", e)?,
|
||||||
Error::MultipleCommands => writeln!(
|
Error::MultipleCommands => writeln!(
|
||||||
f,
|
f,
|
||||||
"Only one of --generate, --identity, --list, --list-all can be specified."
|
"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 {
|
Error::YubiKey(e) => match e {
|
||||||
yubikey_piv::error::Error::NotFound => {
|
yubikey_piv::error::Error::NotFound => {
|
||||||
writeln!(f, "Please insert the YubiKey you want to set up")?
|
writeln!(f, "Please insert the YubiKey you want to set up")?
|
||||||
|
|||||||
+75
-1
@@ -10,11 +10,13 @@ mod error;
|
|||||||
mod p256;
|
mod p256;
|
||||||
mod plugin;
|
mod plugin;
|
||||||
mod util;
|
mod util;
|
||||||
|
mod yubikey;
|
||||||
|
|
||||||
use error::Error;
|
use error::Error;
|
||||||
|
|
||||||
const PLUGIN_NAME: &str = "age-plugin-yubikey";
|
const PLUGIN_NAME: &str = "age-plugin-yubikey";
|
||||||
const RECIPIENT_PREFIX: &str = "age1yubikey";
|
const RECIPIENT_PREFIX: &str = "age1yubikey";
|
||||||
|
const IDENTITY_PREFIX: &str = "age-plugin-yubikey-";
|
||||||
|
|
||||||
const USABLE_SLOTS: [RetiredSlotId; 20] = [
|
const USABLE_SLOTS: [RetiredSlotId; 20] = [
|
||||||
RetiredSlotId::R1,
|
RetiredSlotId::R1,
|
||||||
@@ -62,6 +64,78 @@ struct PluginOptions {
|
|||||||
|
|
||||||
#[options(help = "List all YubiKey keys that are compatible with age.", no_short)]
|
#[options(help = "List all YubiKey keys that are compatible with age.", no_short)]
|
||||||
list_all: bool,
|
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> {
|
fn list(all: bool) -> Result<(), Error> {
|
||||||
@@ -141,7 +215,7 @@ fn main() -> Result<(), Error> {
|
|||||||
} else if opts.generate {
|
} else if opts.generate {
|
||||||
todo!()
|
todo!()
|
||||||
} else if opts.identity {
|
} else if opts.identity {
|
||||||
todo!()
|
identity(opts)
|
||||||
} else if opts.list {
|
} else if opts.list {
|
||||||
list(false)
|
list(false)
|
||||||
} else if opts.list_all {
|
} else if opts.list_all {
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
use bech32::ToBase32;
|
use bech32::ToBase32;
|
||||||
use elliptic_curve::sec1::EncodedPoint;
|
use elliptic_curve::sec1::EncodedPoint;
|
||||||
use p256::NistP256;
|
use p256::NistP256;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::convert::TryInto;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use crate::RECIPIENT_PREFIX;
|
use crate::RECIPIENT_PREFIX;
|
||||||
|
|
||||||
|
pub(crate) const TAG_BYTES: usize = 4;
|
||||||
|
|
||||||
/// Wrapper around a compressed secp256r1 curve point.
|
/// Wrapper around a compressed secp256r1 curve point.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Recipient(EncodedPoint<NistP256>);
|
pub struct Recipient(EncodedPoint<NistP256>);
|
||||||
@@ -43,4 +47,9 @@ impl Recipient {
|
|||||||
pub(crate) fn as_bytes(&self) -> &[u8] {
|
pub(crate) fn as_bytes(&self) -> &[u8] {
|
||||||
self.0.as_bytes()
|
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,
|
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];
|
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