Merge pull request #224 from str4d/detect-critical-extensions

Reject identities with unrecognised critical extensions
This commit is contained in:
Jack Grigg
2026-04-08 05:05:25 +01:00
committed by GitHub
4 changed files with 41 additions and 9 deletions
+6
View File
@@ -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 shown in comments for identities generated with `age-plugin-yubikey 0.5.0` or
earlier. 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 ## [0.5.0] - 2024-08-04
### Fixed ### Fixed
- `age-plugin-yubikey` can now be compiled with Rust 1.80 and above. - `age-plugin-yubikey` can now be compiled with Rust 1.80 and above.
Generated
+1 -1
View File
@@ -84,7 +84,7 @@ dependencies = [
[[package]] [[package]]
name = "age-plugin-yubikey" name = "age-plugin-yubikey"
version = "0.5.0" version = "0.5.1"
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.5.0" version = "0.5.1"
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"
+33 -7
View File
@@ -11,6 +11,7 @@ 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 x509_parser::der_parser::oid::Oid;
use yubikey::{ use yubikey::{
certificate::Certificate, certificate::Certificate,
piv::{decrypt_data, AlgorithmId, RetiredSlotId, SlotId}, piv::{decrypt_data, AlgorithmId, RetiredSlotId, SlotId},
@@ -23,13 +24,16 @@ use crate::{
fl, fl,
native::p256tag, native::p256tag,
recipient::TAG_BYTES, recipient::TAG_BYTES,
util::{otp_serial_prefix, Metadata}, util::{otp_serial_prefix, Metadata, POLICY_EXTENSION_OID},
Recipient, IDENTITY_PREFIX, Recipient, IDENTITY_PREFIX,
}; };
const ONE_SECOND: Duration = Duration::from_secs(1); const ONE_SECOND: Duration = Duration::from_secs(1);
const FIFTEEN_SECONDS: Duration = Duration::from_secs(15); 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 { pub(crate) fn is_connected(reader: Reader) -> bool {
filter_connected(&reader) filter_connected(&reader)
} }
@@ -384,6 +388,30 @@ pub(crate) fn manage(yubikey: &mut YubiKey) -> Result<(), Error> {
Ok(()) Ok(())
} }
/// Parses the certificate to identify the preferred recipient type it corresponds to.
pub(crate) fn identify_recipient(cert: &Certificate) -> Option<Recipient> {
let known_oids = KNOWN_OIDS
.iter()
.map(|oid| Oid::from(oid).unwrap())
.collect::<Vec<_>>();
// 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 /// Returns an iterator of keys that are occupying plugin-compatible slots, along with the
/// corresponding recipient if the key is compatible with this plugin. /// corresponding recipient if the key is compatible with this plugin.
pub(crate) fn list_slots( pub(crate) fn list_slots(
@@ -393,9 +421,7 @@ pub(crate) fn list_slots(
// We only use the retired slots. // We only use the retired slots.
match key.slot() { match key.slot() {
SlotId::Retired(slot) => { SlotId::Retired(slot) => {
// Only P-256 keys are compatible with us. let recipient = identify_recipient(key.certificate());
let recipient =
p256tag::Recipient::from_certificate(key.certificate()).map(Recipient::P256Tag);
Some((key, slot, recipient)) Some((key, slot, recipient))
} }
_ => None, _ => None,
@@ -594,9 +620,9 @@ impl Stub {
.ok() .ok()
.and_then(|cert| { .and_then(|cert| {
// Parse as the preferred recipient for each identity type. // Parse as the preferred recipient for each identity type.
p256tag::Recipient::from_certificate(&cert) identify_recipient(&cert)
.filter(|pk| pk.static_tag() == self.tag) .filter(|recipient| recipient.static_tag() == self.tag)
.map(|pk| (cert, Recipient::P256Tag(pk))) .map(|r| (cert, r))
}) { }) {
Some(pk) => pk, Some(pk) => pk,
None => { None => {