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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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()?;
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user