@@ -9,6 +9,16 @@ to 0.3.0 are beta releases.
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
### Changed
|
### Changed
|
||||||
- MSRV is now 1.60.0.
|
- MSRV is now 1.60.0.
|
||||||
|
- The YubiKey PIV PIN and touch caches are now preserved across processes in
|
||||||
|
most cases. See [README.md](README.md#agent-support) for exceptions. This has
|
||||||
|
several usability effects:
|
||||||
|
- If a YubiKey's PIN is cached by an agent like `yubikey-agent`, and then
|
||||||
|
`age-plugin-yubikey` is run (either directly or as a plugin), the agent
|
||||||
|
won't request a PIN entry on its next use.
|
||||||
|
- If a YubiKey's PIN was requested by either a previous invocation of
|
||||||
|
`age-plugin-yubikey` or an agent like `yubikey-agent`, subsequent calls to
|
||||||
|
`age-plugin-yubikey` won't request a PIN entry to decrypt a file with an
|
||||||
|
identity that has a PIN policy of `once`.
|
||||||
|
|
||||||
## [0.3.2] - 2023-01-01
|
## [0.3.2] - 2023-01-01
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
Generated
+1
-2
@@ -2122,8 +2122,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "yubikey"
|
name = "yubikey"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/iqlusioninc/yubikey.rs.git?rev=1d33ea174791f699dfc0e6a06648aa3d9e066144#1d33ea174791f699dfc0e6a06648aa3d9e066144"
|
||||||
checksum = "10e6fa9476951a9b93d9a31aa5554b5bbac7aafdc5b23e663eb3f9b635c86053"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base16ct",
|
"base16ct",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|||||||
@@ -53,3 +53,6 @@ sysinfo = "0.27"
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
man = "0.3"
|
man = "0.3"
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
yubikey = { git = "https://github.com/iqlusioninc/yubikey.rs.git", rev = "1d33ea174791f699dfc0e6a06648aa3d9e066144" }
|
||||||
|
|||||||
@@ -115,13 +115,24 @@ age client as normal (e.g. `rage -d -i yubikey-identity.txt`).
|
|||||||
### Agent support
|
### Agent support
|
||||||
|
|
||||||
`age-plugin-yubikey` does not provide or interact with an agent for decryption.
|
`age-plugin-yubikey` does not provide or interact with an agent for decryption.
|
||||||
As age plugin binaries have short lifetimes (they only run while the age client
|
It does however preserve the PIN cache by not soft-resetting the YubiKey after a
|
||||||
is running), this means that YubiKey identities configured with a PIN policy of
|
decryption or read-only operation, which enables YubiKey identities configured
|
||||||
`once` will actually prompt for the PIN on every decryption.
|
with a PIN policy of `once` to not prompt for the PIN on every decryption.
|
||||||
|
|
||||||
A decryption agent will most likely be implemented as a separate age plugin that
|
The session that corresponds to the `once` policy can be ended in several ways,
|
||||||
interacts with [`yubikey-agent`](https://github.com/FiloSottile/yubikey-agent),
|
not all of which are necessarily intuitive:
|
||||||
enabling YubiKeys to be used simultaneously with age and SSH.
|
|
||||||
|
- Unplugging the YubiKey (the obvious way).
|
||||||
|
- Using a different applet (e.g. FIDO2). This causes the PIV applet to be closed
|
||||||
|
which clears its state.
|
||||||
|
- Generating a new age identity via `age-plugin-yubikey --generate` or the CLI
|
||||||
|
interface. This is to avoid leaving the YubiKey authenticated with the
|
||||||
|
management key.
|
||||||
|
|
||||||
|
If the current PIN UX proves to be insufficient, a decryption agent will most
|
||||||
|
likely be implemented as a separate age plugin that interacts with
|
||||||
|
[`yubikey-agent`](https://github.com/FiloSottile/yubikey-agent), enabling
|
||||||
|
YubiKeys to be used simultaneously with age and SSH.
|
||||||
|
|
||||||
### Manual setup and technical details
|
### Manual setup and technical details
|
||||||
|
|
||||||
|
|||||||
+28
-4
@@ -236,6 +236,23 @@ pub(crate) fn open(serial: Option<Serial>) -> Result<YubiKey, Error> {
|
|||||||
Ok(yubikey)
|
Ok(yubikey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Disconnect from the YubiKey without resetting it.
|
||||||
|
///
|
||||||
|
/// This can be used to preserve the YubiKey's PIN and touch caches. There are two cases
|
||||||
|
/// where we want to do this:
|
||||||
|
///
|
||||||
|
/// - We connected to this YubiKey in a read-only context, so we have not made any changes
|
||||||
|
/// to the YubiKey's state. However, we might have asked an agent to release the YubiKey
|
||||||
|
/// in `key::open_connection`, and we want to allow any state it may have left behind
|
||||||
|
/// (such as cached PINs or touches) to persist beyond our execution, for usability.
|
||||||
|
/// - We opened this connection in a decryption context, so the only changes to the
|
||||||
|
/// YubiKey's state were to potentially cache the PIN and/or touch (depending on the
|
||||||
|
/// policies of the slot). We want to allow these to persist beyond our execution, for
|
||||||
|
/// usability.
|
||||||
|
pub(crate) fn disconnect_without_reset(yubikey: YubiKey) {
|
||||||
|
let _ = yubikey.disconnect(pcsc::Disposition::LeaveCard);
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn manage(yubikey: &mut YubiKey) -> Result<(), Error> {
|
pub(crate) fn manage(yubikey: &mut YubiKey) -> Result<(), Error> {
|
||||||
const DEFAULT_PIN: &str = "123456";
|
const DEFAULT_PIN: &str = "123456";
|
||||||
const DEFAULT_PUK: &str = "12345678";
|
const DEFAULT_PUK: &str = "12345678";
|
||||||
@@ -575,13 +592,13 @@ impl Connection {
|
|||||||
metadata => metadata,
|
metadata => metadata,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if let Some(PinPolicy::Never) = self.cached_metadata.as_ref().and_then(|m| m.pin_policy) {
|
match self.cached_metadata.as_ref().and_then(|m| m.pin_policy) {
|
||||||
return Ok(Ok(()));
|
Some(PinPolicy::Never) => return Ok(Ok(())),
|
||||||
|
Some(PinPolicy::Once) if self.yubikey.verify_pin(&[]).is_ok() => return Ok(Ok(())),
|
||||||
|
_ => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
|
||||||
// because this plugin is ephemeral, so we always request the PIN.
|
|
||||||
let enter_pin_msg = fl!(
|
let enter_pin_msg = fl!(
|
||||||
"plugin-enter-pin",
|
"plugin-enter-pin",
|
||||||
yubikey_serial = self.yubikey.serial().to_string(),
|
yubikey_serial = self.yubikey.serial().to_string(),
|
||||||
@@ -674,6 +691,13 @@ impl Connection {
|
|||||||
Err(_) => Err(()),
|
Err(_) => Err(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Close this connection without resetting the YubiKey.
|
||||||
|
///
|
||||||
|
/// This can be used to preserve the YubiKey's PIN and touch caches.
|
||||||
|
pub(crate) fn disconnect_without_reset(self) {
|
||||||
|
disconnect_without_reset(self.yubikey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
+18
-2
@@ -181,6 +181,13 @@ fn generate(flags: PluginFlags) -> Result<(), Error> {
|
|||||||
|
|
||||||
util::print_identity(stub, recipient, metadata);
|
util::print_identity(stub, recipient, metadata);
|
||||||
|
|
||||||
|
// We have written to the YubiKey, which means we've authenticated with the management
|
||||||
|
// key. Out of an abundance of caution, we let the YubiKey be reset on disconnect,
|
||||||
|
// which will clear its PIN and touch caches. This has as small negative UX effect,
|
||||||
|
// but identity generation is a relatively infrequent occurrence, and users are more
|
||||||
|
// likely to see their cached PINs reset due to switching applets (e.g. from PIV to
|
||||||
|
// FIDO2).
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,6 +207,8 @@ fn print_single(
|
|||||||
|
|
||||||
printer(stub, recipient, metadata);
|
printer(stub, recipient, metadata);
|
||||||
|
|
||||||
|
key::disconnect_without_reset(yubikey);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,6 +242,8 @@ fn print_multiple(
|
|||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
|
key::disconnect_without_reset(yubikey);
|
||||||
}
|
}
|
||||||
if printed > 1 {
|
if printed > 1 {
|
||||||
eprintln!("{}", fl!("printed-multiple", kind = kind, count = printed));
|
eprintln!("{}", fl!("printed-multiple", kind = kind, count = printed));
|
||||||
@@ -360,11 +371,13 @@ fn main() -> Result<(), Error> {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|reader| {
|
.map(|reader| {
|
||||||
key::open_connection(reader).map(|yk| {
|
key::open_connection(reader).map(|yk| {
|
||||||
fl!(
|
let name = fl!(
|
||||||
"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(),
|
||||||
)
|
);
|
||||||
|
key::disconnect_without_reset(yk);
|
||||||
|
name
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
@@ -457,8 +470,10 @@ fn main() -> Result<(), Error> {
|
|||||||
util::Metadata::extract(&mut yubikey, slot, key.certificate(), true)
|
util::Metadata::extract(&mut yubikey, slot, key.certificate(), true)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
key::disconnect_without_reset(yubikey);
|
||||||
((stub, recipient, metadata), false)
|
((stub, recipient, metadata), false)
|
||||||
} else {
|
} else {
|
||||||
|
key::disconnect_without_reset(yubikey);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -540,6 +555,7 @@ fn main() -> Result<(), Error> {
|
|||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
key::disconnect_without_reset(yubikey);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -249,6 +249,8 @@ impl IdentityPluginV1 for IdentityPlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
conn.disconnect_without_reset();
|
||||||
}
|
}
|
||||||
Ok(file_keys)
|
Ok(file_keys)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user