Refactors for reusability across supported recipients

This commit is contained in:
Jack Grigg
2025-12-08 02:34:04 +00:00
parent 1f1f257ede
commit 5b44faec44
8 changed files with 135 additions and 62 deletions
+5 -3
View File
@@ -11,9 +11,9 @@ use crate::{
error::Error, error::Error,
fl, fl,
key::{self, Stub}, key::{self, Stub},
p256::Recipient, piv_p256,
util::{Metadata, POLICY_EXTENSION_OID}, util::{Metadata, POLICY_EXTENSION_OID},
BINARY_NAME, USABLE_SLOTS, Recipient, BINARY_NAME, USABLE_SLOTS,
}; };
pub(crate) const DEFAULT_PIN_POLICY: PinPolicy = PinPolicy::Once; pub(crate) const DEFAULT_PIN_POLICY: PinPolicy = PinPolicy::Once;
@@ -104,7 +104,9 @@ impl IdentityBuilder {
touch_policy, touch_policy,
)?; )?;
let recipient = Recipient::from_spki(&generated).expect("YubiKey generates a valid pubkey"); let recipient = Recipient::PivP256(
piv_p256::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);
eprintln!(); eprintln!();
+8 -12
View File
@@ -20,11 +20,10 @@ use yubikey::{
use crate::{ use crate::{
error::Error, error::Error,
fl, fl, piv_p256,
p256::{Recipient, TAG_BYTES}, recipient::TAG_BYTES,
piv_p256,
util::{otp_serial_prefix, Metadata}, util::{otp_serial_prefix, Metadata},
IDENTITY_PREFIX, Recipient, IDENTITY_PREFIX,
}; };
const ONE_SECOND: Duration = Duration::from_secs(1); const ONE_SECOND: Duration = Duration::from_secs(1);
@@ -394,7 +393,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 = Recipient::from_certificate(key.certificate()); let recipient = piv_p256::Recipient::from_certificate(key.certificate())
.map(Recipient::PivP256);
Some((key, slot, recipient)) Some((key, slot, recipient))
} }
_ => None, _ => None,
@@ -449,7 +449,7 @@ impl Stub {
Stub { Stub {
serial, serial,
slot, slot,
tag: recipient.tag(), tag: recipient.static_tag(),
identity_index: 0, identity_index: 0,
} }
} }
@@ -476,10 +476,6 @@ impl Stub {
bytes bytes
} }
pub(crate) fn matches(&self, line: &piv_p256::RecipientLine) -> bool {
self.tag == line.tag
}
/// Returns: /// Returns:
/// - `Ok(Ok(Some(connection)))` if we successfully connected to this YubiKey. /// - `Ok(Ok(Some(connection)))` if we successfully connected to this YubiKey.
/// - `Ok(Ok(None))` if the user told us to skip this YubiKey. /// - `Ok(Ok(None))` if the user told us to skip this YubiKey.
@@ -601,9 +597,9 @@ impl Stub {
let (cert, pk) = match Certificate::read(&mut yubikey, SlotId::Retired(self.slot)) let (cert, pk) = match Certificate::read(&mut yubikey, SlotId::Retired(self.slot))
.ok() .ok()
.and_then(|cert| { .and_then(|cert| {
Recipient::from_certificate(&cert) piv_p256::Recipient::from_certificate(&cert)
.filter(|pk| pk.tag() == self.tag) .filter(|pk| pk.tag() == self.tag)
.map(|pk| (cert, pk)) .map(|pk| (cert, Recipient::PivP256(pk)))
}) { }) {
Some(pk) => pk, Some(pk) => pk,
None => { None => {
+6 -5
View File
@@ -17,16 +17,17 @@ use yubikey::{piv::RetiredSlotId, reader::Context, PinPolicy, Serial, TouchPolic
mod builder; mod builder;
mod error; mod error;
mod key; mod key;
mod p256;
mod piv_p256; mod piv_p256;
mod plugin; mod plugin;
mod util; mod util;
mod recipient;
use recipient::Recipient;
use error::Error; use error::Error;
const PLUGIN_NAME: &str = "yubikey"; const PLUGIN_NAME: &str = "yubikey";
const BINARY_NAME: &str = "age-plugin-yubikey"; const BINARY_NAME: &str = "age-plugin-yubikey";
const RECIPIENT_PREFIX: &str = "age1yubikey";
const IDENTITY_PREFIX: &str = "age-plugin-yubikey-"; const IDENTITY_PREFIX: &str = "age-plugin-yubikey-";
const USABLE_SLOTS: [RetiredSlotId; 20] = [ const USABLE_SLOTS: [RetiredSlotId; 20] = [
@@ -193,7 +194,7 @@ fn generate(flags: PluginFlags) -> Result<(), Error> {
fn print_single( fn print_single(
serial: Option<Serial>, serial: Option<Serial>,
slot: RetiredSlotId, slot: RetiredSlotId,
printer: impl Fn(key::Stub, p256::Recipient, util::Metadata), printer: impl Fn(key::Stub, Recipient, util::Metadata),
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut yubikey = key::open(serial)?; let mut yubikey = key::open(serial)?;
@@ -215,7 +216,7 @@ fn print_multiple(
kind: &str, kind: &str,
serial: Option<Serial>, serial: Option<Serial>,
all: bool, all: bool,
printer: impl Fn(key::Stub, p256::Recipient, util::Metadata), printer: impl Fn(key::Stub, Recipient, util::Metadata),
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut readers = Context::open()?; let mut readers = Context::open()?;
@@ -255,7 +256,7 @@ fn print_details(
kind: &str, kind: &str,
flags: PluginFlags, flags: PluginFlags,
all: bool, all: bool,
printer: impl Fn(key::Stub, p256::Recipient, util::Metadata), printer: impl Fn(key::Stub, Recipient, util::Metadata),
) -> Result<(), Error> { ) -> Result<(), Error> {
if let Some(slot) = flags.slot { if let Some(slot) = flags.slot {
print_single(flags.serial, slot, printer) print_single(flags.serial, slot, printer)
+16 -20
View File
@@ -11,12 +11,14 @@ use p256::{
use rand::rngs::OsRng; use rand::rngs::OsRng;
use sha2::Sha256; use sha2::Sha256;
use crate::{key::Connection, p256::Recipient}; use crate::{key::Connection, recipient::TAG_BYTES, util::base64_arg};
mod recipient;
pub(crate) use recipient::Recipient;
const STANZA_TAG: &str = "piv-p256"; const STANZA_TAG: &str = "piv-p256";
pub(crate) const STANZA_KEY_LABEL: &[u8] = b"piv-p256"; pub(crate) const STANZA_KEY_LABEL: &[u8] = b"piv-p256";
const TAG_BYTES: usize = 4;
const EPK_BYTES: usize = 33; const EPK_BYTES: usize = 33;
const ENCRYPTED_FILE_KEY_BYTES: usize = 32; const ENCRYPTED_FILE_KEY_BYTES: usize = 32;
@@ -81,17 +83,6 @@ impl RecipientLine {
return None; return None;
} }
fn base64_arg<A: AsRef<[u8]>, B: AsMut<[u8]>>(arg: &A, mut buf: B) -> Option<B> {
if arg.as_ref().len() != ((4 * buf.as_mut().len()) + 2) / 3 {
return None;
}
BASE64_STANDARD_NO_PAD
.decode_slice_unchecked(arg, buf.as_mut())
.ok()
.and_then(|len| (len == buf.as_mut().len()).then_some(buf))
}
let (tag, epk_bytes) = match &s.args[..] { let (tag, epk_bytes) = match &s.args[..] {
[tag, epk_bytes] => ( [tag, epk_bytes] => (
base64_arg(tag, [0; TAG_BYTES]), base64_arg(tag, [0; TAG_BYTES]),
@@ -110,15 +101,17 @@ impl RecipientLine {
_ => Err(()), _ => Err(()),
}) })
} }
}
pub(crate) fn wrap_file_key(file_key: &FileKey, pk: &Recipient) -> Self { impl Recipient {
pub(crate) fn wrap_file_key(&self, file_key: &FileKey) -> RecipientLine {
let esk = EphemeralSecret::random(&mut OsRng); let esk = EphemeralSecret::random(&mut OsRng);
let epk = esk.public_key(); let epk = esk.public_key();
let epk_bytes = EphemeralKeyBytes::from_public_key(&epk); let epk_bytes = EphemeralKeyBytes::from_public_key(&epk);
let shared_secret = esk.diffie_hellman(pk.public_key()); let shared_secret = esk.diffie_hellman(self.public_key());
let salt = salt(&epk_bytes, pk); let salt = salt(&epk_bytes, self);
let enc_key = { let enc_key = {
let mut okm = [0; 32]; let mut okm = [0; 32];
@@ -136,21 +129,24 @@ impl RecipientLine {
}; };
RecipientLine { RecipientLine {
tag: pk.tag(), tag: self.tag(),
epk_bytes, epk_bytes,
encrypted_file_key, encrypted_file_key,
} }
} }
}
impl RecipientLine {
pub(crate) fn unwrap_file_key(&self, conn: &mut Connection) -> Result<FileKey, ()> { pub(crate) fn unwrap_file_key(&self, conn: &mut Connection) -> Result<FileKey, ()> {
assert_eq!(self.tag, conn.recipient().tag()); let crate::recipient::Recipient::PivP256(recipient) = conn.recipient();
assert_eq!(self.tag, recipient.tag());
let salt = salt(&self.epk_bytes, recipient);
// The YubiKey API for performing scalar multiplication takes the point in its // The YubiKey API for performing scalar multiplication takes the point in its
// uncompressed SEC-1 encoding. // uncompressed SEC-1 encoding.
let shared_secret = conn.p256_ecdh(self.epk_bytes.decompress().as_bytes())?; let shared_secret = conn.p256_ecdh(self.epk_bytes.decompress().as_bytes())?;
let salt = salt(&self.epk_bytes, conn.recipient());
let enc_key = hkdf(&salt, STANZA_KEY_LABEL, shared_secret.as_ref()); let enc_key = hkdf(&salt, STANZA_KEY_LABEL, shared_secret.as_ref());
// A failure to decrypt is fatal, because we assume that we won't // A failure to decrypt is fatal, because we assume that we won't
+3 -5
View File
@@ -1,13 +1,12 @@
use bech32::{ToBase32, Variant}; use bech32::{ToBase32, Variant};
use p256::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint}; use p256::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint};
use sha2::{Digest, Sha256};
use yubikey::{certificate::PublicKeyInfo, Certificate}; use yubikey::{certificate::PublicKeyInfo, Certificate};
use std::fmt; use std::fmt;
use crate::RECIPIENT_PREFIX; use crate::recipient::{static_tag, TAG_BYTES};
pub(crate) const TAG_BYTES: usize = 4; const RECIPIENT_PREFIX: &str = "age1yubikey";
/// Wrapper around a compressed secp256r1 curve point. /// Wrapper around a compressed secp256r1 curve point.
#[derive(Clone)] #[derive(Clone)]
@@ -69,8 +68,7 @@ impl Recipient {
} }
pub(crate) fn tag(&self) -> [u8; TAG_BYTES] { pub(crate) fn tag(&self) -> [u8; TAG_BYTES] {
let tag = Sha256::digest(self.to_encoded().as_bytes()); static_tag(self.to_encoded().as_bytes())
(&tag[0..TAG_BYTES]).try_into().expect("length is correct")
} }
/// Exposes the wrapped public key. /// Exposes the wrapped public key.
+31 -13
View File
@@ -7,7 +7,7 @@ use age_plugin::{
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::io; use std::io;
use crate::{fl, key, p256::Recipient, piv_p256, PLUGIN_NAME}; use crate::{fl, key, piv_p256, Recipient, PLUGIN_NAME};
pub(crate) struct Handler; pub(crate) struct Handler;
@@ -37,11 +37,7 @@ impl RecipientPluginV1 for RecipientPlugin {
plugin_name: &str, plugin_name: &str,
bytes: &[u8], bytes: &[u8],
) -> Result<(), recipient::Error> { ) -> Result<(), recipient::Error> {
if let Some(pk) = if plugin_name == PLUGIN_NAME { if let Some(pk) = Recipient::from_bytes(plugin_name, bytes) {
Recipient::from_bytes(bytes)
} else {
None
} {
self.recipients.push(pk); self.recipients.push(pk);
Ok(()) Ok(())
} else { } else {
@@ -114,7 +110,7 @@ impl RecipientPluginV1 for RecipientPlugin {
self.recipients self.recipients
.iter() .iter()
.chain(yk_recipients.iter()) .chain(yk_recipients.iter())
.map(|pk| piv_p256::RecipientLine::wrap_file_key(&file_key, pk).into()) .map(|pk| pk.wrap_file_key(&file_key))
.collect() .collect()
}) })
.collect()) .collect())
@@ -159,16 +155,16 @@ impl IdentityPluginV1 for IdentityPlugin {
let mut file_keys = HashMap::with_capacity(files.len()); let mut file_keys = HashMap::with_capacity(files.len());
// Filter to files / stanzas for which we have matching YubiKeys // Filter to files / stanzas for which we have matching YubiKeys
let mut candidate_stanzas: Vec<(&key::Stub, HashMap<usize, Vec<piv_p256::RecipientLine>>)> = let mut candidate_stanzas: Vec<(&key::Stub, HashMap<usize, Vec<SupportedStanza>>)> = self
self.yubikeys .yubikeys
.iter() .iter()
.map(|stub| (stub, HashMap::new())) .map(|stub| (stub, HashMap::new()))
.collect(); .collect();
for (file, stanzas) in files.iter().enumerate() { for (file, stanzas) in files.into_iter().enumerate() {
for (stanza_index, stanza) in stanzas.iter().enumerate() { for (stanza_index, stanza) in stanzas.into_iter().enumerate() {
match ( match (
piv_p256::RecipientLine::from_stanza(stanza).map(|res| { SupportedStanza::parse(stanza).map(|res| {
res.map_err(|_| identity::Error::Stanza { res.map_err(|_| identity::Error::Stanza {
file_index: file, file_index: file,
stanza_index, stanza_index,
@@ -182,7 +178,7 @@ impl IdentityPluginV1 for IdentityPlugin {
// A line will match at most one YubiKey. // A line will match at most one YubiKey.
if let Some(files) = if let Some(files) =
candidate_stanzas.iter_mut().find_map(|(stub, files)| { candidate_stanzas.iter_mut().find_map(|(stub, files)| {
if stub.matches(&line) { if line.matches_stub(stub) {
Some(files) Some(files)
} else { } else {
None None
@@ -274,3 +270,25 @@ impl IdentityPluginV1 for IdentityPlugin {
Ok(file_keys) Ok(file_keys)
} }
} }
enum SupportedStanza {
PivP256(piv_p256::RecipientLine),
}
impl SupportedStanza {
fn parse(stanza: Stanza) -> Option<Result<Self, ()>> {
piv_p256::RecipientLine::from_stanza(&stanza).map(|res| res.map(Self::PivP256))
}
pub(crate) fn matches_stub(&self, stub: &key::Stub) -> bool {
match self {
SupportedStanza::PivP256(line) => stub.tag == line.tag,
}
}
pub(crate) fn unwrap_file_key(&self, conn: &mut key::Connection) -> Result<FileKey, ()> {
match self {
SupportedStanza::PivP256(line) => line.unwrap_file_key(conn),
}
}
}
+50
View File
@@ -0,0 +1,50 @@
use std::fmt;
use age_core::format::{FileKey, Stanza};
use sha2::{Digest, Sha256};
use crate::{piv_p256, PLUGIN_NAME};
pub(crate) const TAG_BYTES: usize = 4;
#[derive(Clone, Debug)]
pub(crate) enum Recipient {
PivP256(piv_p256::Recipient),
}
impl fmt::Display for Recipient {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Recipient::PivP256(recipient) => recipient.fmt(f),
}
}
}
impl Recipient {
/// Attempts to parse a supported YubiKey recipient.
pub(crate) fn from_bytes(plugin_name: &str, bytes: &[u8]) -> Option<Self> {
match plugin_name {
PLUGIN_NAME => piv_p256::Recipient::from_bytes(bytes).map(Self::PivP256),
_ => None,
}
}
/// Returns the static tag for this recipient.
pub(crate) fn static_tag(&self) -> [u8; TAG_BYTES] {
match self {
Recipient::PivP256(recipient) => recipient.tag(),
}
}
pub(crate) fn wrap_file_key(&self, file_key: &FileKey) -> Stanza {
match self {
Recipient::PivP256(recipient) => recipient.wrap_file_key(file_key).into(),
}
}
}
pub(crate) fn static_tag(pk: &[u8]) -> [u8; TAG_BYTES] {
Sha256::digest(pk)[0..TAG_BYTES]
.try_into()
.expect("length is correct")
}
+13 -1
View File
@@ -1,6 +1,7 @@
use std::fmt; use std::fmt;
use std::iter; use std::iter;
use base64::{prelude::BASE64_STANDARD_NO_PAD, Engine};
use x509_parser::{certificate::X509Certificate, der_parser::oid::Oid}; use x509_parser::{certificate::X509Certificate, der_parser::oid::Oid};
use yubikey::{ use yubikey::{
piv::{RetiredSlotId, SlotId}, piv::{RetiredSlotId, SlotId},
@@ -8,7 +9,7 @@ use yubikey::{
}; };
use crate::fl; use crate::fl;
use crate::{error::Error, key::Stub, p256::Recipient, BINARY_NAME, USABLE_SLOTS}; use crate::{error::Error, key::Stub, Recipient, BINARY_NAME, USABLE_SLOTS};
pub(crate) const POLICY_EXTENSION_OID: &[u64] = &[1, 3, 6, 1, 4, 1, 41482, 3, 8]; pub(crate) const POLICY_EXTENSION_OID: &[u64] = &[1, 3, 6, 1, 4, 1, 41482, 3, 8];
@@ -219,3 +220,14 @@ pub(crate) fn print_identity(stub: Stub, recipient: Recipient, metadata: Metadat
) )
); );
} }
pub(crate) fn base64_arg<A: AsRef<[u8]>, B: AsMut<[u8]>>(arg: &A, mut buf: B) -> Option<B> {
if arg.as_ref().len() != ((4 * buf.as_mut().len()) + 2) / 3 {
return None;
}
BASE64_STANDARD_NO_PAD
.decode_slice_unchecked(arg, buf.as_mut())
.ok()
.and_then(|len| (len == buf.as_mut().len()).then_some(buf))
}