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.
This commit is contained in:
Jack Grigg
2025-12-21 13:44:19 +00:00
parent 971d63957c
commit 0068b1f343
9 changed files with 107 additions and 48 deletions
+4
View File
@@ -18,6 +18,10 @@ to 0.3.0 are beta releases.
- MSRV is now 1.70.0. - MSRV is now 1.70.0.
- Encryption to an identity now uses the preferred recipient type supported for - Encryption to an identity now uses the preferred recipient type supported for
that identity. 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 ## [0.5.0] - 2024-08-04
### Fixed ### Fixed
+7 -5
View File
@@ -108,15 +108,17 @@ standard output:
$ age-plugin-yubikey --list $ age-plugin-yubikey --list
``` ```
To encrypt files to these YubiKey recipients, ensure that `age-plugin-yubikey` To encrypt files to these YubiKey recipients, ensure you have a recent version
is accessible in your `PATH`, and then use the recipients with an age client as of an age client, and then use the recipients with it as normal (e.g.
normal (e.g. `rage -r age1yubikey1...`). `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 The output of the `--list` command can also be used directly to encrypt files to
all recipients (e.g. `age -R filename.txt`). all recipients (e.g. `age -R filename.txt`).
To decrypt files encrypted to a YubiKey identity, pass the identity file to the To decrypt files encrypted to a YubiKey identity, ensure that
age client as normal (e.g. `rage -d -i yubikey-identity.txt`). `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 ## Advanced topics
+2
View File
@@ -43,6 +43,8 @@ yubikey-metadata =
# Created: {$created} # Created: {$created}
# PIN policy: {$pin_policy} # PIN policy: {$pin_policy}
# Touch policy: {$touch_policy} # Touch policy: {$touch_policy}
yubikey-legacy-recipient =
# Legacy recipient: {$recipient}
yubikey-identity = yubikey-identity =
{$yubikey_metadata} {$yubikey_metadata}
# Recipient: {$recipient} # Recipient: {$recipient}
+3 -3
View File
@@ -11,7 +11,7 @@ use crate::{
error::Error, error::Error,
fl, fl,
key::{self, Stub}, key::{self, Stub},
piv_p256, native::p256tag,
util::{Metadata, POLICY_EXTENSION_OID}, util::{Metadata, POLICY_EXTENSION_OID},
Recipient, BINARY_NAME, USABLE_SLOTS, Recipient, BINARY_NAME, USABLE_SLOTS,
}; };
@@ -104,8 +104,8 @@ impl IdentityBuilder {
touch_policy, touch_policy,
)?; )?;
let recipient = Recipient::PivP256( let recipient = Recipient::P256Tag(
piv_p256::Recipient::from_spki(&generated).expect("YubiKey generates a valid pubkey"), p256tag::Recipient::from_spki(&generated).expect("YubiKey generates a valid pubkey"),
); );
let stub = Stub::new(yubikey.serial(), slot, &recipient); let stub = Stub::new(yubikey.serial(), slot, &recipient);
+2 -3
View File
@@ -22,7 +22,6 @@ use crate::{
error::Error, error::Error,
fl, fl,
native::p256tag, native::p256tag,
piv_p256,
recipient::TAG_BYTES, recipient::TAG_BYTES,
util::{otp_serial_prefix, Metadata}, util::{otp_serial_prefix, Metadata},
Recipient, IDENTITY_PREFIX, Recipient, IDENTITY_PREFIX,
@@ -395,8 +394,8 @@ pub(crate) fn list_slots(
match key.slot() { match key.slot() {
SlotId::Retired(slot) => { SlotId::Retired(slot) => {
// Only P-256 keys are compatible with us. // Only P-256 keys are compatible with us.
let recipient = piv_p256::Recipient::from_certificate(key.certificate()) let recipient =
.map(Recipient::PivP256); p256tag::Recipient::from_certificate(key.certificate()).map(Recipient::P256Tag);
Some((key, slot, recipient)) Some((key, slot, recipient))
} }
_ => None, _ => None,
+17 -2
View File
@@ -298,6 +298,12 @@ fn list(flags: PluginFlags, all: bool) -> Result<(), Error> {
all, all,
|_, recipient, metadata| { |_, recipient, metadata| {
println!("{metadata}"); println!("{metadata}");
if let Some(legacy_recipient) = recipient.legacy_recipient(&metadata) {
println!(
"{}",
fl!("yubikey-legacy-recipient", recipient = legacy_recipient)
);
}
println!("{recipient}"); println!("{recipient}");
}, },
) )
@@ -403,7 +409,7 @@ fn main() -> Result<(), Error> {
let (_, cert) = let (_, cert) =
x509_parser::parse_x509_certificate(key.certificate().as_ref()) x509_parser::parse_x509_certificate(key.certificate().as_ref())
.unwrap(); .unwrap();
let (name, _) = util::extract_name(&cert, true).unwrap(); let (name, _) = util::extract_name_and_version(&cert, true).unwrap();
let created = cert let created = cert
.validity() .validity()
.not_before .not_before
@@ -613,6 +619,15 @@ fn main() -> Result<(), Error> {
Err(e) => return Err(e.into()), 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!( writeln!(
file, file,
"{}", "{}",
@@ -620,7 +635,7 @@ fn main() -> Result<(), Error> {
"yubikey-identity", "yubikey-identity",
yubikey_metadata = metadata.to_string(), yubikey_metadata = metadata.to_string(),
recipient = recipient.to_string(), recipient = recipient.to_string(),
identity = stub.to_string(), identity = identity,
) )
)?; )?;
file.sync_data()?; file.sync_data()?;
-12
View File
@@ -1,6 +1,5 @@
use age_core::primitives::bech32_encode_to_fmt; use age_core::primitives::bech32_encode_to_fmt;
use p256::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint}; use p256::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint};
use yubikey::{certificate::PublicKeyInfo, Certificate};
use std::fmt; use std::fmt;
@@ -35,17 +34,6 @@ impl Recipient {
} }
} }
pub(crate) fn from_certificate(cert: &Certificate) -> Option<Self> {
Self::from_spki(cert.subject_pki())
}
pub(crate) fn from_spki(spki: &PublicKeyInfo) -> Option<Self> {
match spki {
PublicKeyInfo::EcP256(pubkey) => Self::from_encoded(pubkey),
_ => None,
}
}
/// Attempts to parse a valid YubiKey recipient from its SEC-1 encoding. /// 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 /// This accepts both compressed (as used by the plugin) and uncompressed (as used in
+16 -1
View File
@@ -3,7 +3,7 @@ use std::fmt;
use age_core::format::{FileKey, Stanza}; use age_core::format::{FileKey, Stanza};
use sha2::{Digest, Sha256}; 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; 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<String> {
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. /// Returns the static tag for this recipient.
pub(crate) fn static_tag(&self) -> [u8; TAG_BYTES] { pub(crate) fn static_tag(&self) -> [u8; TAG_BYTES] {
match self { match self {
+49 -15
View File
@@ -71,7 +71,10 @@ pub(crate) fn otp_serial_prefix(serial: Serial) -> String {
.collect() .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<String>)> {
// Look at Subject Organization to determine if we created this. // Look at Subject Organization to determine if we created this.
match cert.subject().iter_organization().next() { match cert.subject().iter_organization().next() {
Some(org) if org.as_str() == Ok(BINARY_NAME) => { 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()) .map(|s| s.to_owned())
.unwrap_or_default(); // TODO: This should always be present. .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. // 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. // Display the entire subject.
let name = cert.subject().to_string(); let name = cert.subject().to_string();
Some((name, false)) Some((name, None))
} }
} }
} }
@@ -104,6 +116,7 @@ pub(crate) struct Metadata {
serial: Serial, serial: Serial,
slot: RetiredSlotId, slot: RetiredSlotId,
name: String, name: String,
version: Option<String>,
created: String, created: String,
pub(crate) pin_policy: Option<PinPolicy>, pub(crate) pin_policy: Option<PinPolicy>,
pub(crate) touch_policy: Option<TouchPolicy>, pub(crate) touch_policy: Option<TouchPolicy>,
@@ -149,15 +162,13 @@ impl Metadata {
.unwrap_or((None, None)) .unwrap_or((None, None))
}; };
extract_name(&cert, all) extract_name_and_version(&cert, all)
.map(|(name, ours)| { .map(|(name, version)| {
if ours { let (pin_policy, touch_policy) = if version.is_some() {
let (pin_policy, touch_policy) = policies(&cert); policies(&cert)
(name, pin_policy, touch_policy)
} else { } else {
// We can extract the PIN and touch policies via an attestation. This // We can extract the PIN and touch policies via an attestation. This
// is slow, but the user has asked for all compatible keys, so... // is slow, but the user has asked for all compatible keys, so...
let (pin_policy, touch_policy) =
yubikey::piv::attest(yubikey, SlotId::Retired(slot)) yubikey::piv::attest(yubikey, SlotId::Retired(slot))
.ok() .ok()
.and_then(|buf| { .and_then(|buf| {
@@ -165,15 +176,15 @@ impl Metadata {
.map(|(_, c)| policies(&c)) .map(|(_, c)| policies(&c))
.ok() .ok()
}) })
.unwrap_or((None, None)); .unwrap_or((None, None))
};
(name, pin_policy, touch_policy) (name, version, pin_policy, touch_policy)
}
}) })
.map(|(name, pin_policy, touch_policy)| Metadata { .map(|(name, version, pin_policy, touch_policy)| Metadata {
serial: yubikey.serial(), serial: yubikey.serial(),
slot, slot,
name, name,
version,
created: cert created: cert
.validity() .validity()
.not_before .not_before
@@ -183,6 +194,19 @@ impl Metadata {
touch_policy, 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::<u8>().is_ok_and(|minor| minor < 6)
})
}
} }
impl fmt::Display for Metadata { 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) { pub(crate) fn print_identity(stub: Stub, recipient: Recipient, metadata: Metadata) {
let legacy_recipient = recipient.legacy_recipient(&metadata);
let recipient = recipient.to_string(); let recipient = recipient.to_string();
if !console::user_attended() { if !console::user_attended() {
let recipient = recipient.as_str(); let recipient = recipient.as_str();
eprintln!("{}", fl!("print-recipient", recipient = recipient)); 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!( println!(
"{}", "{}",
fl!( fl!(
"yubikey-identity", "yubikey-identity",
yubikey_metadata = metadata.to_string(), yubikey_metadata = metadata.to_string(),
recipient = recipient, recipient = recipient,
identity = stub.to_string(), identity = identity,
) )
); );
} }