Merge pull request #213 from str4d/p256tag
Add support for `p256tag` recipient type
This commit is contained in:
@@ -8,8 +8,20 @@ to 0.3.0 are beta releases.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Support for the native non-hybrid tagged recipient type (`age1tag1..`).
|
||||||
|
- Encryption requires making the `age-plugin-yubikey` binary available on the
|
||||||
|
`PATH` as `age-plugin-tag`, or upgrading to a client version that builds in
|
||||||
|
support for this new native recipient type.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- MSRV is now 1.70.0.
|
- 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
|
## [0.5.0] - 2024-08-04
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
Generated
+96
-2
@@ -27,16 +27,42 @@ dependencies = [
|
|||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aes"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cipher",
|
||||||
|
"cpufeatures",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aes-gcm"
|
||||||
|
version = "0.10.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
|
||||||
|
dependencies = [
|
||||||
|
"aead",
|
||||||
|
"aes",
|
||||||
|
"cipher",
|
||||||
|
"ctr",
|
||||||
|
"ghash",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "age-core"
|
name = "age-core"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
source = "git+https://github.com/str4d/rage.git?rev=5e530a3a6aad9e189e26903bc8114e2da526b4b5#5e530a3a6aad9e189e26903bc8114e2da526b4b5"
|
source = "git+https://github.com/str4d/rage.git?rev=e08c450aa5d7b1cc5706094080c0042ddd60aaf7#e08c450aa5d7b1cc5706094080c0042ddd60aaf7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bech32",
|
"bech32",
|
||||||
"chacha20poly1305",
|
"chacha20poly1305",
|
||||||
"cookie-factory",
|
"cookie-factory",
|
||||||
"hkdf",
|
"hkdf",
|
||||||
|
"hpke",
|
||||||
"io_tee",
|
"io_tee",
|
||||||
"nom 8.0.0",
|
"nom 8.0.0",
|
||||||
"rand",
|
"rand",
|
||||||
@@ -48,7 +74,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "age-plugin"
|
name = "age-plugin"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
source = "git+https://github.com/str4d/rage.git?rev=5e530a3a6aad9e189e26903bc8114e2da526b4b5#5e530a3a6aad9e189e26903bc8114e2da526b4b5"
|
source = "git+https://github.com/str4d/rage.git?rev=e08c450aa5d7b1cc5706094080c0042ddd60aaf7#e08c450aa5d7b1cc5706094080c0042ddd60aaf7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"age-core",
|
"age-core",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
@@ -70,6 +96,8 @@ dependencies = [
|
|||||||
"flate2",
|
"flate2",
|
||||||
"gumdrop",
|
"gumdrop",
|
||||||
"hex",
|
"hex",
|
||||||
|
"hkdf",
|
||||||
|
"hpke",
|
||||||
"i18n-embed",
|
"i18n-embed",
|
||||||
"i18n-embed-fl",
|
"i18n-embed-fl",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
@@ -449,9 +477,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array",
|
"generic-array",
|
||||||
|
"rand_core",
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ctr"
|
||||||
|
version = "0.9.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
|
||||||
|
dependencies = [
|
||||||
|
"cipher",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dashmap"
|
name = "dashmap"
|
||||||
version = "6.1.0"
|
version = "6.1.0"
|
||||||
@@ -855,6 +893,16 @@ dependencies = [
|
|||||||
"wasi",
|
"wasi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ghash"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
|
||||||
|
dependencies = [
|
||||||
|
"opaque-debug",
|
||||||
|
"polyval",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gimli"
|
name = "gimli"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
@@ -968,6 +1016,26 @@ dependencies = [
|
|||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hpke"
|
||||||
|
version = "0.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4917627a14198c3603282c5158b815ad5534795451d3c074b53cf3cee0960b11"
|
||||||
|
dependencies = [
|
||||||
|
"aead",
|
||||||
|
"aes-gcm",
|
||||||
|
"chacha20poly1305",
|
||||||
|
"digest",
|
||||||
|
"generic-array",
|
||||||
|
"hkdf",
|
||||||
|
"hmac",
|
||||||
|
"p256",
|
||||||
|
"rand_core",
|
||||||
|
"sha2",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "0.2.12"
|
version = "0.2.12"
|
||||||
@@ -1710,6 +1778,18 @@ dependencies = [
|
|||||||
"universal-hash",
|
"universal-hash",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "polyval"
|
||||||
|
version = "0.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"opaque-debug",
|
||||||
|
"universal-hash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "powerfmt"
|
name = "powerfmt"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -3087,3 +3167,17 @@ name = "zeroize"
|
|||||||
version = "1.8.1"
|
version = "1.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
||||||
|
dependencies = [
|
||||||
|
"zeroize_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize_derive"
|
||||||
|
version = "1.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.86",
|
||||||
|
]
|
||||||
|
|||||||
+4
-2
@@ -31,6 +31,8 @@ dialoguer = { version = "0.11", default-features = false, features = ["password"
|
|||||||
env_logger = "0.10"
|
env_logger = "0.10"
|
||||||
gumdrop = "0.8"
|
gumdrop = "0.8"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
|
hkdf = "0.12"
|
||||||
|
hpke = { version = "0.12", default-features = false, features = ["alloc", "p256"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
p256 = { version = "0.13", features = ["ecdh"] }
|
p256 = { version = "0.13", features = ["ecdh"] }
|
||||||
pcsc = "2.4"
|
pcsc = "2.4"
|
||||||
@@ -58,5 +60,5 @@ test-with = "0.11"
|
|||||||
which = "5"
|
which = "5"
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
age-core = { git = "https://github.com/str4d/rage.git", rev = "5e530a3a6aad9e189e26903bc8114e2da526b4b5" }
|
age-core = { git = "https://github.com/str4d/rage.git", rev = "e08c450aa5d7b1cc5706094080c0042ddd60aaf7" }
|
||||||
age-plugin = { git = "https://github.com/str4d/rage.git", rev = "5e530a3a6aad9e189e26903bc8114e2da526b4b5" }
|
age-plugin = { git = "https://github.com/str4d/rage.git", rev = "e08c450aa5d7b1cc5706094080c0042ddd60aaf7" }
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
+13
-6
@@ -20,7 +20,8 @@ use yubikey::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::Error,
|
error::Error,
|
||||||
fl, piv_p256,
|
fl,
|
||||||
|
native::p256tag,
|
||||||
recipient::TAG_BYTES,
|
recipient::TAG_BYTES,
|
||||||
util::{otp_serial_prefix, Metadata},
|
util::{otp_serial_prefix, Metadata},
|
||||||
Recipient, IDENTITY_PREFIX,
|
Recipient, IDENTITY_PREFIX,
|
||||||
@@ -393,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,
|
||||||
@@ -592,9 +593,10 @@ 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| {
|
||||||
piv_p256::Recipient::from_certificate(&cert)
|
// Parse as the preferred recipient for each identity type.
|
||||||
.filter(|pk| pk.tag() == self.tag)
|
p256tag::Recipient::from_certificate(&cert)
|
||||||
.map(|pk| (cert, Recipient::PivP256(pk)))
|
.filter(|pk| pk.static_tag() == self.tag)
|
||||||
|
.map(|pk| (cert, Recipient::P256Tag(pk)))
|
||||||
}) {
|
}) {
|
||||||
Some(pk) => pk,
|
Some(pk) => pk,
|
||||||
None => {
|
None => {
|
||||||
@@ -628,10 +630,15 @@ pub(crate) struct Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Connection {
|
impl Connection {
|
||||||
|
/// Returns the preferred recipient for encrypting to this identity.
|
||||||
pub(crate) fn recipient(&self) -> &Recipient {
|
pub(crate) fn recipient(&self) -> &Recipient {
|
||||||
&self.pk
|
&self.pk
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn stub(&self) -> Stub {
|
||||||
|
Stub::new(self.yubikey.serial(), self.slot, &self.pk)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn request_pin_if_necessary<E>(
|
pub(crate) fn request_pin_if_necessary<E>(
|
||||||
&mut self,
|
&mut self,
|
||||||
callbacks: &mut dyn Callbacks<E>,
|
callbacks: &mut dyn Callbacks<E>,
|
||||||
|
|||||||
+18
-2
@@ -17,6 +17,7 @@ use yubikey::{piv::RetiredSlotId, reader::Context, PinPolicy, Serial, TouchPolic
|
|||||||
mod builder;
|
mod builder;
|
||||||
mod error;
|
mod error;
|
||||||
mod key;
|
mod key;
|
||||||
|
mod native;
|
||||||
mod piv_p256;
|
mod piv_p256;
|
||||||
mod plugin;
|
mod plugin;
|
||||||
mod util;
|
mod util;
|
||||||
@@ -297,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}");
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -402,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
|
||||||
@@ -612,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,
|
||||||
"{}",
|
"{}",
|
||||||
@@ -619,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()?;
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
use std::marker::PhantomData;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::sync::RwLock;
|
||||||
|
|
||||||
|
use hkdf::Hkdf;
|
||||||
|
use sha2::Sha256;
|
||||||
|
|
||||||
|
use crate::key::Connection;
|
||||||
|
|
||||||
|
pub(crate) mod p256tag;
|
||||||
|
|
||||||
|
/// Derives a tag for the tagged age recipient formats.
|
||||||
|
fn stanza_tag(ikm: &[u8], salt: &str) -> [u8; 4] {
|
||||||
|
let (tag, _) = Hkdf::<Sha256>::extract(Some(salt.as_bytes()), ikm);
|
||||||
|
tag[..4].try_into().expect("correct length")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pretend that a YubiKey connection is a KEM private key.
|
||||||
|
struct YubiKeyKemPrivateKey<'a, Kem> {
|
||||||
|
conn: Rc<RwLock<&'a mut Connection>>,
|
||||||
|
_kem: PhantomData<Kem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Kem> YubiKeyKemPrivateKey<'a, Kem> {
|
||||||
|
fn new(conn: &'a mut Connection) -> Self {
|
||||||
|
Self {
|
||||||
|
conn: Rc::new(RwLock::new(conn)),
|
||||||
|
_kem: PhantomData::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Kem> Clone for YubiKeyKemPrivateKey<'a, Kem> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
conn: self.conn.clone(),
|
||||||
|
_kem: PhantomData::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Kem> PartialEq for YubiKeyKemPrivateKey<'a, Kem> {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.conn.read().unwrap().stub() == other.conn.read().unwrap().stub()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a, Kem> Eq for YubiKeyKemPrivateKey<'a, Kem> {}
|
||||||
|
|
||||||
|
impl<'a, Kem: hpke::Kem> hpke::Serializable for YubiKeyKemPrivateKey<'a, Kem> {
|
||||||
|
type OutputSize = <Kem::PrivateKey as hpke::Serializable>::OutputSize;
|
||||||
|
fn write_exact(&self, _: &mut [u8]) {
|
||||||
|
unreachable!("Never called")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a, Kem: hpke::Kem> hpke::Deserializable for YubiKeyKemPrivateKey<'a, Kem> {
|
||||||
|
fn from_bytes(_: &[u8]) -> Result<Self, hpke::HpkeError> {
|
||||||
|
unreachable!("Never called")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
use std::fmt;
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
|
use age_core::{
|
||||||
|
format::{FileKey, Stanza},
|
||||||
|
primitives::{bech32_encode_to_fmt, hpke_open, hpke_seal},
|
||||||
|
secrecy::{zeroize::Zeroize, ExposeSecret},
|
||||||
|
};
|
||||||
|
use base64::{prelude::BASE64_STANDARD_NO_PAD, Engine};
|
||||||
|
use hpke::{Deserializable, Serializable};
|
||||||
|
use p256::{
|
||||||
|
elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint},
|
||||||
|
EncodedPoint,
|
||||||
|
};
|
||||||
|
use rand::rngs::OsRng;
|
||||||
|
use yubikey::{certificate::PublicKeyInfo, Certificate};
|
||||||
|
|
||||||
|
use super::{stanza_tag, YubiKeyKemPrivateKey};
|
||||||
|
use crate::{
|
||||||
|
key::{self, Connection},
|
||||||
|
recipient::static_tag,
|
||||||
|
util::base64_arg,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) const PLUGIN_NAME: &str = "tag";
|
||||||
|
const RECIPIENT_PREFIX: bech32::Hrp = bech32::Hrp::parse_unchecked("age1tag");
|
||||||
|
|
||||||
|
const P256TAG_RECIPIENT_TAG: &str = "p256tag";
|
||||||
|
const P256TAG_SALT: &str = "age-encryption.org/p256tag";
|
||||||
|
|
||||||
|
const TAG_BYTES: usize = 4;
|
||||||
|
/// Per [RFC 9180 section 7.1.1]:
|
||||||
|
/// > For P-256, P-384, and P-521, the `SerializePublicKey()` function of the KEM performs
|
||||||
|
/// > the uncompressed Elliptic-Curve-Point-to-Octet-String conversion according to [SECG].
|
||||||
|
///
|
||||||
|
/// [RFC 9180 section 7.1.1]: https://www.rfc-editor.org/rfc/rfc9180.html#section-7.1.1
|
||||||
|
/// [SECG]: https://secg.org/sec1-v2.pdf
|
||||||
|
const ENC_BYTES: usize = 65;
|
||||||
|
|
||||||
|
type Kem = hpke::kem::DhP256HkdfSha256;
|
||||||
|
|
||||||
|
/// The non-hybrid tagged age recipient type, designed for hardware keys where decryption
|
||||||
|
/// potentially requires user presence.
|
||||||
|
///
|
||||||
|
/// With knowledge of the recipient, it is possible to check if a stanza was addressed to
|
||||||
|
/// a specific recipient before attempting decryption. This offers less privacy than the
|
||||||
|
/// untagged recipient types.
|
||||||
|
#[derive(Clone, PartialEq, Eq)]
|
||||||
|
pub(crate) struct Recipient {
|
||||||
|
/// Compressed encoding of the recipient public key.
|
||||||
|
compressed: EncodedPoint,
|
||||||
|
/// Cached in-memory representation, for HPKE.
|
||||||
|
pk_recip: <Kem as hpke::Kem>::PublicKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Recipient {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
bech32_encode_to_fmt(f, RECIPIENT_PREFIX, self.compressed.as_bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for Recipient {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Recipient {
|
||||||
|
/// Attempts to parse a valid p256tag recipient from its compressed SEC-1 byte encoding.
|
||||||
|
pub(crate) fn from_bytes(bytes: &[u8]) -> Option<Self> {
|
||||||
|
let encoded = p256::EncodedPoint::from_bytes(bytes).ok()?;
|
||||||
|
if !encoded.is_compressed() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let point = p256::PublicKey::from_encoded_point(&encoded).into_option()?;
|
||||||
|
|
||||||
|
let pk_recip =
|
||||||
|
<Kem as hpke::Kem>::PublicKey::from_bytes(point.to_encoded_point(false).as_bytes())
|
||||||
|
.expect("valid");
|
||||||
|
|
||||||
|
Some(Self {
|
||||||
|
compressed: encoded,
|
||||||
|
pk_recip,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn from_certificate(cert: &Certificate) -> Option<Self> {
|
||||||
|
Self::from_spki(cert.subject_pki())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn from_spki(spki: &PublicKeyInfo) -> Option<Self> {
|
||||||
|
let encoded = match spki {
|
||||||
|
PublicKeyInfo::EcP256(pubkey) => Some(pubkey),
|
||||||
|
_ => None,
|
||||||
|
}?;
|
||||||
|
|
||||||
|
// Check that the certificate encoding is uncompressed.
|
||||||
|
let pk_recip = <Kem as hpke::Kem>::PublicKey::from_bytes(encoded.as_bytes()).ok()?;
|
||||||
|
|
||||||
|
let point = p256::PublicKey::from_encoded_point(encoded).into_option()?;
|
||||||
|
let compressed = point.to_encoded_point(true);
|
||||||
|
|
||||||
|
Some(Self {
|
||||||
|
compressed,
|
||||||
|
pk_recip,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the compressed SEC-1 encoding of this recipient.
|
||||||
|
pub(crate) fn to_compressed(&self) -> p256::EncodedPoint {
|
||||||
|
self.compressed
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn static_tag(&self) -> [u8; TAG_BYTES] {
|
||||||
|
static_tag(self.compressed.as_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn wrap_file_key(&self, file_key: &FileKey) -> RecipientLine {
|
||||||
|
let (enc, ct) = hpke_seal::<Kem, _>(
|
||||||
|
&self.pk_recip,
|
||||||
|
P256TAG_SALT.as_bytes(),
|
||||||
|
file_key.expose_secret(),
|
||||||
|
&mut OsRng,
|
||||||
|
);
|
||||||
|
|
||||||
|
RecipientLine {
|
||||||
|
tag: tag(&enc, self.static_tag()),
|
||||||
|
enc,
|
||||||
|
ct,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tag(enc: &<Kem as hpke::Kem>::EncappedKey, static_tag: [u8; TAG_BYTES]) -> [u8; TAG_BYTES] {
|
||||||
|
let ikm = enc
|
||||||
|
.to_bytes()
|
||||||
|
.into_iter()
|
||||||
|
.chain(static_tag)
|
||||||
|
.collect::<Vec<u8>>();
|
||||||
|
|
||||||
|
stanza_tag(&ikm, P256TAG_SALT)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct RecipientLine {
|
||||||
|
tag: [u8; TAG_BYTES],
|
||||||
|
enc: <Kem as hpke::Kem>::EncappedKey,
|
||||||
|
ct: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RecipientLine> for Stanza {
|
||||||
|
fn from(r: RecipientLine) -> Self {
|
||||||
|
Stanza {
|
||||||
|
tag: P256TAG_RECIPIENT_TAG.to_owned(),
|
||||||
|
args: vec![
|
||||||
|
BASE64_STANDARD_NO_PAD.encode(r.tag),
|
||||||
|
BASE64_STANDARD_NO_PAD.encode(r.enc.to_bytes()),
|
||||||
|
],
|
||||||
|
body: r.ct,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RecipientLine {
|
||||||
|
pub(crate) fn from_stanza(s: Stanza) -> Option<Result<Self, ()>> {
|
||||||
|
if s.tag != P256TAG_RECIPIENT_TAG {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (tag, enc) = match &s.args[..] {
|
||||||
|
[encoded_tag, encoded_enc] => (
|
||||||
|
base64_arg(encoded_tag, [0; TAG_BYTES]),
|
||||||
|
base64_arg(encoded_enc, [0; ENC_BYTES])
|
||||||
|
.and_then(|bytes| <Kem as hpke::Kem>::EncappedKey::from_bytes(&bytes[..]).ok()),
|
||||||
|
),
|
||||||
|
_ => (None, None),
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(match (tag, enc) {
|
||||||
|
(Some(tag), Some(epk_bytes)) => Ok(RecipientLine {
|
||||||
|
tag,
|
||||||
|
enc: epk_bytes,
|
||||||
|
ct: s.body,
|
||||||
|
}),
|
||||||
|
// Anything else indicates a structurally-invalid stanza.
|
||||||
|
_ => Err(()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn matches_stub(&self, stub: &key::Stub) -> bool {
|
||||||
|
self.tag == tag(&self.enc, stub.tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn unwrap_file_key(&self, conn: &mut Connection) -> Result<FileKey, ()> {
|
||||||
|
// > The identity implementation [...] MUST check that the body length is exactly
|
||||||
|
// > 32 bytes before attempting to decrypt it, to mitigate partitioning oracle
|
||||||
|
// > attacks.
|
||||||
|
if self.ct.len() != 32 {
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let sk_recip = YubiKeyKemPrivateKey::new(conn);
|
||||||
|
|
||||||
|
// A failure to decrypt is fatal, because we assume that we won't
|
||||||
|
// encounter 32-bit collisions on the key tag embedded in the header.
|
||||||
|
hpke_open::<YubiKeyDhP256HkdfSha256>(
|
||||||
|
&self.enc,
|
||||||
|
&sk_recip,
|
||||||
|
P256TAG_SALT.as_bytes(),
|
||||||
|
&self.ct,
|
||||||
|
)
|
||||||
|
.map_err(|_| ())
|
||||||
|
.map(|mut pt| {
|
||||||
|
FileKey::init_with_mut(|file_key| {
|
||||||
|
file_key.copy_from_slice(&pt);
|
||||||
|
pt.zeroize();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A decap-only version of [`Kem`] where the private key is stored on a YubiKey.
|
||||||
|
struct YubiKeyDhP256HkdfSha256<'a>(PhantomData<&'a ()>);
|
||||||
|
|
||||||
|
impl<'a> hpke::Kem for YubiKeyDhP256HkdfSha256<'a> {
|
||||||
|
type PublicKey = <Kem as hpke::Kem>::PublicKey;
|
||||||
|
type PrivateKey = YubiKeyKemPrivateKey<'a, Kem>;
|
||||||
|
|
||||||
|
fn sk_to_pk(_: &Self::PrivateKey) -> Self::PublicKey {
|
||||||
|
unreachable!("Never called")
|
||||||
|
}
|
||||||
|
|
||||||
|
type EncappedKey = <Kem as hpke::Kem>::EncappedKey;
|
||||||
|
type NSecret = <Kem as hpke::Kem>::NSecret;
|
||||||
|
const KEM_ID: u16 = <Kem as hpke::Kem>::KEM_ID;
|
||||||
|
|
||||||
|
fn derive_keypair(_: &[u8]) -> (Self::PrivateKey, Self::PublicKey) {
|
||||||
|
unreachable!("Never called")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decap(
|
||||||
|
sk_recip: &Self::PrivateKey,
|
||||||
|
pk_sender_id: Option<&Self::PublicKey>,
|
||||||
|
encapped_key: &Self::EncappedKey,
|
||||||
|
) -> Result<hpke::kem::SharedSecret<Self>, hpke::HpkeError> {
|
||||||
|
let mut sk_recip = sk_recip.conn.write().unwrap();
|
||||||
|
|
||||||
|
// Put together the binding context used for all KDF operations
|
||||||
|
let suite_id = b"KEM\x00\x10";
|
||||||
|
|
||||||
|
// Compute the shared secret from the ephemeral inputs
|
||||||
|
let kex_res_eph = sk_recip
|
||||||
|
.p256_ecdh(&encapped_key.to_bytes())
|
||||||
|
.map_err(|_| hpke::HpkeError::DecapError)?;
|
||||||
|
|
||||||
|
// Compute the sender's pubkey from their privkey
|
||||||
|
let pk_recip = match sk_recip.recipient() {
|
||||||
|
crate::recipient::Recipient::P256Tag(recipient) => &recipient.pk_recip,
|
||||||
|
_ => panic!("should have been filtered out earlier"),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(pk_sender_id.is_none());
|
||||||
|
|
||||||
|
// kem_context = encapped_key || pk_recip || pk_sender_id
|
||||||
|
let kem_context = [encapped_key.to_bytes(), pk_recip.to_bytes()]
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// The "unauthed shared secret" is derived from just the KEX of the ephemeral
|
||||||
|
// input with the recipient pubkey. The HKDF-Expand call only errors if the
|
||||||
|
// output values are 255x the digest size of the hash function. Since these
|
||||||
|
// values are fixed at compile time, we don't worry about it.
|
||||||
|
let mut shared_secret = <hpke::kem::SharedSecret<Self> as Default>::default();
|
||||||
|
hpke::kdf::extract_and_expand::<hpke::kdf::HkdfSha256>(
|
||||||
|
&kex_res_eph,
|
||||||
|
suite_id,
|
||||||
|
&kem_context,
|
||||||
|
&mut shared_secret.0,
|
||||||
|
)
|
||||||
|
.expect("shared secret is way too big");
|
||||||
|
Ok(shared_secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encap<R: rand::CryptoRng + rand::RngCore>(
|
||||||
|
_: &Self::PublicKey,
|
||||||
|
_: Option<(&Self::PrivateKey, &Self::PublicKey)>,
|
||||||
|
_: &mut R,
|
||||||
|
) -> Result<(hpke::kem::SharedSecret<Self>, Self::EncappedKey), hpke::HpkeError> {
|
||||||
|
unreachable!("Never called")
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
-6
@@ -111,7 +111,7 @@ impl Recipient {
|
|||||||
|
|
||||||
let shared_secret = esk.diffie_hellman(self.public_key());
|
let shared_secret = esk.diffie_hellman(self.public_key());
|
||||||
|
|
||||||
let salt = salt(&epk_bytes, self);
|
let salt = salt(&epk_bytes, self.to_encoded());
|
||||||
|
|
||||||
let enc_key = {
|
let enc_key = {
|
||||||
let mut okm = [0; 32];
|
let mut okm = [0; 32];
|
||||||
@@ -138,10 +138,17 @@ impl Recipient {
|
|||||||
|
|
||||||
impl RecipientLine {
|
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, ()> {
|
||||||
let crate::recipient::Recipient::PivP256(recipient) = conn.recipient();
|
let (static_tag, pk) = match conn.recipient() {
|
||||||
assert_eq!(self.tag, recipient.tag());
|
crate::recipient::Recipient::PivP256(recipient) => {
|
||||||
|
(recipient.tag(), recipient.to_encoded())
|
||||||
|
}
|
||||||
|
crate::recipient::Recipient::P256Tag(recipient) => {
|
||||||
|
(recipient.static_tag(), recipient.to_compressed())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
assert_eq!(self.tag, static_tag);
|
||||||
|
|
||||||
let salt = salt(&self.epk_bytes, recipient);
|
let salt = salt(&self.epk_bytes, pk);
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -162,9 +169,10 @@ impl RecipientLine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn salt(epk_bytes: &EphemeralKeyBytes, pk: &Recipient) -> Vec<u8> {
|
fn salt(epk_bytes: &EphemeralKeyBytes, pk: p256::EncodedPoint) -> Vec<u8> {
|
||||||
|
assert!(pk.is_compressed());
|
||||||
let mut salt = vec![];
|
let mut salt = vec![];
|
||||||
salt.extend_from_slice(epk_bytes.as_bytes());
|
salt.extend_from_slice(epk_bytes.as_bytes());
|
||||||
salt.extend_from_slice(pk.to_encoded().as_bytes());
|
salt.extend_from_slice(pk.as_bytes());
|
||||||
salt
|
salt
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+9
-2
@@ -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, piv_p256, Recipient, PLUGIN_NAME};
|
use crate::{fl, key, native::p256tag, piv_p256, Recipient, PLUGIN_NAME};
|
||||||
|
|
||||||
pub(crate) struct Handler;
|
pub(crate) struct Handler;
|
||||||
|
|
||||||
@@ -273,22 +273,29 @@ impl IdentityPluginV1 for IdentityPlugin {
|
|||||||
|
|
||||||
enum SupportedStanza {
|
enum SupportedStanza {
|
||||||
PivP256(piv_p256::RecipientLine),
|
PivP256(piv_p256::RecipientLine),
|
||||||
|
P256Tag(p256tag::RecipientLine),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SupportedStanza {
|
impl SupportedStanza {
|
||||||
fn parse(stanza: Stanza) -> Option<Result<Self, ()>> {
|
fn parse(stanza: Stanza) -> Option<Result<Self, ()>> {
|
||||||
piv_p256::RecipientLine::from_stanza(&stanza).map(|res| res.map(Self::PivP256))
|
piv_p256::RecipientLine::from_stanza(&stanza)
|
||||||
|
.map(|res| res.map(Self::PivP256))
|
||||||
|
.or_else(|| {
|
||||||
|
p256tag::RecipientLine::from_stanza(stanza).map(|res| res.map(Self::P256Tag))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn matches_stub(&self, stub: &key::Stub) -> bool {
|
pub(crate) fn matches_stub(&self, stub: &key::Stub) -> bool {
|
||||||
match self {
|
match self {
|
||||||
SupportedStanza::PivP256(line) => stub.tag == line.tag,
|
SupportedStanza::PivP256(line) => stub.tag == line.tag,
|
||||||
|
SupportedStanza::P256Tag(line) => line.matches_stub(stub),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn unwrap_file_key(&self, conn: &mut key::Connection) -> Result<FileKey, ()> {
|
pub(crate) fn unwrap_file_key(&self, conn: &mut key::Connection) -> Result<FileKey, ()> {
|
||||||
match self {
|
match self {
|
||||||
SupportedStanza::PivP256(line) => line.unwrap_file_key(conn),
|
SupportedStanza::PivP256(line) => line.unwrap_file_key(conn),
|
||||||
|
SupportedStanza::P256Tag(line) => line.unwrap_file_key(conn),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-1
@@ -3,19 +3,21 @@ 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::{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;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) enum Recipient {
|
pub(crate) enum Recipient {
|
||||||
PivP256(piv_p256::Recipient),
|
PivP256(piv_p256::Recipient),
|
||||||
|
P256Tag(p256tag::Recipient),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Recipient {
|
impl fmt::Display for Recipient {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Recipient::PivP256(recipient) => recipient.fmt(f),
|
Recipient::PivP256(recipient) => recipient.fmt(f),
|
||||||
|
Recipient::P256Tag(recipient) => recipient.fmt(f),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,20 +27,38 @@ impl Recipient {
|
|||||||
pub(crate) fn from_bytes(plugin_name: &str, bytes: &[u8]) -> Option<Self> {
|
pub(crate) fn from_bytes(plugin_name: &str, bytes: &[u8]) -> Option<Self> {
|
||||||
match plugin_name {
|
match plugin_name {
|
||||||
PLUGIN_NAME => piv_p256::Recipient::from_bytes(bytes).map(Self::PivP256),
|
PLUGIN_NAME => piv_p256::Recipient::from_bytes(bytes).map(Self::PivP256),
|
||||||
|
p256tag::PLUGIN_NAME => p256tag::Recipient::from_bytes(bytes).map(Self::P256Tag),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
Recipient::PivP256(recipient) => recipient.tag(),
|
Recipient::PivP256(recipient) => recipient.tag(),
|
||||||
|
Recipient::P256Tag(recipient) => recipient.static_tag(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn wrap_file_key(&self, file_key: &FileKey) -> Stanza {
|
pub(crate) fn wrap_file_key(&self, file_key: &FileKey) -> Stanza {
|
||||||
match self {
|
match self {
|
||||||
Recipient::PivP256(recipient) => recipient.wrap_file_key(file_key).into(),
|
Recipient::PivP256(recipient) => recipient.wrap_file_key(file_key).into(),
|
||||||
|
Recipient::P256Tag(recipient) => recipient.wrap_file_key(file_key).into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+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