From 0068b1f3435fdc1af03032ccff2c06fc9a773780 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sun, 21 Dec 2025 13:44:19 +0000 Subject: [PATCH] Change default recipient type to `p256tag` Identities generated with older versions of `age-plugin-yubikey` show their legacy recipient in comments; newer identities only show the new recipient. --- CHANGELOG.md | 4 ++ README.md | 12 +++-- i18n/en-US/age_plugin_yubikey.ftl | 2 + src/builder.rs | 6 +-- src/key.rs | 5 +- src/main.rs | 19 +++++++- src/piv_p256/recipient.rs | 12 ----- src/recipient.rs | 17 ++++++- src/util.rs | 78 ++++++++++++++++++++++--------- 9 files changed, 107 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4728cfc..0be8da1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,10 @@ to 0.3.0 are beta releases. - MSRV is now 1.70.0. - Encryption to an identity now uses the preferred recipient type supported for that identity. +- `age-plugin-yubikey` now prints `age1tag1..` recipients in its CLI and + identity files instead of `age1yubikey1..` recipients. The latter is now only + shown in comments for identities generated with `age-plugin-yubikey 0.5.0` or + earlier. ## [0.5.0] - 2024-08-04 ### Fixed diff --git a/README.md b/README.md index 3a7a103..5235051 100644 --- a/README.md +++ b/README.md @@ -108,15 +108,17 @@ standard output: $ age-plugin-yubikey --list ``` -To encrypt files to these YubiKey recipients, ensure that `age-plugin-yubikey` -is accessible in your `PATH`, and then use the recipients with an age client as -normal (e.g. `rage -r age1yubikey1...`). +To encrypt files to these YubiKey recipients, ensure you have a recent version +of an age client, and then use the recipients with it as normal (e.g. +`rage -r age1tag1...`). If this does not work, make `age-plugin-yubikey` +accessible in your `PATH` with the name `age-plugin-tag` and try again. The output of the `--list` command can also be used directly to encrypt files to all recipients (e.g. `age -R filename.txt`). -To decrypt files encrypted to a YubiKey identity, pass the identity file to the -age client as normal (e.g. `rage -d -i yubikey-identity.txt`). +To decrypt files encrypted to a YubiKey identity, ensure that +`age-plugin-yubikey` is accessible in your `PATH`, and then pass the identity +file to the age client as normal (e.g. `rage -d -i yubikey-identity.txt`). ## Advanced topics diff --git a/i18n/en-US/age_plugin_yubikey.ftl b/i18n/en-US/age_plugin_yubikey.ftl index 2174b6c..0baf801 100644 --- a/i18n/en-US/age_plugin_yubikey.ftl +++ b/i18n/en-US/age_plugin_yubikey.ftl @@ -43,6 +43,8 @@ yubikey-metadata = # Created: {$created} # PIN policy: {$pin_policy} # Touch policy: {$touch_policy} +yubikey-legacy-recipient = + # Legacy recipient: {$recipient} yubikey-identity = {$yubikey_metadata} # Recipient: {$recipient} diff --git a/src/builder.rs b/src/builder.rs index 8ff96ba..aa4a534 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -11,7 +11,7 @@ use crate::{ error::Error, fl, key::{self, Stub}, - piv_p256, + native::p256tag, util::{Metadata, POLICY_EXTENSION_OID}, Recipient, BINARY_NAME, USABLE_SLOTS, }; @@ -104,8 +104,8 @@ impl IdentityBuilder { touch_policy, )?; - let recipient = Recipient::PivP256( - piv_p256::Recipient::from_spki(&generated).expect("YubiKey generates a valid pubkey"), + let recipient = Recipient::P256Tag( + p256tag::Recipient::from_spki(&generated).expect("YubiKey generates a valid pubkey"), ); let stub = Stub::new(yubikey.serial(), slot, &recipient); diff --git a/src/key.rs b/src/key.rs index 1714532..e1b6277 100644 --- a/src/key.rs +++ b/src/key.rs @@ -22,7 +22,6 @@ use crate::{ error::Error, fl, native::p256tag, - piv_p256, recipient::TAG_BYTES, util::{otp_serial_prefix, Metadata}, Recipient, IDENTITY_PREFIX, @@ -395,8 +394,8 @@ pub(crate) fn list_slots( match key.slot() { SlotId::Retired(slot) => { // Only P-256 keys are compatible with us. - let recipient = piv_p256::Recipient::from_certificate(key.certificate()) - .map(Recipient::PivP256); + let recipient = + p256tag::Recipient::from_certificate(key.certificate()).map(Recipient::P256Tag); Some((key, slot, recipient)) } _ => None, diff --git a/src/main.rs b/src/main.rs index 5492493..24d7c0e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -298,6 +298,12 @@ fn list(flags: PluginFlags, all: bool) -> Result<(), Error> { all, |_, recipient, metadata| { println!("{metadata}"); + if let Some(legacy_recipient) = recipient.legacy_recipient(&metadata) { + println!( + "{}", + fl!("yubikey-legacy-recipient", recipient = legacy_recipient) + ); + } println!("{recipient}"); }, ) @@ -403,7 +409,7 @@ fn main() -> Result<(), Error> { let (_, cert) = x509_parser::parse_x509_certificate(key.certificate().as_ref()) .unwrap(); - let (name, _) = util::extract_name(&cert, true).unwrap(); + let (name, _) = util::extract_name_and_version(&cert, true).unwrap(); let created = cert .validity() .not_before @@ -613,6 +619,15 @@ fn main() -> Result<(), Error> { Err(e) => return Err(e.into()), }; + let identity = if let Some(legacy_recipient) = recipient.legacy_recipient(&metadata) { + format!( + "{}\n{stub}", + fl!("yubikey-legacy-recipient", recipient = legacy_recipient), + ) + } else { + stub.to_string() + }; + writeln!( file, "{}", @@ -620,7 +635,7 @@ fn main() -> Result<(), Error> { "yubikey-identity", yubikey_metadata = metadata.to_string(), recipient = recipient.to_string(), - identity = stub.to_string(), + identity = identity, ) )?; file.sync_data()?; diff --git a/src/piv_p256/recipient.rs b/src/piv_p256/recipient.rs index a0a661b..0b41efe 100644 --- a/src/piv_p256/recipient.rs +++ b/src/piv_p256/recipient.rs @@ -1,6 +1,5 @@ use age_core::primitives::bech32_encode_to_fmt; use p256::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint}; -use yubikey::{certificate::PublicKeyInfo, Certificate}; use std::fmt; @@ -35,17 +34,6 @@ impl Recipient { } } - pub(crate) fn from_certificate(cert: &Certificate) -> Option { - Self::from_spki(cert.subject_pki()) - } - - pub(crate) fn from_spki(spki: &PublicKeyInfo) -> Option { - match spki { - PublicKeyInfo::EcP256(pubkey) => Self::from_encoded(pubkey), - _ => None, - } - } - /// 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 diff --git a/src/recipient.rs b/src/recipient.rs index 03a2223..2333ddc 100644 --- a/src/recipient.rs +++ b/src/recipient.rs @@ -3,7 +3,7 @@ use std::fmt; use age_core::format::{FileKey, Stanza}; use sha2::{Digest, Sha256}; -use crate::{native::p256tag, piv_p256, PLUGIN_NAME}; +use crate::{native::p256tag, piv_p256, util::Metadata, PLUGIN_NAME}; pub(crate) const TAG_BYTES: usize = 4; @@ -32,6 +32,21 @@ impl Recipient { } } + /// Helper for returning the legacy encoding of this recipient, if any. + pub(crate) fn legacy_recipient(&self, metadata: &Metadata) -> Option { + metadata + .is_pre_p256tag() + .then(|| match self { + Recipient::P256Tag(recipient) => Some( + piv_p256::Recipient::from_bytes(recipient.to_compressed().as_bytes()) + .expect("valid") + .to_string(), + ), + _ => None, + }) + .flatten() + } + /// Returns the static tag for this recipient. pub(crate) fn static_tag(&self) -> [u8; TAG_BYTES] { match self { diff --git a/src/util.rs b/src/util.rs index 36141d7..b723ce9 100644 --- a/src/util.rs +++ b/src/util.rs @@ -71,7 +71,10 @@ pub(crate) fn otp_serial_prefix(serial: Serial) -> String { .collect() } -pub(crate) fn extract_name(cert: &X509Certificate, all: bool) -> Option<(String, bool)> { +pub(crate) fn extract_name_and_version( + cert: &X509Certificate, + all: bool, +) -> Option<(String, Option)> { // Look at Subject Organization to determine if we created this. match cert.subject().iter_organization().next() { Some(org) if org.as_str() == Ok(BINARY_NAME) => { @@ -84,7 +87,16 @@ pub(crate) fn extract_name(cert: &X509Certificate, all: bool) -> Option<(String, .map(|s| s.to_owned()) .unwrap_or_default(); // TODO: This should always be present. - Some((name, true)) + // We store the binary version as an Organizational Unit attribute. + let version = cert + .subject() + .iter_organizational_unit() + .next() + .and_then(|cn| cn.as_str().ok()) + .map(|s| s.to_owned()) + .unwrap_or_default(); // TODO: This should always be present. + + Some((name, Some(version))) } _ => { // Not one of ours, but we've already filtered for compatibility. @@ -95,7 +107,7 @@ pub(crate) fn extract_name(cert: &X509Certificate, all: bool) -> Option<(String, // Display the entire subject. let name = cert.subject().to_string(); - Some((name, false)) + Some((name, None)) } } } @@ -104,6 +116,7 @@ pub(crate) struct Metadata { serial: Serial, slot: RetiredSlotId, name: String, + version: Option, created: String, pub(crate) pin_policy: Option, pub(crate) touch_policy: Option, @@ -149,31 +162,29 @@ impl Metadata { .unwrap_or((None, None)) }; - extract_name(&cert, all) - .map(|(name, ours)| { - if ours { - let (pin_policy, touch_policy) = policies(&cert); - (name, pin_policy, touch_policy) + extract_name_and_version(&cert, all) + .map(|(name, version)| { + let (pin_policy, touch_policy) = if version.is_some() { + policies(&cert) } else { // We can extract the PIN and touch policies via an attestation. This // is slow, but the user has asked for all compatible keys, so... - let (pin_policy, touch_policy) = - yubikey::piv::attest(yubikey, SlotId::Retired(slot)) - .ok() - .and_then(|buf| { - x509_parser::parse_x509_certificate(&buf) - .map(|(_, c)| policies(&c)) - .ok() - }) - .unwrap_or((None, None)); - - (name, pin_policy, touch_policy) - } + yubikey::piv::attest(yubikey, SlotId::Retired(slot)) + .ok() + .and_then(|buf| { + x509_parser::parse_x509_certificate(&buf) + .map(|(_, c)| policies(&c)) + .ok() + }) + .unwrap_or((None, None)) + }; + (name, version, pin_policy, touch_policy) }) - .map(|(name, pin_policy, touch_policy)| Metadata { + .map(|(name, version, pin_policy, touch_policy)| Metadata { serial: yubikey.serial(), slot, name, + version, created: cert .validity() .not_before @@ -183,6 +194,19 @@ impl Metadata { touch_policy, }) } + + /// Returns `true` if this identity was generated with an `age-plugin-yubikey` version + /// before `p256tag` was added (and became the default). + pub(crate) fn is_pre_p256tag(&self) -> bool { + self.version + .as_ref() + .and_then(|version| version.split_once('.')) + .and_then(|(major, rest)| rest.split_once('.').map(|(minor, _)| (major, minor))) + .is_some_and(|(major, minor)| { + // `p256tag` added in v0.6.0 + major == "0" && minor.parse::().is_ok_and(|minor| minor < 6) + }) + } } impl fmt::Display for Metadata { @@ -204,19 +228,29 @@ impl fmt::Display for Metadata { } pub(crate) fn print_identity(stub: Stub, recipient: Recipient, metadata: Metadata) { + let legacy_recipient = recipient.legacy_recipient(&metadata); let recipient = recipient.to_string(); if !console::user_attended() { let recipient = recipient.as_str(); eprintln!("{}", fl!("print-recipient", recipient = recipient)); } + let identity = if let Some(legacy_recipient) = legacy_recipient { + format!( + "{}\n{stub}", + fl!("yubikey-legacy-recipient", recipient = legacy_recipient), + ) + } else { + stub.to_string() + }; + println!( "{}", fl!( "yubikey-identity", yubikey_metadata = metadata.to_string(), recipient = recipient, - identity = stub.to_string(), + identity = identity, ) ); }