diff --git a/CHANGELOG.md b/CHANGELOG.md index 0be8da1..6b3068a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,12 @@ to 0.3.0 are beta releases. shown in comments for identities generated with `age-plugin-yubikey 0.5.0` or earlier. +## [0.3.4], [0.4.1], [0.5.1] - 2026-04-08 +### Fixed +- `age-plugin-yubikey` now completely ignores any identity that has unrecognised + critical extensions in its certificate, to ensure it doesn't misuse a newer + identity type. + ## [0.5.0] - 2024-08-04 ### Fixed - `age-plugin-yubikey` can now be compiled with Rust 1.80 and above. diff --git a/Cargo.lock b/Cargo.lock index 41d8438..e0d9b55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,7 +84,7 @@ dependencies = [ [[package]] name = "age-plugin-yubikey" -version = "0.5.0" +version = "0.5.1" dependencies = [ "age-core", "age-plugin", diff --git a/Cargo.toml b/Cargo.toml index 141b4fd..22564e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "age-plugin-yubikey" description = "YubiKey plugin for age clients" -version = "0.5.0" +version = "0.5.1" authors = ["Jack Grigg "] repository = "https://github.com/str4d/age-plugin-yubikey" readme = "README.md" diff --git a/src/key.rs b/src/key.rs index e1b6277..b5d5e28 100644 --- a/src/key.rs +++ b/src/key.rs @@ -11,6 +11,7 @@ use std::io; use std::iter; use std::thread::sleep; use std::time::{Duration, Instant, SystemTime}; +use x509_parser::der_parser::oid::Oid; use yubikey::{ certificate::Certificate, piv::{decrypt_data, AlgorithmId, RetiredSlotId, SlotId}, @@ -23,13 +24,16 @@ use crate::{ fl, native::p256tag, recipient::TAG_BYTES, - util::{otp_serial_prefix, Metadata}, + util::{otp_serial_prefix, Metadata, POLICY_EXTENSION_OID}, Recipient, IDENTITY_PREFIX, }; const ONE_SECOND: Duration = Duration::from_secs(1); const FIFTEEN_SECONDS: Duration = Duration::from_secs(15); +/// The set of OIDs that we understand and use when parsing YubiKey slot certificates. +const KNOWN_OIDS: &[&[u64]] = &[POLICY_EXTENSION_OID]; + pub(crate) fn is_connected(reader: Reader) -> bool { filter_connected(&reader) } @@ -384,6 +388,30 @@ pub(crate) fn manage(yubikey: &mut YubiKey) -> Result<(), Error> { Ok(()) } +/// Parses the certificate to identify the preferred recipient type it corresponds to. +pub(crate) fn identify_recipient(cert: &Certificate) -> Option { + let known_oids = KNOWN_OIDS + .iter() + .map(|oid| Oid::from(oid).unwrap()) + .collect::>(); + + // If the certificate contains any unrecognised critical extensions, reject it: we + // don't know how to correctly use the identity. In particular, some identities store + // parts of their private key material in certificate extensions to work around + // hardware limitations. Not understanding these extensions could lead to encrypting + // with the wrong protocol and violating security assumptions. + let (_, c) = x509_parser::parse_x509_certificate(cert.as_ref()).ok()?; + if c.tbs_certificate + .extensions() + .iter() + .any(|ext| ext.critical && !known_oids.contains(&ext.oid)) + { + return None; + } + + p256tag::Recipient::from_certificate(cert).map(Recipient::P256Tag) +} + /// 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( @@ -393,9 +421,7 @@ pub(crate) fn list_slots( // We only use the retired slots. match key.slot() { SlotId::Retired(slot) => { - // Only P-256 keys are compatible with us. - let recipient = - p256tag::Recipient::from_certificate(key.certificate()).map(Recipient::P256Tag); + let recipient = identify_recipient(key.certificate()); Some((key, slot, recipient)) } _ => None, @@ -594,9 +620,9 @@ impl Stub { .ok() .and_then(|cert| { // Parse as the preferred recipient for each identity type. - p256tag::Recipient::from_certificate(&cert) - .filter(|pk| pk.static_tag() == self.tag) - .map(|pk| (cert, Recipient::P256Tag(pk))) + identify_recipient(&cert) + .filter(|recipient| recipient.static_tag() == self.tag) + .map(|r| (cert, r)) }) { Some(pk) => pk, None => {