Enable users to skip YubiKeys at plugging-in time

This requires the `confirm` plugin command to be supported by the age
client; otherwise we fall back to the previous message-plus-timer
method.
This commit is contained in:
Jack Grigg
2022-04-26 11:30:16 +00:00
parent 3b0da8bd25
commit f8314c5d6d
5 changed files with 196 additions and 64 deletions
Generated
+77 -22
View File
@@ -20,27 +20,27 @@ dependencies = [
[[package]]
name = "age-core"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70afa630ef12a4fc666277713efbe6da2bc87bb3f3af0f1149415b701362c615"
source = "git+https://github.com/str4d/rage.git?rev=fdb518c6d802a3618b47d959d56af6e60c668627#fdb518c6d802a3618b47d959d56af6e60c668627"
dependencies = [
"base64",
"chacha20poly1305",
"cookie-factory",
"hkdf",
"io_tee",
"nom",
"rand",
"secrecy",
"sha2",
"sha2 0.10.2",
"tempfile",
]
[[package]]
name = "age-plugin"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74049e94d591e0b96128880bb9dcbc5f27432b3089725524d020616e1dc36e2b"
source = "git+https://github.com/str4d/rage.git?rev=fdb518c6d802a3618b47d959d56af6e60c668627#fdb518c6d802a3618b47d959d56af6e60c668627"
dependencies = [
"age-core",
"base64",
"bech32",
"chrono",
]
@@ -68,7 +68,7 @@ dependencies = [
"pcsc",
"rand",
"rust-embed",
"sha2",
"sha2 0.9.9",
"which",
"x509",
"x509-parser",
@@ -149,6 +149,15 @@ dependencies = [
"generic-array",
]
[[package]]
name = "block-buffer"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
dependencies = [
"generic-array",
]
[[package]]
name = "byteorder"
version = "1.4.3"
@@ -265,6 +274,16 @@ dependencies = [
"zeroize",
]
[[package]]
name = "crypto-common"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "crypto-mac"
version = "0.11.1"
@@ -357,6 +376,17 @@ dependencies = [
"generic-array",
]
[[package]]
name = "digest"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
dependencies = [
"block-buffer 0.10.2",
"crypto-common",
"subtle",
]
[[package]]
name = "ecdsa"
version = "0.12.4"
@@ -365,7 +395,7 @@ checksum = "43ee23aa5b4f68c7a092b5c3beb25f50c406adc75e2363634f242f28ab255372"
dependencies = [
"der",
"elliptic-curve",
"hmac",
"hmac 0.11.0",
"signature",
]
@@ -563,12 +593,11 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hkdf"
version = "0.11.0"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01706d578d5c281058480e673ae4086a9f4710d8df1ad80a5b03e39ece5f886b"
checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437"
dependencies = [
"digest",
"hmac",
"hmac 0.12.1",
]
[[package]]
@@ -578,7 +607,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b"
dependencies = [
"crypto-mac",
"digest",
"digest 0.9.0",
]
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest 0.10.3",
]
[[package]]
@@ -685,6 +723,12 @@ dependencies = [
"unic-langid",
]
[[package]]
name = "io_tee"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b3f7cef34251886990511df1c61443aa928499d598a9473929ab5a90a527304"
[[package]]
name = "lazy_static"
version = "1.4.0"
@@ -917,7 +961,7 @@ checksum = "d053368e1bae4c8a672953397bd1bd7183dde1c72b0b7612a15719173148d186"
dependencies = [
"ecdsa",
"elliptic-curve",
"sha2",
"sha2 0.9.9",
]
[[package]]
@@ -1156,7 +1200,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05c2603e2823634ab331437001b411b9ed11660fbc4066f3908c84a9439260d"
dependencies = [
"byteorder",
"digest",
"digest 0.9.0",
"lazy_static",
"num-bigint-dig",
"num-integer",
@@ -1199,7 +1243,7 @@ version = "7.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "756feca3afcbb1487a1d01f4ecd94cf8ec98ea074c55a69e7136d29fb6166029"
dependencies = [
"sha2",
"sha2 0.9.9",
"walkdir",
]
@@ -1274,10 +1318,10 @@ version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6"
dependencies = [
"block-buffer",
"block-buffer 0.9.0",
"cfg-if",
"cpufeatures",
"digest",
"digest 0.9.0",
"opaque-debug",
]
@@ -1287,20 +1331,31 @@ version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
dependencies = [
"block-buffer",
"block-buffer 0.9.0",
"cfg-if",
"cpufeatures",
"digest",
"digest 0.9.0",
"opaque-debug",
]
[[package]]
name = "sha2"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676"
dependencies = [
"cfg-if",
"cpufeatures",
"digest 0.10.3",
]
[[package]]
name = "signature"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2807892cfa58e081aa1f1111391c7a0649d4fa127a4ffbe34bcbfb35a1171a4"
dependencies = [
"digest",
"digest 0.9.0",
"rand_core",
]
@@ -1658,7 +1713,7 @@ dependencies = [
"der-parser",
"des",
"elliptic-curve",
"hmac",
"hmac 0.11.0",
"log",
"nom",
"num-bigint-dig",
@@ -1672,7 +1727,7 @@ dependencies = [
"rsa",
"secrecy",
"sha-1",
"sha2",
"sha2 0.9.9",
"subtle",
"subtle-encoding",
"uuid",
+4
View File
@@ -50,3 +50,7 @@ rust-embed = "6"
[dev-dependencies]
flate2 = "1"
man = "0.3"
[patch.crates-io]
age-core = { git = "https://github.com/str4d/rage.git", rev = "fdb518c6d802a3618b47d959d56af6e60c668627" }
age-plugin = { git = "https://github.com/str4d/rage.git", rev = "fdb518c6d802a3618b47d959d56af6e60c668627" }
+3
View File
@@ -151,6 +151,9 @@ plugin-err-invalid-stanza = Invalid {-yubikey} stanza
plugin-err-decryption-failed = Failed to decrypt {-yubikey} stanza
plugin-insert-yk = Please insert {-yubikey} with serial {$yubikey_serial}
plugin-yk-is-plugged-in = {-yubikey} is plugged in
plugin-skip-this-yk = Skip this {-yubikey}
plugin-insert-yk-retry = Could not open {-yubikey}. Please insert {-yubikey} with serial {$yubikey_serial}
plugin-err-yk-not-found = Could not find {-yubikey} with serial {$yubikey_serial}
plugin-err-yk-opening = Could not open {-yubikey} with serial {$yubikey_serial}
plugin-err-yk-timed-out = Timed out while waiting for {-yubikey} with serial {$yubikey_serial} to be inserted
+98 -40
View File
@@ -199,7 +199,7 @@ pub struct Stub {
pub(crate) serial: Serial,
pub(crate) slot: RetiredSlotId,
pub(crate) tag: [u8; TAG_BYTES],
identity_index: usize,
pub(crate) identity_index: usize,
}
impl fmt::Display for Stub {
@@ -260,38 +260,53 @@ impl Stub {
self.tag == line.tag
}
/// Returns:
/// - `Ok(Ok(Some(connection)))` if we successfully connected to this YubiKey.
/// - `Ok(Ok(None))` if the user told us to skip this YubiKey.
/// - `Ok(Err(_))` if we encountered an error while trying to connect to the YubiKey.
/// - `Err(_)` on communication errors with the age client.
pub(crate) fn connect<E>(
&self,
callbacks: &mut dyn Callbacks<E>,
) -> io::Result<Result<Connection, identity::Error>> {
) -> io::Result<Result<Option<Connection>, identity::Error>> {
let mut yubikey = match YubiKey::open_by_serial(self.serial) {
Ok(yk) => yk,
Err(yubikey::Error::NotFound) => {
if callbacks
.message(&i18n_embed_fl::fl!(
crate::LANGUAGE_LOADER,
"plugin-insert-yk",
yubikey_serial = self.serial.to_string(),
))?
.is_err()
{
return Ok(Err(identity::Error::Identity {
index: self.identity_index,
message: i18n_embed_fl::fl!(
crate::LANGUAGE_LOADER,
"plugin-err-yk-not-found",
yubikey_serial = self.serial.to_string(),
),
}));
}
let mut message = i18n_embed_fl::fl!(
crate::LANGUAGE_LOADER,
"plugin-insert-yk",
yubikey_serial = self.serial.to_string(),
);
// Start a 15-second timer waiting for the YubiKey to be inserted
let start = SystemTime::now();
loop {
match YubiKey::open_by_serial(self.serial) {
Ok(yubikey) => break yubikey,
Err(yubikey::Error::NotFound) => (),
Err(_) => {
// If the `confirm` command is available, we loop until either the YubiKey
// we want is inserted, or the used explicitly skips.
let yubikey = loop {
match callbacks.confirm(
&message,
&fl!("plugin-yk-is-plugged-in"),
Some(&fl!("plugin-skip-this-yk")),
)? {
// `confirm` command is not available.
Err(age_core::plugin::Error::Unsupported) => break None,
// User told us to skip this key.
Ok(false) => return Ok(Ok(None)),
// User said they plugged it in; try it.
Ok(true) => match YubiKey::open_by_serial(self.serial) {
Ok(yubikey) => break Some(yubikey),
Err(yubikey::Error::NotFound) => (),
Err(_) => {
return Ok(Err(identity::Error::Identity {
index: self.identity_index,
message: i18n_embed_fl::fl!(
crate::LANGUAGE_LOADER,
"plugin-err-yk-opening",
yubikey_serial = self.serial.to_string(),
),
}));
}
},
// We can't communicate with the user.
Err(age_core::plugin::Error::Fail) => {
return Ok(Err(identity::Error::Identity {
index: self.identity_index,
message: i18n_embed_fl::fl!(
@@ -299,22 +314,65 @@ impl Stub {
"plugin-err-yk-opening",
yubikey_serial = self.serial.to_string(),
),
}));
}))
}
}
match SystemTime::now().duration_since(start) {
Ok(end) if end >= FIFTEEN_SECONDS => {
return Ok(Err(identity::Error::Identity {
index: self.identity_index,
message: i18n_embed_fl::fl!(
crate::LANGUAGE_LOADER,
"plugin-err-yk-timed-out",
yubikey_serial = self.serial.to_string(),
),
}))
// We're going to loop around, meaning that the first attempt failed.
// Change the message to indicate this to the user.
message = i18n_embed_fl::fl!(
crate::LANGUAGE_LOADER,
"plugin-insert-yk-retry",
yubikey_serial = self.serial.to_string(),
);
};
if let Some(yk) = yubikey {
yk
} else {
// `confirm` is not available; fall back to `message` with a timeout.
if callbacks.message(&message)?.is_err() {
return Ok(Err(identity::Error::Identity {
index: self.identity_index,
message: i18n_embed_fl::fl!(
crate::LANGUAGE_LOADER,
"plugin-err-yk-not-found",
yubikey_serial = self.serial.to_string(),
),
}));
}
// Start a 15-second timer waiting for the YubiKey to be inserted
let start = SystemTime::now();
loop {
match YubiKey::open_by_serial(self.serial) {
Ok(yubikey) => break yubikey,
Err(yubikey::Error::NotFound) => (),
Err(_) => {
return Ok(Err(identity::Error::Identity {
index: self.identity_index,
message: i18n_embed_fl::fl!(
crate::LANGUAGE_LOADER,
"plugin-err-yk-opening",
yubikey_serial = self.serial.to_string(),
),
}));
}
}
match SystemTime::now().duration_since(start) {
Ok(end) if end >= FIFTEEN_SECONDS => {
return Ok(Err(identity::Error::Identity {
index: self.identity_index,
message: i18n_embed_fl::fl!(
crate::LANGUAGE_LOADER,
"plugin-err-yk-timed-out",
yubikey_serial = self.serial.to_string(),
),
}))
}
_ => sleep(ONE_SECOND),
}
_ => sleep(ONE_SECOND),
}
}
}
@@ -348,7 +406,7 @@ impl Stub {
}
};
Ok(Ok(Connection {
Ok(Ok(Some(Connection {
yubikey,
cert,
pk,
@@ -357,7 +415,7 @@ impl Stub {
identity_index: self.identity_index,
cached_metadata: None,
last_touch: None,
}))
})))
}
}
+14 -2
View File
@@ -68,7 +68,15 @@ impl RecipientPluginV1 for RecipientPlugin {
let mut yk_errors = vec![];
for stub in &self.yubikeys {
match stub.connect(&mut callbacks)? {
Ok(conn) => yk_recipients.push(conn.recipient().clone()),
Ok(Some(conn)) => yk_recipients.push(conn.recipient().clone()),
Ok(None) => yk_errors.push(recipient::Error::Identity {
index: stub.identity_index,
message: i18n_embed_fl::fl!(
crate::LANGUAGE_LOADER,
"plugin-err-yk-opening",
yubikey_serial = stub.serial.to_string(),
),
}),
Err(e) => yk_errors.push(match e {
identity::Error::Identity { index, message } => {
recipient::Error::Identity { index, message }
@@ -203,7 +211,11 @@ impl IdentityPluginV1 for IdentityPlugin {
for (stub, files) in candidate_stanzas.iter() {
let mut conn = match stub.connect(&mut callbacks)? {
Ok(conn) => conn,
// The user skipped this YubiKey.
Ok(None) => continue,
// We connected to this YubiKey.
Ok(Some(conn)) => conn,
// We failed to connect to this YubiKey.
Err(e) => {
callbacks.error(e)?.unwrap();
continue;