17 Commits

Author SHA1 Message Date
str4d 51910edfab Merge pull request #41 from str4d/release-0.2.0
CI checks / Test on linux (push) Has been cancelled
CI checks / Test on macos (push) Has been cancelled
CI checks / Test on windows (push) Has been cancelled
CI checks / Clippy (1.51.0) (push) Has been cancelled
CI checks / Clippy (nightly) (push) Has been cancelled
CI checks / Code coverage (push) Has been cancelled
CI checks / Intra-doc links (push) Has been cancelled
CI checks / Rustfmt (push) Has been cancelled
Publish release binaries / Publish for macos (push) Has been cancelled
Publish release binaries / Publish for linux (push) Has been cancelled
Publish release binaries / Publish for windows (push) Has been cancelled
Publish release binaries / Debian linux (push) Has been cancelled
Release 0.2.0
2021-11-22 02:40:05 +00:00
Jack Grigg 4f30e2e6f6 v0.2.0 2021-11-22 02:27:14 +00:00
str4d 37f1a07b60 Merge pull request #38 from str4d/update-deps
Update dependencies
2021-11-21 17:38:06 +00:00
Jack Grigg 822a10f8f6 yubikey 0.5 2021-11-21 15:51:54 +00:00
Jack Grigg b486276421 cargo update 2021-11-21 11:38:18 +00:00
Jack Grigg c7ad7a671b Add rust-toolchain file with MSRV 2021-11-21 10:54:07 +00:00
Jack Grigg f2237ed2a7 yubikey 0.4 2021-10-18 21:11:42 +01:00
Jack Grigg 399f0b4c11 Rename crate::yubikey to crate::key
So that it doesn't conflict with the renamed `yubikey` crate.
2021-10-18 21:07:23 +01:00
Jack Grigg 22dfc3ee89 env_logger 0.9 2021-10-18 20:42:10 +01:00
Jack Grigg 72d5682454 console 0.15, dialoguer 0.9 2021-10-18 20:40:11 +01:00
Jack Grigg 77bd7aa3a3 age-plugin 0.2 2021-10-18 20:37:28 +01:00
Jack Grigg 5c8a7cced8 cargo update 2021-10-18 20:33:33 +01:00
str4d 6042d5266f Merge pull request #35 from str4d/ux-improvements
UX improvements
2021-08-20 16:30:46 +01:00
Jack Grigg f5f140d172 Fix various clippy lints 2021-08-20 16:22:22 +01:00
Jack Grigg 2c90195f99 Check PIN policy before requesting PIN
Closes str4d/age-plugin-yubikey#34.
2021-08-20 15:11:39 +01:00
Jack Grigg 30f4d00902 Move verify_pin after Stub::connect
If all we want is to determine the recipient, we don't need to verify
the PIN.

Closes str4d/age-plugin-yubikey#30.
2021-08-20 15:08:14 +01:00
Jack Grigg 7f43d15942 Use CLI error type to render errors from yubikey.verify_pin()
This ensures that the attempts-before-blocked counter is displayed to
users during the plugin protocol.
2021-08-20 13:18:11 +01:00
12 changed files with 435 additions and 497 deletions
+22
View File
@@ -0,0 +1,22 @@
# Changelog
All notable changes to this crate will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to Rust's notion of
[Semantic Versioning](https://semver.org/spec/v2.0.0.html). All versions prior
to 1.0.0 are beta releases.
## [Unreleased]
## [0.2.0] - 2021-11-22
### Fixed
- Attempts-before-blocked counter is now returned as part of the invalid PIN
error string.
- PIN is no longer requested when fetching the recipient for a slot, or when
decrypting with a slot that has a PIN policy of Never.
- Migrated to `yubikey 0.5` to fix `cargo install age-plugin-yubikey` error
(caused by the `yubikey-piv` crate being yanked after it was renamed).
## [0.1.0] - 2021-05-02
Initial beta release.
Generated
+273 -390
View File
File diff suppressed because it is too large Load Diff
+10 -11
View File
@@ -1,7 +1,7 @@
[package] [package]
name = "age-plugin-yubikey" name = "age-plugin-yubikey"
description = "[BETA] YubiKey plugin for age clients" description = "[BETA] YubiKey plugin for age clients"
version = "0.1.0" version = "0.2.0"
authors = ["Jack Grigg <thestr4d@gmail.com>"] authors = ["Jack Grigg <thestr4d@gmail.com>"]
repository = "https://github.com/str4d/age-plugin-yubikey" repository = "https://github.com/str4d/age-plugin-yubikey"
readme = "README.md" readme = "README.md"
@@ -22,25 +22,24 @@ assets = [
] ]
[dependencies] [dependencies]
age-core = "0.6" age-core = "0.7"
age-plugin = "0.1" age-plugin = "0.2"
base64 = "0.13" base64 = "0.13"
bech32 = "0.8" bech32 = "0.8"
console = "0.14" console = { version = "0.15", default-features = false }
dialoguer = "0.8" dialoguer = { version = "0.9", default-features = false, features = ["password"] }
env_logger = "0.8" env_logger = "0.9"
gumdrop = "0.8" gumdrop = "0.8"
hex = "0.4" hex = "0.4"
log = "0.4" log = "0.4"
p256 = { version = "0.7", features = ["ecdh"] } p256 = { version = "0.9", features = ["ecdh"] }
pcsc = "2.4" pcsc = "2.4"
rand = "0.7" rand = "0.8"
secrecy = "0.7"
sha2 = "0.9" sha2 = "0.9"
which = "4.1" which = "4.1"
x509 = "0.2" x509 = "0.2"
x509-parser = "0.9" x509-parser = "0.12"
yubikey-piv = { version = "0.3", features = ["untested"] } yubikey = { version = "0.5", features = ["untested"] }
[dev-dependencies] [dev-dependencies]
flate2 = "1" flate2 = "1"
+1
View File
@@ -0,0 +1 @@
1.51.0
+5 -6
View File
@@ -1,17 +1,16 @@
use rand::{rngs::OsRng, RngCore}; use rand::{rngs::OsRng, RngCore};
use x509::RelativeDistinguishedName; use x509::RelativeDistinguishedName;
use yubikey_piv::{ use yubikey::{
certificate::{Certificate, PublicKeyInfo}, certificate::{Certificate, PublicKeyInfo},
key::{generate as yubikey_generate, AlgorithmId, RetiredSlotId, SlotId}, piv::{generate as yubikey_generate, AlgorithmId, RetiredSlotId, SlotId},
policy::{PinPolicy, TouchPolicy}, Key, PinPolicy, TouchPolicy, YubiKey,
Key, YubiKey,
}; };
use crate::{ use crate::{
error::Error, error::Error,
key::{self, Stub},
p256::Recipient, p256::Recipient,
util::{Metadata, POLICY_EXTENSION_OID}, util::{Metadata, POLICY_EXTENSION_OID},
yubikey::{self, Stub},
BINARY_NAME, USABLE_SLOTS, BINARY_NAME, USABLE_SLOTS,
}; };
@@ -90,7 +89,7 @@ impl IdentityBuilder {
// No need to ask for users to enter their PIN if the PIN policy requires it, // No need to ask for users to enter their PIN if the PIN policy requires it,
// because here we _always_ require them to enter their PIN in order to access the // because here we _always_ require them to enter their PIN in order to access the
// protected management key (which is necessary in order to generate identities). // protected management key (which is necessary in order to generate identities).
yubikey::manage(yubikey)?; key::manage(yubikey)?;
if let TouchPolicy::Never = touch_policy { if let TouchPolicy::Never = touch_policy {
// No need to touch YubiKey // No need to touch YubiKey
+6 -6
View File
@@ -1,6 +1,6 @@
use std::fmt; use std::fmt;
use std::io; use std::io;
use yubikey_piv::{key::RetiredSlotId, Serial}; use yubikey::{piv::RetiredSlotId, Serial};
use crate::util::slot_to_ui; use crate::util::slot_to_ui;
@@ -21,7 +21,7 @@ pub enum Error {
SlotIsNotEmpty(RetiredSlotId), SlotIsNotEmpty(RetiredSlotId),
TimedOut, TimedOut,
UseListForSingleSlot, UseListForSingleSlot,
YubiKey(yubikey_piv::Error), YubiKey(yubikey::Error),
} }
impl From<io::Error> for Error { impl From<io::Error> for Error {
@@ -30,8 +30,8 @@ impl From<io::Error> for Error {
} }
} }
impl From<yubikey_piv::error::Error> for Error { impl From<yubikey::Error> for Error {
fn from(e: yubikey_piv::error::Error) -> Self { fn from(e: yubikey::Error) -> Self {
Error::YubiKey(e) Error::YubiKey(e)
} }
} }
@@ -100,10 +100,10 @@ impl fmt::Debug for Error {
writeln!(f, "Use --list to print the recipient for a single slot.")? writeln!(f, "Use --list to print the recipient for a single slot.")?
} }
Error::YubiKey(e) => match e { Error::YubiKey(e) => match e {
yubikey_piv::error::Error::NotFound => { yubikey::Error::NotFound => {
writeln!(f, "Please insert the YubiKey you want to set up")? writeln!(f, "Please insert the YubiKey you want to set up")?
} }
yubikey_piv::error::Error::WrongPin { tries } => writeln!( yubikey::Error::WrongPin { tries } => writeln!(
f, f,
"Invalid PIN ({} tries remaining before it is blocked)", "Invalid PIN ({} tries remaining before it is blocked)",
tries tries
+2 -2
View File
@@ -1,10 +1,10 @@
use age_core::{ use age_core::{
format::{FileKey, Stanza}, format::{FileKey, Stanza},
primitives::{aead_encrypt, hkdf}, primitives::{aead_encrypt, hkdf},
secrecy::ExposeSecret,
}; };
use p256::{ecdh::EphemeralSecret, elliptic_curve::sec1::ToEncodedPoint}; use p256::{ecdh::EphemeralSecret, elliptic_curve::sec1::ToEncodedPoint};
use rand::rngs::OsRng; use rand::rngs::OsRng;
use secrecy::ExposeSecret;
use std::convert::TryInto; use std::convert::TryInto;
use crate::{p256::Recipient, STANZA_TAG}; use crate::{p256::Recipient, STANZA_TAG};
@@ -106,7 +106,7 @@ impl RecipientLine {
let epk = esk.public_key(); let epk = esk.public_key();
let epk_bytes = EphemeralKeyBytes::from_public_key(&epk); let epk_bytes = EphemeralKeyBytes::from_public_key(&epk);
let shared_secret = esk.diffie_hellman(&pk.public_key()); let shared_secret = esk.diffie_hellman(pk.public_key());
let mut salt = vec![]; let mut salt = vec![];
salt.extend_from_slice(epk_bytes.as_bytes()); salt.extend_from_slice(epk_bytes.as_bytes());
+69 -36
View File
@@ -3,30 +3,30 @@
use age_core::{ use age_core::{
format::{FileKey, FILE_KEY_BYTES}, format::{FileKey, FILE_KEY_BYTES},
primitives::{aead_decrypt, hkdf}, primitives::{aead_decrypt, hkdf},
secrecy::ExposeSecret,
}; };
use age_plugin::{identity, Callbacks}; use age_plugin::{identity, Callbacks};
use bech32::{ToBase32, Variant}; use bech32::{ToBase32, Variant};
use dialoguer::Password; use dialoguer::Password;
use log::warn; use log::warn;
use secrecy::ExposeSecret;
use std::convert::TryInto; use std::convert::TryInto;
use std::fmt; use std::fmt;
use std::io; use std::io;
use std::iter; use std::iter;
use std::thread::sleep; use std::thread::sleep;
use std::time::{Duration, SystemTime}; use std::time::{Duration, SystemTime};
use yubikey_piv::{ use yubikey::{
certificate::{Certificate, PublicKeyInfo}, certificate::{Certificate, PublicKeyInfo},
key::{decrypt_data, AlgorithmId, RetiredSlotId, SlotId}, piv::{decrypt_data, AlgorithmId, RetiredSlotId, SlotId},
readers::Reader, reader::{Context, Reader},
yubikey::Serial, MgmKey, PinPolicy, Serial, YubiKey,
MgmKey, Readers, YubiKey,
}; };
use crate::{ use crate::{
error::Error, error::Error,
format::{RecipientLine, STANZA_KEY_LABEL}, format::{RecipientLine, STANZA_KEY_LABEL},
p256::{Recipient, TAG_BYTES}, p256::{Recipient, TAG_BYTES},
util::Metadata,
IDENTITY_PREFIX, IDENTITY_PREFIX,
}; };
@@ -54,11 +54,11 @@ pub(crate) fn filter_connected(reader: &Reader) -> bool {
} }
} }
pub(crate) fn wait_for_readers() -> Result<Readers, Error> { pub(crate) fn wait_for_readers() -> Result<Context, Error> {
// Start a 15-second timer waiting for a YubiKey to be inserted (if necessary). // Start a 15-second timer waiting for a YubiKey to be inserted (if necessary).
let start = SystemTime::now(); let start = SystemTime::now();
loop { loop {
let mut readers = Readers::open()?; let mut readers = Context::open()?;
if readers.iter()?.any(is_connected) { if readers.iter()?.any(is_connected) {
break Ok(readers); break Ok(readers);
} }
@@ -71,7 +71,7 @@ pub(crate) fn wait_for_readers() -> Result<Readers, Error> {
} }
pub(crate) fn open(serial: Option<Serial>) -> Result<YubiKey, Error> { pub(crate) fn open(serial: Option<Serial>) -> Result<YubiKey, Error> {
if !Readers::open()?.iter()?.any(is_connected) { if !Context::open()?.iter()?.any(is_connected) {
if let Some(serial) = serial { if let Some(serial) = serial {
eprintln!("⏳ Please insert the YubiKey with serial {}.", serial); eprintln!("⏳ Please insert the YubiKey with serial {}.", serial);
} else { } else {
@@ -155,7 +155,7 @@ pub(crate) fn manage(yubikey: &mut YubiKey) -> Result<(), Error> {
.map_err(|_| Error::CustomManagementKey)?; .map_err(|_| Error::CustomManagementKey)?;
// Migrate to a PIN-protected management key. // Migrate to a PIN-protected management key.
let mgm_key = MgmKey::generate()?; let mgm_key = MgmKey::generate();
eprintln!(); eprintln!();
eprintln!("✨ Your YubiKey is using the default management key."); eprintln!("✨ Your YubiKey is using the default management key.");
eprintln!("✨ We'll migrate it to a PIN-protected management key."); eprintln!("✨ We'll migrate it to a PIN-protected management key.");
@@ -245,7 +245,7 @@ impl Stub {
) -> io::Result<Result<Connection, identity::Error>> { ) -> io::Result<Result<Connection, identity::Error>> {
let mut yubikey = match YubiKey::open_by_serial(self.serial) { let mut yubikey = match YubiKey::open_by_serial(self.serial) {
Ok(yk) => yk, Ok(yk) => yk,
Err(yubikey_piv::Error::NotFound) => { Err(yubikey::Error::NotFound) => {
if callbacks if callbacks
.message(&format!( .message(&format!(
"Please insert YubiKey with serial {}", "Please insert YubiKey with serial {}",
@@ -264,7 +264,7 @@ impl Stub {
loop { loop {
match YubiKey::open_by_serial(self.serial) { match YubiKey::open_by_serial(self.serial) {
Ok(yubikey) => break yubikey, Ok(yubikey) => break yubikey,
Err(yubikey_piv::Error::NotFound) => (), Err(yubikey::Error::NotFound) => (),
Err(_) => { Err(_) => {
return Ok(Err(identity::Error::Identity { return Ok(Err(identity::Error::Identity {
index: self.identity_index, index: self.identity_index,
@@ -299,12 +299,12 @@ impl Stub {
}; };
// Read the pubkey from the YubiKey slot and check it still matches. // Read the pubkey from the YubiKey slot and check it still matches.
let pk = match Certificate::read(&mut yubikey, SlotId::Retired(self.slot)) let (cert, pk) = match Certificate::read(&mut yubikey, SlotId::Retired(self.slot))
.ok() .ok()
.and_then(|cert| match cert.subject_pki() { .and_then(|cert| match cert.subject_pki() {
PublicKeyInfo::EcP256(pubkey) => { PublicKeyInfo::EcP256(pubkey) => Recipient::from_encoded(pubkey)
Recipient::from_encoded(pubkey).filter(|pk| pk.tag() == self.tag) .filter(|pk| pk.tag() == self.tag)
} .map(|pk| (cert, pk)),
_ => None, _ => None,
}) { }) {
Some(pk) => pk, Some(pk) => pk,
@@ -316,39 +316,24 @@ impl Stub {
} }
}; };
let pin = match callbacks.request_secret(&format!(
"Enter PIN for YubiKey with serial {}",
self.serial
))? {
Ok(pin) => pin,
Err(_) => {
return Ok(Err(identity::Error::Identity {
index: self.identity_index,
message: format!("A PIN is required for YubiKey with serial {}", self.serial),
}))
}
};
if yubikey.verify_pin(pin.expose_secret().as_bytes()).is_err() {
return Ok(Err(identity::Error::Identity {
index: self.identity_index,
message: "Invalid YubiKey PIN".to_owned(),
}));
}
Ok(Ok(Connection { Ok(Ok(Connection {
yubikey, yubikey,
cert,
pk, pk,
slot: self.slot, slot: self.slot,
tag: self.tag, tag: self.tag,
identity_index: self.identity_index,
})) }))
} }
} }
pub(crate) struct Connection { pub(crate) struct Connection {
yubikey: YubiKey, yubikey: YubiKey,
cert: Certificate,
pk: Recipient, pk: Recipient,
slot: RetiredSlotId, slot: RetiredSlotId,
tag: [u8; 4], tag: [u8; 4],
identity_index: usize,
} }
impl Connection { impl Connection {
@@ -356,6 +341,54 @@ impl Connection {
&self.pk &self.pk
} }
pub(crate) fn request_pin_if_necessary<E>(
&mut self,
callbacks: &mut dyn Callbacks<E>,
) -> io::Result<Result<(), identity::Error>> {
// Check if we can skip requesting a PIN.
let (_, cert) = x509_parser::parse_x509_certificate(self.cert.as_ref()).unwrap();
match Metadata::extract(&mut self.yubikey, self.slot, &cert, true) {
Some(metadata) => {
if let Some(PinPolicy::Never) = metadata.pin_policy {
return Ok(Ok(()));
}
}
None => {
return Ok(Err(identity::Error::Identity {
index: self.identity_index,
message: "Certificate for YubiKey identity contains an invalid PIN policy"
.to_string(),
}))
}
}
// The policy requires a PIN, so request it.
// Note that we can't distinguish between PinPolicy::Once and PinPolicy::Always
// because this plugin is ephemeral, so we always request the PIN.
let pin = match callbacks.request_secret(&format!(
"Enter PIN for YubiKey with serial {}",
self.yubikey.serial()
))? {
Ok(pin) => pin,
Err(_) => {
return Ok(Err(identity::Error::Identity {
index: self.identity_index,
message: format!(
"A PIN is required for YubiKey with serial {}",
self.yubikey.serial()
),
}))
}
};
if let Err(e) = self.yubikey.verify_pin(pin.expose_secret().as_bytes()) {
return Ok(Err(identity::Error::Identity {
index: self.identity_index,
message: format!("{:?}", Error::YubiKey(e)),
}));
}
Ok(Ok(()))
}
pub(crate) fn unwrap_file_key(&mut self, line: &RecipientLine) -> Result<FileKey, ()> { pub(crate) fn unwrap_file_key(&mut self, line: &RecipientLine) -> Result<FileKey, ()> {
assert_eq!(self.tag, line.tag); assert_eq!(self.tag, line.tag);
@@ -390,7 +423,7 @@ impl Connection {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use yubikey_piv::{key::RetiredSlotId, Serial}; use yubikey::{piv::RetiredSlotId, Serial};
use super::Stub; use super::Stub;
+18 -18
View File
@@ -5,20 +5,20 @@ use std::io::{self, Write};
use age_plugin::run_state_machine; use age_plugin::run_state_machine;
use dialoguer::{Confirm, Input, Select}; use dialoguer::{Confirm, Input, Select};
use gumdrop::Options; use gumdrop::Options;
use yubikey_piv::{ use yubikey::{
certificate::PublicKeyInfo, certificate::PublicKeyInfo,
key::{RetiredSlotId, SlotId}, piv::{RetiredSlotId, SlotId},
policy::{PinPolicy, TouchPolicy}, reader::Context,
Key, Readers, Serial, Key, PinPolicy, Serial, TouchPolicy,
}; };
mod builder; mod builder;
mod error; mod error;
mod format; mod format;
mod key;
mod p256; mod p256;
mod plugin; mod plugin;
mod util; mod util;
mod yubikey;
use error::Error; use error::Error;
@@ -148,7 +148,7 @@ impl TryFrom<PluginOptions> for PluginFlags {
} }
fn generate(flags: PluginFlags) -> Result<(), Error> { fn generate(flags: PluginFlags) -> Result<(), Error> {
let mut yubikey = yubikey::open(flags.serial)?; let mut yubikey = key::open(flags.serial)?;
let (stub, recipient, metadata) = builder::IdentityBuilder::new(flags.slot) let (stub, recipient, metadata) = builder::IdentityBuilder::new(flags.slot)
.with_name(flags.name) .with_name(flags.name)
@@ -165,9 +165,9 @@ fn generate(flags: PluginFlags) -> Result<(), Error> {
fn print_single( fn print_single(
serial: Option<Serial>, serial: Option<Serial>,
slot: RetiredSlotId, slot: RetiredSlotId,
printer: impl Fn(yubikey::Stub, p256::Recipient, util::Metadata), printer: impl Fn(key::Stub, p256::Recipient, util::Metadata),
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut yubikey = yubikey::open(serial)?; let mut yubikey = key::open(serial)?;
let mut keys = Key::list(&mut yubikey)?.into_iter().filter_map(|key| { let mut keys = Key::list(&mut yubikey)?.into_iter().filter_map(|key| {
// - We only use the retired slots. // - We only use the retired slots.
@@ -184,7 +184,7 @@ fn print_single(
.find(|(_, s, _)| s == &slot) .find(|(_, s, _)| s == &slot)
.ok_or(Error::SlotHasNoIdentity(slot))?; .ok_or(Error::SlotHasNoIdentity(slot))?;
let stub = yubikey::Stub::new(yubikey.serial(), slot, &recipient); let stub = key::Stub::new(yubikey.serial(), slot, &recipient);
let metadata = x509_parser::parse_x509_certificate(key.certificate().as_ref()) let metadata = x509_parser::parse_x509_certificate(key.certificate().as_ref())
.ok() .ok()
.and_then(|(_, cert)| util::Metadata::extract(&mut yubikey, slot, &cert, true)) .and_then(|(_, cert)| util::Metadata::extract(&mut yubikey, slot, &cert, true))
@@ -199,12 +199,12 @@ fn print_multiple(
kind: &str, kind: &str,
serial: Option<Serial>, serial: Option<Serial>,
all: bool, all: bool,
printer: impl Fn(yubikey::Stub, p256::Recipient, util::Metadata), printer: impl Fn(key::Stub, p256::Recipient, util::Metadata),
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut readers = Readers::open()?; let mut readers = Context::open()?;
let mut printed = 0; let mut printed = 0;
for reader in readers.iter()?.filter(yubikey::filter_connected) { for reader in readers.iter()?.filter(key::filter_connected) {
let mut yubikey = reader.open()?; let mut yubikey = reader.open()?;
if let Some(serial) = serial { if let Some(serial) = serial {
if yubikey.serial() != serial { if yubikey.serial() != serial {
@@ -228,7 +228,7 @@ fn print_multiple(
_ => continue, _ => continue,
}; };
let stub = yubikey::Stub::new(yubikey.serial(), slot, &recipient); let stub = key::Stub::new(yubikey.serial(), slot, &recipient);
let metadata = match x509_parser::parse_x509_certificate(key.certificate().as_ref()) let metadata = match x509_parser::parse_x509_certificate(key.certificate().as_ref())
.ok() .ok()
.and_then(|(_, cert)| util::Metadata::extract(&mut yubikey, slot, &cert, all)) .and_then(|(_, cert)| util::Metadata::extract(&mut yubikey, slot, &cert, all))
@@ -257,7 +257,7 @@ fn print_details(
kind: &str, kind: &str,
flags: PluginFlags, flags: PluginFlags,
all: bool, all: bool,
printer: impl Fn(yubikey::Stub, p256::Recipient, util::Metadata), printer: impl Fn(key::Stub, p256::Recipient, util::Metadata),
) -> Result<(), Error> { ) -> Result<(), Error> {
if let Some(slot) = flags.slot { if let Some(slot) = flags.slot {
print_single(flags.serial, slot, printer) print_single(flags.serial, slot, printer)
@@ -350,13 +350,13 @@ fn main() -> Result<(), Error> {
eprintln!("make your choice, or press [Esc] or [q] to quit."); eprintln!("make your choice, or press [Esc] or [q] to quit.");
eprintln!(); eprintln!();
if !Readers::open()?.iter()?.any(yubikey::is_connected) { if !Context::open()?.iter()?.any(key::is_connected) {
eprintln!("⏳ Please insert the YubiKey you want to set up."); eprintln!("⏳ Please insert the YubiKey you want to set up.");
}; };
let mut readers = yubikey::wait_for_readers()?; let mut readers = key::wait_for_readers()?;
// Filter out readers we can't connect to. // Filter out readers we can't connect to.
let readers_list: Vec<_> = readers.iter()?.filter(yubikey::filter_connected).collect(); let readers_list: Vec<_> = readers.iter()?.filter(key::filter_connected).collect();
let reader_names = readers_list let reader_names = readers_list
.iter() .iter()
@@ -447,7 +447,7 @@ fn main() -> Result<(), Error> {
.with_prompt(&format!("Use existing identity in slot {}?", slot_index)) .with_prompt(&format!("Use existing identity in slot {}?", slot_index))
.interact()? .interact()?
{ {
let stub = yubikey::Stub::new(yubikey.serial(), slot, &recipient); let stub = key::Stub::new(yubikey.serial(), slot, &recipient);
let (_, cert) = let (_, cert) =
x509_parser::parse_x509_certificate(key.certificate().as_ref()).unwrap(); x509_parser::parse_x509_certificate(key.certificate().as_ref()).unwrap();
let metadata = let metadata =
+1 -1
View File
@@ -48,7 +48,7 @@ impl Recipient {
/// This accepts both compressed (as used by the plugin) and uncompressed (as used in /// This accepts both compressed (as used by the plugin) and uncompressed (as used in
/// the YubiKey certificate) encodings. /// the YubiKey certificate) encodings.
pub(crate) fn from_encoded(encoded: &p256::EncodedPoint) -> Option<Self> { pub(crate) fn from_encoded(encoded: &p256::EncodedPoint) -> Option<Self> {
p256::PublicKey::from_encoded_point(&encoded).map(Recipient) p256::PublicKey::from_encoded_point(encoded).map(Recipient)
} }
/// Returns the compressed SEC-1 encoding of this recipient. /// Returns the compressed SEC-1 encoding of this recipient.
+19 -17
View File
@@ -7,12 +7,12 @@ use age_plugin::{
use std::collections::HashMap; use std::collections::HashMap;
use std::io; use std::io;
use crate::{format, p256::Recipient, yubikey, PLUGIN_NAME}; use crate::{format, key, p256::Recipient, PLUGIN_NAME};
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub(crate) struct RecipientPlugin { pub(crate) struct RecipientPlugin {
recipients: Vec<Recipient>, recipients: Vec<Recipient>,
yubikeys: Vec<yubikey::Stub>, yubikeys: Vec<key::Stub>,
} }
impl RecipientPluginV1 for RecipientPlugin { impl RecipientPluginV1 for RecipientPlugin {
@@ -23,7 +23,7 @@ impl RecipientPluginV1 for RecipientPlugin {
bytes: &[u8], bytes: &[u8],
) -> Result<(), recipient::Error> { ) -> Result<(), recipient::Error> {
if let Some(pk) = if plugin_name == PLUGIN_NAME { if let Some(pk) = if plugin_name == PLUGIN_NAME {
Recipient::from_bytes(&bytes) Recipient::from_bytes(bytes)
} else { } else {
None None
} { } {
@@ -44,7 +44,7 @@ impl RecipientPluginV1 for RecipientPlugin {
bytes: &[u8], bytes: &[u8],
) -> Result<(), recipient::Error> { ) -> Result<(), recipient::Error> {
if let Some(stub) = if plugin_name == PLUGIN_NAME { if let Some(stub) = if plugin_name == PLUGIN_NAME {
yubikey::Stub::from_bytes(&bytes, index) key::Stub::from_bytes(bytes, index)
} else { } else {
None None
} { } {
@@ -88,7 +88,7 @@ impl RecipientPluginV1 for RecipientPlugin {
self.recipients self.recipients
.iter() .iter()
.chain(yk_recipients.iter()) .chain(yk_recipients.iter())
.map(|pk| format::RecipientLine::wrap_file_key(&file_key, &pk).into()) .map(|pk| format::RecipientLine::wrap_file_key(&file_key, pk).into())
.collect() .collect()
}) })
.collect()) .collect())
@@ -100,7 +100,7 @@ impl RecipientPluginV1 for RecipientPlugin {
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub(crate) struct IdentityPlugin { pub(crate) struct IdentityPlugin {
yubikeys: Vec<yubikey::Stub>, yubikeys: Vec<key::Stub>,
} }
impl IdentityPluginV1 for IdentityPlugin { impl IdentityPluginV1 for IdentityPlugin {
@@ -111,7 +111,7 @@ impl IdentityPluginV1 for IdentityPlugin {
bytes: &[u8], bytes: &[u8],
) -> Result<(), identity::Error> { ) -> Result<(), identity::Error> {
if let Some(stub) = if plugin_name == PLUGIN_NAME { if let Some(stub) = if plugin_name == PLUGIN_NAME {
yubikey::Stub::from_bytes(&bytes, index) key::Stub::from_bytes(bytes, index)
} else { } else {
None None
} { } {
@@ -133,19 +133,16 @@ impl IdentityPluginV1 for IdentityPlugin {
let mut file_keys = HashMap::with_capacity(files.len()); let mut file_keys = HashMap::with_capacity(files.len());
// Filter to files / stanzas for which we have matching YubiKeys // Filter to files / stanzas for which we have matching YubiKeys
let mut candidate_stanzas: Vec<( let mut candidate_stanzas: Vec<(&key::Stub, HashMap<usize, Vec<format::RecipientLine>>)> =
&yubikey::Stub, self.yubikeys
HashMap<usize, Vec<format::RecipientLine>>, .iter()
)> = self .map(|stub| (stub, HashMap::new()))
.yubikeys .collect();
.iter()
.map(|stub| (stub, HashMap::new()))
.collect();
for (file, stanzas) in files.iter().enumerate() { for (file, stanzas) in files.iter().enumerate() {
for (stanza_index, stanza) in stanzas.iter().enumerate() { for (stanza_index, stanza) in stanzas.iter().enumerate() {
match ( match (
format::RecipientLine::from_stanza(&stanza).map(|res| { format::RecipientLine::from_stanza(stanza).map(|res| {
res.map_err(|_| identity::Error::Stanza { res.map_err(|_| identity::Error::Stanza {
file_index: file, file_index: file,
stanza_index, stanza_index,
@@ -213,6 +210,11 @@ impl IdentityPluginV1 for IdentityPlugin {
} }
}; };
if let Err(e) = conn.request_pin_if_necessary(&mut callbacks)? {
callbacks.error(e)?.unwrap();
continue;
}
for (&file_index, stanzas) in files { for (&file_index, stanzas) in files {
if file_keys.contains_key(&file_index) { if file_keys.contains_key(&file_index) {
// We decrypted this file with an earlier YubiKey. // We decrypted this file with an earlier YubiKey.
@@ -220,7 +222,7 @@ impl IdentityPluginV1 for IdentityPlugin {
} }
for (stanza_index, line) in stanzas.iter().enumerate() { for (stanza_index, line) in stanzas.iter().enumerate() {
match conn.unwrap_file_key(&line) { match conn.unwrap_file_key(line) {
Ok(file_key) => { Ok(file_key) => {
// We've managed to decrypt this file! // We've managed to decrypt this file!
file_keys.entry(file_index).or_insert(Ok(file_key)); file_keys.entry(file_index).or_insert(Ok(file_key));
+9 -10
View File
@@ -1,13 +1,12 @@
use std::fmt; use std::fmt;
use x509_parser::{certificate::X509Certificate, der_parser::oid::Oid}; use x509_parser::{certificate::X509Certificate, der_parser::oid::Oid};
use yubikey_piv::{ use yubikey::{
key::{RetiredSlotId, SlotId}, piv::{RetiredSlotId, SlotId},
policy::{PinPolicy, TouchPolicy}, PinPolicy, Serial, TouchPolicy, YubiKey,
Serial, YubiKey,
}; };
use crate::{error::Error, p256::Recipient, yubikey::Stub, BINARY_NAME, USABLE_SLOTS}; use crate::{error::Error, key::Stub, p256::Recipient, BINARY_NAME, USABLE_SLOTS};
pub(crate) const POLICY_EXTENSION_OID: &[u64] = &[1, 3, 6, 1, 4, 1, 41482, 3, 8]; pub(crate) const POLICY_EXTENSION_OID: &[u64] = &[1, 3, 6, 1, 4, 1, 41482, 3, 8];
@@ -96,7 +95,7 @@ pub(crate) struct Metadata {
slot: RetiredSlotId, slot: RetiredSlotId,
name: String, name: String,
created: String, created: String,
pin_policy: Option<PinPolicy>, pub(crate) pin_policy: Option<PinPolicy>,
touch_policy: Option<TouchPolicy>, touch_policy: Option<TouchPolicy>,
} }
@@ -111,8 +110,8 @@ impl Metadata {
// using the same certificate extension as PIV attestations. // using the same certificate extension as PIV attestations.
// https://developers.yubico.com/PIV/Introduction/PIV_attestation.html // https://developers.yubico.com/PIV/Introduction/PIV_attestation.html
let policies = |c: &X509Certificate| { let policies = |c: &X509Certificate| {
c.extensions() c.tbs_certificate
.get(&Oid::from(POLICY_EXTENSION_OID).unwrap()) .find_extension(&Oid::from(POLICY_EXTENSION_OID).unwrap())
// If the encoded extension doesn't have 2 bytes, we assume it is invalid. // If the encoded extension doesn't have 2 bytes, we assume it is invalid.
.filter(|policy| policy.value.len() >= 2) .filter(|policy| policy.value.len() >= 2)
.map(|policy| { .map(|policy| {
@@ -138,13 +137,13 @@ impl Metadata {
extract_name(cert, all) extract_name(cert, all)
.map(|(name, ours)| { .map(|(name, ours)| {
if ours { if ours {
let (pin_policy, touch_policy) = policies(&cert); let (pin_policy, touch_policy) = policies(cert);
(name, pin_policy, touch_policy) (name, pin_policy, touch_policy)
} else { } else {
// We can extract the PIN and touch policies via an attestation. This // We can extract the PIN and touch policies via an attestation. This
// is slow, but the user has asked for all compatible keys, so... // is slow, but the user has asked for all compatible keys, so...
let (pin_policy, touch_policy) = let (pin_policy, touch_policy) =
yubikey_piv::key::attest(yubikey, SlotId::Retired(slot)) yubikey::piv::attest(yubikey, SlotId::Retired(slot))
.ok() .ok()
.and_then(|buf| { .and_then(|buf| {
x509_parser::parse_x509_certificate(&buf) x509_parser::parse_x509_certificate(&buf)