13 Commits

Author SHA1 Message Date
str4d fc2081c216 Merge pull request #98 from str4d/release-0.3.2
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.56.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-arm64 (push) Has been cancelled
Publish release binaries / Publish for macos-x86_64 (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.3.2
2023-01-01 13:53:55 +00:00
Jack Grigg 367a081eea Release 0.3.2 2023-01-01 13:45:21 +00:00
str4d cfb1e5e3d5 Merge pull request #97 from str4d/more-cleanups
More cleanups
2023-01-01 13:44:15 +00:00
Jack Grigg 1dfadc7e27 Clean up key::filter_connected 2023-01-01 13:29:30 +00:00
Jack Grigg fc66d9f6fd Add helper methods for filtering available keys 2023-01-01 13:27:10 +00:00
Jack Grigg d8eb198e97 Move certificate parsing into Metadata::extract 2023-01-01 13:27:10 +00:00
str4d c8f9df1b45 Merge pull request #95 from str4d/94-yubikey-agent-sighup
Extend "sharing violation" logic to send SIGHUP to `yubikey-agent` processes
2023-01-01 13:24:57 +00:00
Jack Grigg 3597d96332 Correctly hunt agents in plugin mode 2023-01-01 13:18:41 +00:00
Jack Grigg 1913838f8e Hunt for yubikey-agent 2023-01-01 12:52:17 +00:00
Jack Grigg 6e47448560 Generalise code for hunting agents that may be holding YubiKeys 2023-01-01 12:52:17 +00:00
str4d 4d4d8cc183 Merge pull request #96 from str4d/refactors-and-cleanups
Refactors and cleanups
2022-12-31 16:41:21 +00:00
Jack Grigg ac7b04a61d Add keyword argument support to fl! and wlnfl! macros 2022-12-31 14:31:25 +00:00
Jack Grigg 493479344c De-duplicate parsing recipients from SubjectPublicKeyInfo 2022-12-31 12:49:44 +00:00
10 changed files with 248 additions and 348 deletions
+9
View File
@@ -8,6 +8,15 @@ to 0.3.0 are beta releases.
## [Unreleased] ## [Unreleased]
## [0.3.2] - 2023-01-01
### Changed
- The "sharing violation" logic now also sends SIGHUP to any `yubikey-agent`
that is running, to have them release any YubiKey locks they are holding.
### Fixed
- The "sharing violation" logic now runs during plugin mode as intended. In the
previous release it only ran during direct `age-plugin-yubikey` usage.
## [0.3.1] - 2022-12-30 ## [0.3.1] - 2022-12-30
### Changed ### Changed
- If a "sharing violation" error is encountered while opening a connection to a - If a "sharing violation" error is encountered while opening a connection to a
Generated
+1 -1
View File
@@ -49,7 +49,7 @@ dependencies = [
[[package]] [[package]]
name = "age-plugin-yubikey" name = "age-plugin-yubikey"
version = "0.3.1" version = "0.3.2"
dependencies = [ dependencies = [
"age-core", "age-core",
"age-plugin", "age-plugin",
+1 -1
View File
@@ -1,7 +1,7 @@
[package] [package]
name = "age-plugin-yubikey" name = "age-plugin-yubikey"
description = "YubiKey plugin for age clients" description = "YubiKey plugin for age clients"
version = "0.3.1" version = "0.3.2"
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"
+2 -8
View File
@@ -1,7 +1,7 @@
use rand::{rngs::OsRng, RngCore}; use rand::{rngs::OsRng, RngCore};
use x509::RelativeDistinguishedName; use x509::RelativeDistinguishedName;
use yubikey::{ use yubikey::{
certificate::{Certificate, PublicKeyInfo}, certificate::Certificate,
piv::{generate as yubikey_generate, AlgorithmId, RetiredSlotId, SlotId}, piv::{generate as yubikey_generate, AlgorithmId, RetiredSlotId, SlotId},
Key, PinPolicy, TouchPolicy, YubiKey, Key, PinPolicy, TouchPolicy, YubiKey,
}; };
@@ -106,12 +106,7 @@ impl IdentityBuilder {
touch_policy, touch_policy,
)?; )?;
let recipient = match &generated { let recipient = Recipient::from_spki(&generated).expect("YubiKey generates a valid pubkey");
PublicKeyInfo::EcP256(pubkey) => {
Recipient::from_encoded(pubkey).expect("YubiKey generates a valid pubkey")
}
_ => unreachable!(),
};
let stub = Stub::new(yubikey.serial(), slot, &recipient); let stub = Stub::new(yubikey.serial(), slot, &recipient);
// Pick a random serial for the new self-signed certificate. // Pick a random serial for the new self-signed certificate.
@@ -139,7 +134,6 @@ impl IdentityBuilder {
)], )],
)?; )?;
let (_, cert) = x509_parser::parse_x509_certificate(cert.as_ref()).unwrap();
let metadata = Metadata::extract(yubikey, slot, &cert, false).unwrap(); let metadata = Metadata::extract(yubikey, slot, &cert, false).unwrap();
Ok(( Ok((
+28 -119
View File
@@ -1,4 +1,3 @@
use i18n_embed_fl::fl;
use std::fmt; use std::fmt;
use std::io; use std::io;
use yubikey::{piv::RetiredSlotId, Serial}; use yubikey::{piv::RetiredSlotId, Serial};
@@ -9,6 +8,9 @@ macro_rules! wlnfl {
($f:ident, $message_id:literal) => { ($f:ident, $message_id:literal) => {
writeln!($f, "{}", $crate::fl!($message_id)) writeln!($f, "{}", $crate::fl!($message_id))
}; };
($f:ident, $message_id:literal, $($kwarg:expr),* $(,)*) => {{
writeln!($f, "{}", $crate::fl!($message_id, $($kwarg,)*))
}};
} }
pub enum Error { pub enum Error {
@@ -52,105 +54,44 @@ impl fmt::Debug for Error {
wlnfl!(f, "err-custom-mgmt-key")?; wlnfl!(f, "err-custom-mgmt-key")?;
let cmd = "ykman piv access change-management-key --protect"; let cmd = "ykman piv access change-management-key --protect";
let url = "https://developers.yubico.com/yubikey-manager/"; let url = "https://developers.yubico.com/yubikey-manager/";
writeln!( wlnfl!(f, "rec-custom-mgmt-key", cmd = cmd, url = url)?;
f,
"{}",
fl!(
crate::LANGUAGE_LOADER,
"rec-custom-mgmt-key",
cmd = cmd,
url = url,
),
)?;
} }
Error::InvalidFlagCommand(flag, command) => writeln!( Error::InvalidFlagCommand(flag, command) => wlnfl!(
f, f,
"{}",
fl!(
crate::LANGUAGE_LOADER,
"err-invalid-flag-command", "err-invalid-flag-command",
flag = flag.as_str(), flag = flag.as_str(),
command = command.as_str(), command = command.as_str(),
),
)?,
Error::InvalidFlagTui(flag) => writeln!(
f,
"{}",
fl!(
crate::LANGUAGE_LOADER,
"err-invalid-flag-tui",
flag = flag.as_str(),
),
)?, )?,
Error::InvalidFlagTui(flag) => wlnfl!(f, "err-invalid-flag-tui", flag = flag.as_str())?,
Error::InvalidPinLength => wlnfl!(f, "err-invalid-pin-length")?, Error::InvalidPinLength => wlnfl!(f, "err-invalid-pin-length")?,
Error::InvalidPinPolicy(s) => writeln!( Error::InvalidPinPolicy(s) => wlnfl!(
f, f,
"{}",
fl!(
crate::LANGUAGE_LOADER,
"err-invalid-pin-policy", "err-invalid-pin-policy",
policy = s.as_str(), policy = s.as_str(),
expected = "always, once, never", expected = "always, once, never",
),
)?, )?,
Error::InvalidSlot(slot) => writeln!( Error::InvalidSlot(slot) => wlnfl!(f, "err-invalid-slot", slot = slot)?,
Error::InvalidTouchPolicy(s) => wlnfl!(
f, f,
"{}",
fl!(crate::LANGUAGE_LOADER, "err-invalid-slot", slot = slot),
)?,
Error::InvalidTouchPolicy(s) => writeln!(
f,
"{}",
fl!(
crate::LANGUAGE_LOADER,
"err-invalid-touch-policy", "err-invalid-touch-policy",
policy = s.as_str(), policy = s.as_str(),
expected = "always, cached, never", expected = "always, cached, never",
),
)?,
Error::Io(e) => writeln!(
f,
"{}",
fl!(crate::LANGUAGE_LOADER, "err-io", err = e.to_string()),
)?, )?,
Error::Io(e) => wlnfl!(f, "err-io", err = e.to_string())?,
Error::MultipleCommands => wlnfl!(f, "err-multiple-commands")?, Error::MultipleCommands => wlnfl!(f, "err-multiple-commands")?,
Error::MultipleYubiKeys => wlnfl!(f, "err-multiple-yubikeys")?, Error::MultipleYubiKeys => wlnfl!(f, "err-multiple-yubikeys")?,
Error::NoEmptySlots(serial) => writeln!( Error::NoEmptySlots(serial) => {
f, wlnfl!(f, "err-no-empty-slots", serial = serial.to_string())?
"{}", }
fl!( Error::NoMatchingSerial(serial) => {
crate::LANGUAGE_LOADER, wlnfl!(f, "err-no-matching-serial", serial = serial.to_string())?
"err-no-empty-slots", }
serial = serial.to_string(), Error::SlotHasNoIdentity(slot) => {
), wlnfl!(f, "err-slot-has-no-identity", slot = slot_to_ui(slot))?
)?, }
Error::NoMatchingSerial(serial) => writeln!( Error::SlotIsNotEmpty(slot) => {
f, wlnfl!(f, "err-slot-is-not-empty", slot = slot_to_ui(slot))?
"{}", }
fl!(
crate::LANGUAGE_LOADER,
"err-no-matching-serial",
serial = serial.to_string(),
),
)?,
Error::SlotHasNoIdentity(slot) => writeln!(
f,
"{}",
fl!(
crate::LANGUAGE_LOADER,
"err-slot-has-no-identity",
slot = slot_to_ui(slot),
),
)?,
Error::SlotIsNotEmpty(slot) => writeln!(
f,
"{}",
fl!(
crate::LANGUAGE_LOADER,
"err-slot-is-not-empty",
slot = slot_to_ui(slot),
),
)?,
Error::TimedOut => wlnfl!(f, "err-timed-out")?, Error::TimedOut => wlnfl!(f, "err-timed-out")?,
Error::UseListForSingleSlot => wlnfl!(f, "err-use-list-for-single")?, Error::UseListForSingleSlot => wlnfl!(f, "err-use-list-for-single")?,
Error::YubiKey(e) => match e { Error::YubiKey(e) => match e {
@@ -161,55 +102,23 @@ impl fmt::Debug for Error {
if cfg!(windows) { if cfg!(windows) {
wlnfl!(f, "err-yk-no-service-win")?; wlnfl!(f, "err-yk-no-service-win")?;
let url = "https://learn.microsoft.com/en-us/windows/security/identity-protection/smart-cards/smart-card-debugging-information#smart-card-service"; let url = "https://learn.microsoft.com/en-us/windows/security/identity-protection/smart-cards/smart-card-debugging-information#smart-card-service";
writeln!( wlnfl!(f, "rec-yk-no-service-win", url = url)?;
f,
"{}",
fl!(crate::LANGUAGE_LOADER, "rec-yk-no-service-win", url = url),
)?;
} else if cfg!(target_os = "macos") { } else if cfg!(target_os = "macos") {
wlnfl!(f, "err-yk-no-service-macos")?; wlnfl!(f, "err-yk-no-service-macos")?;
let url = "https://apple.stackexchange.com/a/438198"; let url = "https://apple.stackexchange.com/a/438198";
writeln!( wlnfl!(f, "rec-yk-no-service-macos", url = url)?;
f,
"{}",
fl!(crate::LANGUAGE_LOADER, "rec-yk-no-service-macos", url = url),
)?;
} else { } else {
wlnfl!(f, "err-yk-no-service-pcscd")?; wlnfl!(f, "err-yk-no-service-pcscd")?;
let apt = "sudo apt-get install pcscd"; let apt = "sudo apt-get install pcscd";
writeln!( wlnfl!(f, "rec-yk-no-service-pcscd", apt = apt)?;
f,
"{}",
fl!(crate::LANGUAGE_LOADER, "rec-yk-no-service-pcscd", apt = apt),
)?;
} }
} }
yubikey::Error::WrongPin { tries } => writeln!( yubikey::Error::WrongPin { tries } => wlnfl!(f, "err-yk-wrong-pin", tries = tries)?,
f,
"{}",
fl!(crate::LANGUAGE_LOADER, "err-yk-wrong-pin", tries = tries),
)?,
e => { e => {
writeln!( wlnfl!(f, "err-yk-general", err = e.to_string())?;
f,
"{}",
fl!(
crate::LANGUAGE_LOADER,
"err-yk-general",
err = e.to_string(),
),
)?;
use std::error::Error; use std::error::Error;
if let Some(inner) = e.source() { if let Some(inner) = e.source() {
writeln!( wlnfl!(f, "err-yk-general-cause", inner_err = inner.to_string())?;
f,
"{}",
fl!(
crate::LANGUAGE_LOADER,
"err-yk-general-cause",
inner_err = inner.to_string(),
),
)?;
} }
} }
}, },
+129 -78
View File
@@ -8,17 +8,17 @@ use age_core::{
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::{debug, warn}; use log::{debug, error, warn};
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, Instant, SystemTime}; use std::time::{Duration, Instant, SystemTime};
use yubikey::{ use yubikey::{
certificate::{Certificate, PublicKeyInfo}, certificate::Certificate,
piv::{decrypt_data, AlgorithmId, RetiredSlotId, SlotId}, piv::{decrypt_data, AlgorithmId, RetiredSlotId, SlotId},
reader::{Context, Reader}, reader::{Context, Reader},
MgmKey, PinPolicy, Serial, TouchPolicy, YubiKey, Key, MgmKey, PinPolicy, Serial, TouchPolicy, YubiKey,
}; };
use crate::{ use crate::{
@@ -39,25 +39,16 @@ pub(crate) fn is_connected(reader: Reader) -> bool {
pub(crate) fn filter_connected(reader: &Reader) -> bool { pub(crate) fn filter_connected(reader: &Reader) -> bool {
match reader.open() { match reader.open() {
Ok(_) => true, Err(yubikey::Error::PcscError {
Err(e) => { inner: Some(pcsc::Error::RemovedCard),
use std::error::Error; }) => {
if let Some(pcsc::Error::RemovedCard) =
e.source().and_then(|inner| inner.downcast_ref())
{
warn!( warn!(
"{}", "{}",
i18n_embed_fl::fl!( fl!("warn-yk-not-connected", yubikey_name = reader.name())
crate::LANGUAGE_LOADER,
"warn-yk-not-connected",
yubikey_name = reader.name(),
)
); );
false false
} else {
true
}
} }
_ => true,
} }
} }
@@ -77,27 +68,27 @@ pub(crate) fn wait_for_readers() -> Result<Context, Error> {
} }
} }
/// Stops `scdaemon` if it is running. /// Looks for agent processes that might be holding exclusive access to a YubiKey, and
/// asks them as nicely as possible to release it.
/// ///
/// Returns `true` if `scdaemon` was running and was successfully interrupted (or killed /// Returns `true` if any known agent was running and was successfully interrupted (or
/// if the platform doesn't support interrupts). /// killed if the platform doesn't support interrupts).
fn stop_scdaemon() -> bool { fn hunt_agents() -> bool {
debug!("Sharing violation encountered, looking for scdaemon processes to stop"); debug!("Sharing violation encountered, looking for agent processes");
use sysinfo::{ use sysinfo::{ProcessExt, ProcessRefreshKind, RefreshKind, Signal, System, SystemExt};
Process, ProcessExt, ProcessRefreshKind, RefreshKind, Signal, System, SystemExt,
};
let mut interrupted = false; let mut interrupted = false;
let sys = let sys =
System::new_with_specifics(RefreshKind::new().with_processes(ProcessRefreshKind::new())); System::new_with_specifics(RefreshKind::new().with_processes(ProcessRefreshKind::new()));
for process in sys for process in sys.processes().values() {
.processes() match process.name() {
.values() "scdaemon" | "scdaemon.exe" => {
.filter(|val: &&Process| ["scdaemon", "scdaemon.exe"].contains(&val.name())) // gpg-agent runs scdaemon to interact with smart cards. The canonical way
{ // to reload it is `gpgconf --reload scdaemon`, which kills and restarts
// the process. We emulate that here with SIGINT (which it listens to).
if process if process
.kill_with(Signal::Interrupt) .kill_with(Signal::Interrupt)
.unwrap_or_else(|| process.kill()) .unwrap_or_else(|| process.kill())
@@ -106,8 +97,25 @@ fn stop_scdaemon() -> bool {
interrupted = true; interrupted = true;
} }
} }
"yubikey-agent" | "yubikey-agent.exe" => {
// yubikey-agent releases all YubiKey locks when it receives a SIGHUP.
match process.kill_with(Signal::Hangup) {
Some(true) => {
debug!("Sent SIGHUP to yubikey-agent (PID {})", process.pid());
interrupted = true;
}
Some(false) => (),
None => debug!(
"Found yubikey-agent (PID {}) but platform doesn't support SIGHUP",
process.pid(),
),
}
}
_ => (),
}
}
// If we did interrupt `scdaemon`, pause briefly to allow it to exit. // If we did interrupt an agent, pause briefly to allow it to finish up.
if interrupted { if interrupted {
sleep(Duration::from_millis(100)); sleep(Duration::from_millis(100));
} }
@@ -121,7 +129,7 @@ fn open_sesame(
op().or_else(|e| match e { op().or_else(|e| match e {
yubikey::Error::PcscError { yubikey::Error::PcscError {
inner: Some(pcsc::Error::SharingViolation), inner: Some(pcsc::Error::SharingViolation),
} if stop_scdaemon() => op(), } if hunt_agents() => op(),
_ => Err(e), _ => Err(e),
}) })
} }
@@ -129,7 +137,7 @@ fn open_sesame(
/// Opens a connection to this reader, returning a `YubiKey` if successful. /// Opens a connection to this reader, returning a `YubiKey` if successful.
/// ///
/// This is equivalent to [`Reader::open`], but additionally handles the presence of /// This is equivalent to [`Reader::open`], but additionally handles the presence of
/// `scdaemon` (which can indefinitely hold exclusive access to a YubiKey). /// agents (which can indefinitely hold exclusive access to a YubiKey).
pub(crate) fn open_connection(reader: &Reader) -> Result<YubiKey, yubikey::Error> { pub(crate) fn open_connection(reader: &Reader) -> Result<YubiKey, yubikey::Error> {
open_sesame(|| reader.open()) open_sesame(|| reader.open())
} }
@@ -137,9 +145,51 @@ pub(crate) fn open_connection(reader: &Reader) -> Result<YubiKey, yubikey::Error
/// Opens a YubiKey with a specific serial number. /// Opens a YubiKey with a specific serial number.
/// ///
/// This is equivalent to [`YubiKey::open_by_serial`], but additionally handles the /// This is equivalent to [`YubiKey::open_by_serial`], but additionally handles the
/// presence of `scdaemon` (which can indefinitely hold exclusive access to a YubiKey). /// presence of agents (which can indefinitely hold exclusive access to a YubiKey).
fn open_by_serial(serial: Serial) -> Result<YubiKey, yubikey::Error> { fn open_by_serial(serial: Serial) -> Result<YubiKey, yubikey::Error> {
open_sesame(|| YubiKey::open_by_serial(serial)) // `YubiKey::open_by_serial` has a bug where it ignores all opening errors, even if
// it potentially could have found a matching YubiKey if not for an error, and thus
// returns `Error::NotFound` if another agent is holding exclusive access to the
// required YubiKey. This gives misleading UX behaviour where age-plugin-yubikey asks
// the user to insert a YubiKey they have already inserted.
//
// For now, we instead implement the correct behaviour manually. Once MSRV has been
// raised to 1.60, we can upstream this into the `yubikey` crate.
open_sesame(|| {
let mut readers = Context::open()?;
let mut open_error = None;
for reader in readers.iter()? {
let yubikey = match reader.open() {
Ok(yk) => yk,
Err(e) => {
// Save the first error we see that indicates we might have been able
// to find a matching YubiKey.
if open_error.is_none() {
if let yubikey::Error::PcscError {
inner: Some(pcsc::Error::SharingViolation),
} = e
{
open_error = Some(e);
}
}
continue;
}
};
if serial == yubikey.serial() {
return Ok(yubikey);
}
}
Err(if let Some(e) = open_error {
e
} else {
error!("no YubiKey detected with serial: {}", serial);
yubikey::Error::NotFound
})
})
} }
pub(crate) fn open(serial: Option<Serial>) -> Result<YubiKey, Error> { pub(crate) fn open(serial: Option<Serial>) -> Result<YubiKey, Error> {
@@ -147,11 +197,7 @@ pub(crate) fn open(serial: Option<Serial>) -> Result<YubiKey, Error> {
if let Some(serial) = serial { if let Some(serial) = serial {
eprintln!( eprintln!(
"{}", "{}",
i18n_embed_fl::fl!( fl!("open-yk-with-serial", yubikey_serial = serial.to_string())
crate::LANGUAGE_LOADER,
"open-yk-with-serial",
yubikey_serial = serial.to_string(),
)
); );
} else { } else {
eprintln!("{}", fl!("open-yk-without-serial")); eprintln!("{}", fl!("open-yk-without-serial"));
@@ -196,8 +242,7 @@ pub(crate) fn manage(yubikey: &mut YubiKey) -> Result<(), Error> {
eprintln!(); eprintln!();
let pin = Password::new() let pin = Password::new()
.with_prompt(i18n_embed_fl::fl!( .with_prompt(fl!(
crate::LANGUAGE_LOADER,
"mgr-enter-pin", "mgr-enter-pin",
yubikey_serial = yubikey.serial().to_string(), yubikey_serial = yubikey.serial().to_string(),
default_pin = DEFAULT_PIN, default_pin = DEFAULT_PIN,
@@ -211,11 +256,7 @@ pub(crate) fn manage(yubikey: &mut YubiKey) -> Result<(), Error> {
eprintln!("{}", fl!("mgr-change-default-pin")); eprintln!("{}", fl!("mgr-change-default-pin"));
eprintln!(); eprintln!();
let current_puk = Password::new() let current_puk = Password::new()
.with_prompt(i18n_embed_fl::fl!( .with_prompt(fl!("mgr-enter-current-puk", default_puk = DEFAULT_PUK))
crate::LANGUAGE_LOADER,
"mgr-enter-current-puk",
default_puk = DEFAULT_PUK,
))
.interact()?; .interact()?;
let new_pin = Password::new() let new_pin = Password::new()
.with_prompt(fl!("mgr-choose-new-pin")) .with_prompt(fl!("mgr-choose-new-pin"))
@@ -244,8 +285,7 @@ pub(crate) fn manage(yubikey: &mut YubiKey) -> Result<(), Error> {
mgm_key.set_protected(yubikey).map_err(|e| { mgm_key.set_protected(yubikey).map_err(|e| {
eprintln!( eprintln!(
"{}", "{}",
i18n_embed_fl::fl!( fl!(
crate::LANGUAGE_LOADER,
"mgr-changing-mgmt-key-error", "mgr-changing-mgmt-key-error",
management_key = hex::encode(mgm_key.as_ref()), management_key = hex::encode(mgm_key.as_ref()),
) )
@@ -258,6 +298,32 @@ pub(crate) fn manage(yubikey: &mut YubiKey) -> Result<(), Error> {
Ok(()) Ok(())
} }
/// Returns an iterator of keys that are occupying plugin-compatible slots, along with the
/// corresponding recipient if the key is compatible with this plugin.
pub(crate) fn list_slots(
yubikey: &mut YubiKey,
) -> Result<impl Iterator<Item = (Key, RetiredSlotId, Option<Recipient>)>, Error> {
Ok(Key::list(yubikey)?.into_iter().filter_map(|key| {
// We only use the retired slots.
match key.slot() {
SlotId::Retired(slot) => {
// Only P-256 keys are compatible with us.
let recipient = Recipient::from_certificate(key.certificate());
Some((key, slot, recipient))
}
_ => None,
}
}))
}
/// Returns an iterator of keys that are compatible with this plugin.
pub(crate) fn list_compatible(
yubikey: &mut YubiKey,
) -> Result<impl Iterator<Item = (Key, RetiredSlotId, Recipient)>, Error> {
list_slots(yubikey)
.map(|iter| iter.filter_map(|(key, slot, res)| res.map(|recipient| (key, slot, recipient))))
}
/// A reference to an age key stored in a YubiKey. /// A reference to an age key stored in a YubiKey.
#[derive(Debug)] #[derive(Debug)]
pub struct Stub { pub struct Stub {
@@ -340,11 +406,7 @@ impl Stub {
let mut yubikey = match open_by_serial(self.serial) { let mut yubikey = match open_by_serial(self.serial) {
Ok(yk) => yk, Ok(yk) => yk,
Err(yubikey::Error::NotFound) => { Err(yubikey::Error::NotFound) => {
let mut message = i18n_embed_fl::fl!( let mut message = fl!("plugin-insert-yk", yubikey_serial = self.serial.to_string());
crate::LANGUAGE_LOADER,
"plugin-insert-yk",
yubikey_serial = self.serial.to_string(),
);
// If the `confirm` command is available, we loop until either the YubiKey // If the `confirm` command is available, we loop until either the YubiKey
// we want is inserted, or the used explicitly skips. // we want is inserted, or the used explicitly skips.
@@ -365,8 +427,7 @@ impl Stub {
Err(_) => { Err(_) => {
return Ok(Err(identity::Error::Identity { return Ok(Err(identity::Error::Identity {
index: self.identity_index, index: self.identity_index,
message: i18n_embed_fl::fl!( message: fl!(
crate::LANGUAGE_LOADER,
"plugin-err-yk-opening", "plugin-err-yk-opening",
yubikey_serial = self.serial.to_string(), yubikey_serial = self.serial.to_string(),
), ),
@@ -377,8 +438,7 @@ impl Stub {
Err(age_core::plugin::Error::Fail) => { Err(age_core::plugin::Error::Fail) => {
return Ok(Err(identity::Error::Identity { return Ok(Err(identity::Error::Identity {
index: self.identity_index, index: self.identity_index,
message: i18n_embed_fl::fl!( message: fl!(
crate::LANGUAGE_LOADER,
"plugin-err-yk-opening", "plugin-err-yk-opening",
yubikey_serial = self.serial.to_string(), yubikey_serial = self.serial.to_string(),
), ),
@@ -388,8 +448,7 @@ impl Stub {
// We're going to loop around, meaning that the first attempt failed. // We're going to loop around, meaning that the first attempt failed.
// Change the message to indicate this to the user. // Change the message to indicate this to the user.
message = i18n_embed_fl::fl!( message = fl!(
crate::LANGUAGE_LOADER,
"plugin-insert-yk-retry", "plugin-insert-yk-retry",
yubikey_serial = self.serial.to_string(), yubikey_serial = self.serial.to_string(),
); );
@@ -402,8 +461,7 @@ impl Stub {
if callbacks.message(&message)?.is_err() { if callbacks.message(&message)?.is_err() {
return Ok(Err(identity::Error::Identity { return Ok(Err(identity::Error::Identity {
index: self.identity_index, index: self.identity_index,
message: i18n_embed_fl::fl!( message: fl!(
crate::LANGUAGE_LOADER,
"plugin-err-yk-not-found", "plugin-err-yk-not-found",
yubikey_serial = self.serial.to_string(), yubikey_serial = self.serial.to_string(),
), ),
@@ -419,8 +477,7 @@ impl Stub {
Err(_) => { Err(_) => {
return Ok(Err(identity::Error::Identity { return Ok(Err(identity::Error::Identity {
index: self.identity_index, index: self.identity_index,
message: i18n_embed_fl::fl!( message: fl!(
crate::LANGUAGE_LOADER,
"plugin-err-yk-opening", "plugin-err-yk-opening",
yubikey_serial = self.serial.to_string(), yubikey_serial = self.serial.to_string(),
), ),
@@ -432,8 +489,7 @@ impl Stub {
Ok(end) if end >= FIFTEEN_SECONDS => { Ok(end) if end >= FIFTEEN_SECONDS => {
return Ok(Err(identity::Error::Identity { return Ok(Err(identity::Error::Identity {
index: self.identity_index, index: self.identity_index,
message: i18n_embed_fl::fl!( message: fl!(
crate::LANGUAGE_LOADER,
"plugin-err-yk-timed-out", "plugin-err-yk-timed-out",
yubikey_serial = self.serial.to_string(), yubikey_serial = self.serial.to_string(),
), ),
@@ -447,8 +503,7 @@ impl Stub {
Err(_) => { Err(_) => {
return Ok(Err(identity::Error::Identity { return Ok(Err(identity::Error::Identity {
index: self.identity_index, index: self.identity_index,
message: i18n_embed_fl::fl!( message: fl!(
crate::LANGUAGE_LOADER,
"plugin-err-yk-opening", "plugin-err-yk-opening",
yubikey_serial = self.serial.to_string(), yubikey_serial = self.serial.to_string(),
), ),
@@ -459,11 +514,10 @@ 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 (cert, 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| {
PublicKeyInfo::EcP256(pubkey) => Recipient::from_encoded(pubkey) Recipient::from_certificate(&cert)
.filter(|pk| pk.tag() == self.tag) .filter(|pk| pk.tag() == self.tag)
.map(|pk| (cert, pk)), .map(|pk| (cert, pk))
_ => None,
}) { }) {
Some(pk) => pk, Some(pk) => pk,
None => { None => {
@@ -509,9 +563,8 @@ impl Connection {
) -> io::Result<Result<(), identity::Error>> { ) -> io::Result<Result<(), identity::Error>> {
// Check if we can skip requesting a PIN. // Check if we can skip requesting a PIN.
if self.cached_metadata.is_none() { if self.cached_metadata.is_none() {
let (_, cert) = x509_parser::parse_x509_certificate(self.cert.as_ref()).unwrap();
self.cached_metadata = self.cached_metadata =
match Metadata::extract(&mut self.yubikey, self.slot, &cert, true) { match Metadata::extract(&mut self.yubikey, self.slot, &self.cert, true) {
None => { None => {
return Ok(Err(identity::Error::Identity { return Ok(Err(identity::Error::Identity {
index: self.identity_index, index: self.identity_index,
@@ -528,8 +581,7 @@ impl Connection {
// The policy requires a PIN, so request it. // The policy requires a PIN, so request it.
// Note that we can't distinguish between PinPolicy::Once and PinPolicy::Always // Note that we can't distinguish between PinPolicy::Once and PinPolicy::Always
// because this plugin is ephemeral, so we always request the PIN. // because this plugin is ephemeral, so we always request the PIN.
let enter_pin_msg = i18n_embed_fl::fl!( let enter_pin_msg = fl!(
crate::LANGUAGE_LOADER,
"plugin-enter-pin", "plugin-enter-pin",
yubikey_serial = self.yubikey.serial().to_string(), yubikey_serial = self.yubikey.serial().to_string(),
); );
@@ -555,8 +607,7 @@ impl Connection {
Err(_) => { Err(_) => {
return Ok(Err(identity::Error::Identity { return Ok(Err(identity::Error::Identity {
index: self.identity_index, index: self.identity_index,
message: i18n_embed_fl::fl!( message: fl!(
crate::LANGUAGE_LOADER,
"plugin-err-pin-required", "plugin-err-pin-required",
yubikey_serial = self.yubikey.serial().to_string(), yubikey_serial = self.yubikey.serial().to_string(),
), ),
+26 -95
View File
@@ -12,12 +12,7 @@ use i18n_embed::{
}; };
use lazy_static::lazy_static; use lazy_static::lazy_static;
use rust_embed::RustEmbed; use rust_embed::RustEmbed;
use yubikey::{ use yubikey::{piv::RetiredSlotId, reader::Context, PinPolicy, Serial, TouchPolicy};
certificate::PublicKeyInfo,
piv::{RetiredSlotId, SlotId},
reader::Context,
Key, PinPolicy, Serial, TouchPolicy,
};
mod builder; mod builder;
mod error; mod error;
@@ -73,6 +68,9 @@ macro_rules! fl {
($message_id:literal) => {{ ($message_id:literal) => {{
i18n_embed_fl::fl!($crate::LANGUAGE_LOADER, $message_id) i18n_embed_fl::fl!($crate::LANGUAGE_LOADER, $message_id)
}}; }};
($message_id:literal, $($kwarg:expr),* $(,)*) => {{
i18n_embed_fl::fl!($crate::LANGUAGE_LOADER, $message_id, $($kwarg,)*)
}};
} }
#[derive(Debug, Options)] #[derive(Debug, Options)]
@@ -193,26 +191,12 @@ fn print_single(
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut yubikey = key::open(serial)?; let mut yubikey = key::open(serial)?;
let mut keys = Key::list(&mut yubikey)?.into_iter().filter_map(|key| { let (key, slot, recipient) = key::list_compatible(&mut yubikey)?
// - 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_encoded(pubkey).map(|r| (key, slot, r))
}
_ => None,
}
});
let (key, slot, recipient) = keys
.find(|(_, s, _)| s == &slot) .find(|(_, s, _)| s == &slot)
.ok_or(Error::SlotHasNoIdentity(slot))?; .ok_or(Error::SlotHasNoIdentity(slot))?;
let stub = key::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 = util::Metadata::extract(&mut yubikey, slot, key.certificate(), true).unwrap();
.ok()
.and_then(|(_, cert)| util::Metadata::extract(&mut yubikey, slot, &cert, true))
.unwrap();
printer(stub, recipient, metadata); printer(stub, recipient, metadata);
@@ -236,26 +220,9 @@ fn print_multiple(
} }
} }
for key in Key::list(&mut yubikey)? { for (key, slot, recipient) in key::list_compatible(&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_encoded(pubkey) {
Some(recipient) => recipient,
None => continue,
},
_ => continue,
};
let stub = key::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 util::Metadata::extract(&mut yubikey, slot, key.certificate(), all)
.ok()
.and_then(|(_, cert)| util::Metadata::extract(&mut yubikey, slot, &cert, all))
{ {
Some(res) => res, Some(res) => res,
None => continue, None => continue,
@@ -268,15 +235,7 @@ fn print_multiple(
println!(); println!();
} }
if printed > 1 { if printed > 1 {
eprintln!( eprintln!("{}", fl!("printed-multiple", kind = kind, count = printed));
"{}",
i18n_embed_fl::fl!(
LANGUAGE_LOADER,
"printed-multiple",
kind = kind,
count = printed,
)
);
} }
Ok(()) Ok(())
@@ -382,8 +341,7 @@ fn main() -> Result<(), Error> {
eprintln!( eprintln!(
"{}", "{}",
i18n_embed_fl::fl!( fl!(
LANGUAGE_LOADER,
"cli-setup-intro", "cli-setup-intro",
generate_usage = "age-plugin-yubikey --generate", generate_usage = "age-plugin-yubikey --generate",
) )
@@ -402,8 +360,7 @@ fn main() -> Result<(), Error> {
.iter() .iter()
.map(|reader| { .map(|reader| {
key::open_connection(reader).map(|yk| { key::open_connection(reader).map(|yk| {
i18n_embed_fl::fl!( fl!(
LANGUAGE_LOADER,
"cli-setup-yk-name", "cli-setup-yk-name",
yubikey_name = reader.name(), yubikey_name = reader.name(),
yubikey_serial = yk.serial().to_string(), yubikey_serial = yk.serial().to_string(),
@@ -421,17 +378,16 @@ fn main() -> Result<(), Error> {
None => return Ok(()), None => return Ok(()),
}; };
let keys = Key::list(&mut yubikey)?; let keys = key::list_slots(&mut yubikey)?.collect::<Vec<_>>();
// Identify slots that we can't allow the user to select. // Identify slots that we can't allow the user to select.
let slot_details: Vec<_> = USABLE_SLOTS let slot_details: Vec<_> = USABLE_SLOTS
.iter() .iter()
.map(|&slot| { .map(|&slot| {
keys.iter() keys.iter()
.find(|key| key.slot() == SlotId::Retired(slot)) .find(|(_, s, _)| s == &slot)
.map(|key| match key.certificate().subject_pki() { .map(|(key, _, recipient)| {
PublicKeyInfo::EcP256(pubkey) => { recipient.as_ref().map(|_| {
p256::Recipient::from_encoded(pubkey).map(|_| {
// Cache the details we need to display to the user. // Cache the details we need to display to the user.
let (_, cert) = let (_, cert) =
x509_parser::parse_x509_certificate(key.certificate().as_ref()) x509_parser::parse_x509_certificate(key.certificate().as_ref())
@@ -441,8 +397,6 @@ fn main() -> Result<(), Error> {
format!("{}, created: {}", name, created) format!("{}, created: {}", name, created)
}) })
}
_ => None,
}) })
}) })
.collect(); .collect();
@@ -455,20 +409,13 @@ fn main() -> Result<(), Error> {
let i = i + 1; let i = i + 1;
match occupied { match occupied {
Some(Some(name)) => i18n_embed_fl::fl!( Some(Some(name)) => fl!(
LANGUAGE_LOADER,
"cli-setup-slot-usable", "cli-setup-slot-usable",
slot_index = i, slot_index = i,
slot_name = name.as_str(), slot_name = name.as_str(),
), ),
Some(None) => i18n_embed_fl::fl!( Some(None) => fl!("cli-setup-slot-unusable", slot_index = i),
LANGUAGE_LOADER, None => fl!("cli-setup-slot-empty", slot_index = i),
"cli-setup-slot-unusable",
slot_index = i,
),
None => {
i18n_embed_fl::fl!(LANGUAGE_LOADER, "cli-setup-slot-empty", slot_index = i)
}
} }
}) })
.collect(); .collect();
@@ -491,27 +438,17 @@ fn main() -> Result<(), Error> {
} }
}; };
if let Some(key) = keys.iter().find(|key| key.slot() == SlotId::Retired(slot)) { if let Some((key, _, recipient)) = keys.into_iter().find(|(_, s, _)| s == &slot) {
let recipient = match key.certificate().subject_pki() { let recipient = recipient.expect("We checked this above");
PublicKeyInfo::EcP256(pubkey) => {
p256::Recipient::from_encoded(pubkey).expect("We checked this above")
}
_ => unreachable!(),
};
if Confirm::new() if Confirm::new()
.with_prompt(i18n_embed_fl::fl!( .with_prompt(fl!("cli-setup-use-existing", slot_index = slot_index))
LANGUAGE_LOADER,
"cli-setup-use-existing",
slot_index = slot_index,
))
.interact()? .interact()?
{ {
let stub = key::Stub::new(yubikey.serial(), slot, &recipient); let stub = key::Stub::new(yubikey.serial(), slot, &recipient);
let (_, cert) =
x509_parser::parse_x509_certificate(key.certificate().as_ref()).unwrap();
let metadata = let metadata =
util::Metadata::extract(&mut yubikey, slot, &cert, true).unwrap(); util::Metadata::extract(&mut yubikey, slot, key.certificate(), true)
.unwrap();
((stub, recipient, metadata), false) ((stub, recipient, metadata), false)
} else { } else {
@@ -576,11 +513,7 @@ fn main() -> Result<(), Error> {
}; };
if Confirm::new() if Confirm::new()
.with_prompt(i18n_embed_fl::fl!( .with_prompt(fl!("cli-setup-generate-new", slot_index = slot_index))
LANGUAGE_LOADER,
"cli-setup-generate-new",
slot_index = slot_index,
))
.interact()? .interact()?
{ {
eprintln!(); eprintln!();
@@ -632,8 +565,7 @@ fn main() -> Result<(), Error> {
writeln!( writeln!(
file, file,
"{}", "{}",
i18n_embed_fl::fl!( fl!(
LANGUAGE_LOADER,
"yubikey-identity", "yubikey-identity",
yubikey_metadata = metadata.to_string(), yubikey_metadata = metadata.to_string(),
recipient = recipient.to_string(), recipient = recipient.to_string(),
@@ -668,8 +600,7 @@ fn main() -> Result<(), Error> {
eprintln!(); eprintln!();
eprintln!( eprintln!(
"{}", "{}",
i18n_embed_fl::fl!( fl!(
LANGUAGE_LOADER,
"cli-setup-finished", "cli-setup-finished",
is_new = if is_new { "true" } else { "false" }, is_new = if is_new { "true" } else { "false" },
recipient = recipient.to_string(), recipient = recipient.to_string(),
+14 -1
View File
@@ -1,6 +1,8 @@
use bech32::{ToBase32, Variant}; use bech32::{ToBase32, Variant};
use p256::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint}; use p256::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use yubikey::{certificate::PublicKeyInfo, Certificate};
use std::fmt; use std::fmt;
use crate::RECIPIENT_PREFIX; use crate::RECIPIENT_PREFIX;
@@ -42,11 +44,22 @@ impl Recipient {
} }
} }
pub(crate) fn from_certificate(cert: &Certificate) -> Option<Self> {
Self::from_spki(cert.subject_pki())
}
pub(crate) fn from_spki(spki: &PublicKeyInfo) -> Option<Self> {
match spki {
PublicKeyInfo::EcP256(pubkey) => Self::from_encoded(pubkey),
_ => None,
}
}
/// Attempts to parse a valid YubiKey recipient from its SEC-1 encoding. /// Attempts to parse a valid YubiKey recipient from its SEC-1 encoding.
/// ///
/// 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> { fn from_encoded(encoded: &p256::EncodedPoint) -> Option<Self> {
p256::PublicKey::from_encoded_point(encoded).map(Recipient) p256::PublicKey::from_encoded_point(encoded).map(Recipient)
} }
+1 -2
View File
@@ -71,8 +71,7 @@ impl RecipientPluginV1 for RecipientPlugin {
Ok(Some(conn)) => yk_recipients.push(conn.recipient().clone()), Ok(Some(conn)) => yk_recipients.push(conn.recipient().clone()),
Ok(None) => yk_errors.push(recipient::Error::Identity { Ok(None) => yk_errors.push(recipient::Error::Identity {
index: stub.identity_index, index: stub.identity_index,
message: i18n_embed_fl::fl!( message: fl!(
crate::LANGUAGE_LOADER,
"plugin-err-yk-opening", "plugin-err-yk-opening",
yubikey_serial = stub.serial.to_string(), yubikey_serial = stub.serial.to_string(),
), ),
+10 -16
View File
@@ -4,7 +4,7 @@ use std::iter;
use x509_parser::{certificate::X509Certificate, der_parser::oid::Oid}; use x509_parser::{certificate::X509Certificate, der_parser::oid::Oid};
use yubikey::{ use yubikey::{
piv::{RetiredSlotId, SlotId}, piv::{RetiredSlotId, SlotId},
PinPolicy, Serial, TouchPolicy, YubiKey, Certificate, PinPolicy, Serial, TouchPolicy, YubiKey,
}; };
use crate::fl; use crate::fl;
@@ -112,9 +112,11 @@ impl Metadata {
pub(crate) fn extract( pub(crate) fn extract(
yubikey: &mut YubiKey, yubikey: &mut YubiKey,
slot: RetiredSlotId, slot: RetiredSlotId,
cert: &X509Certificate, cert: &Certificate,
all: bool, all: bool,
) -> Option<Self> { ) -> Option<Self> {
let (_, cert) = x509_parser::parse_x509_certificate(cert.as_ref()).ok()?;
// We store the PIN and touch policies for identities in their certificates // We store the PIN and touch policies for identities in their certificates
// 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
@@ -143,10 +145,10 @@ impl Metadata {
.unwrap_or((None, None)) .unwrap_or((None, None))
}; };
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
@@ -180,8 +182,7 @@ impl fmt::Display for Metadata {
write!( write!(
f, f,
"{}", "{}",
i18n_embed_fl::fl!( fl!(
crate::LANGUAGE_LOADER,
"yubikey-metadata", "yubikey-metadata",
serial = self.serial.to_string(), serial = self.serial.to_string(),
slot = slot_to_ui(&self.slot), slot = slot_to_ui(&self.slot),
@@ -197,20 +198,13 @@ impl fmt::Display for Metadata {
pub(crate) fn print_identity(stub: Stub, recipient: Recipient, metadata: Metadata) { pub(crate) fn print_identity(stub: Stub, recipient: Recipient, metadata: Metadata) {
let recipient = recipient.to_string(); let recipient = recipient.to_string();
if !console::user_attended() { if !console::user_attended() {
eprintln!( let recipient = recipient.as_str();
"{}", eprintln!("{}", fl!("print-recipient", recipient = recipient));
i18n_embed_fl::fl!(
crate::LANGUAGE_LOADER,
"print-recipient",
recipient = recipient.as_str(),
)
);
} }
println!( println!(
"{}", "{}",
i18n_embed_fl::fl!( fl!(
crate::LANGUAGE_LOADER,
"yubikey-identity", "yubikey-identity",
yubikey_metadata = metadata.to_string(), yubikey_metadata = metadata.to_string(),
recipient = recipient, recipient = recipient,