4 Commits

Author SHA1 Message Date
Jack Grigg 23a1f61e5a v0.4.1
CI checks / Test on linux (push) Has been cancelled
CI checks / Test on macos (push) Has been cancelled
CI checks / Test on windows (push) Has been cancelled
CI checks / Code coverage (push) Has been cancelled
CI checks / Intra-doc links (push) Has been cancelled
CI checks / Rustfmt (push) Has been cancelled
Publish release binaries / Publish for macos-arm64 (push) Has been cancelled
Publish release binaries / Publish for macos-x86_64 (push) Has been cancelled
Publish release binaries / Publish for linux (push) Has been cancelled
Publish release binaries / Publish for windows (push) Has been cancelled
Publish release binaries / Debian linux (push) Has been cancelled
2026-04-08 04:20:54 +01:00
Jack Grigg eb945b2849 Merge tag 'v0.4.0' into detect-critical-extensions 2026-04-08 04:16:00 +01:00
Jack Grigg bf081835c4 Release 0.3.4
CI checks / Test on linux (push) Has been cancelled
CI checks / Test on macos (push) Has been cancelled
CI checks / Test on windows (push) Has been cancelled
CI checks / Clippy (1.56.0) (push) Has been cancelled
CI checks / Clippy (nightly) (push) Has been cancelled
CI checks / Code coverage (push) Has been cancelled
CI checks / Intra-doc links (push) Has been cancelled
CI checks / Rustfmt (push) Has been cancelled
Publish release binaries / Publish for macos-arm64 (push) Has been cancelled
Publish release binaries / Publish for macos-x86_64 (push) Has been cancelled
Publish release binaries / Publish for linux (push) Has been cancelled
Publish release binaries / Publish for windows (push) Has been cancelled
Publish release binaries / Debian linux (push) Has been cancelled
2026-04-08 04:14:54 +01:00
Jack Grigg 9503f406ae Reject identities with unrecognised critical extensions
We don't know how to correctly use these identities. In particular, some
identities store parts of their private key material in certificate
extensions to work around hardware limitations. Not understanding these
extensions could lead to encrypting with the wrong protocol and
violating security assumptions.
2026-04-08 04:12:35 +01:00
15 changed files with 773 additions and 904 deletions
+17 -65
View File
@@ -1,13 +1,10 @@
name: CI checks
on:
pull_request:
push:
branches: main
on: [push, pull_request]
jobs:
test-msrv:
name: Test MSRV on ${{ matrix.name }}
test:
name: Test on ${{ matrix.name }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
@@ -25,83 +22,37 @@ jobs:
os: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Install build dependencies
run: sudo apt install ${{ matrix.build_deps }}
if: matrix.build_deps != ''
- uses: dtolnay/rust-toolchain@stable
id: stable-toolchain
- name: Install test dependencies using latest stable Rust
run: cargo +${{steps.stable-toolchain.outputs.name}} install rage
- name: Run tests
run: cargo test
- name: Verify working directory is clean
run: git diff --exit-code
test-latest:
name: Test latest stable on ${{ matrix.name }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
name: [linux, windows, macos]
include:
- name: linux
os: ubuntu-latest
build_deps: >
libpcsclite-dev
- name: windows
os: windows-latest
- name: macos
os: macos-latest
steps:
- uses: actions/checkout@v4
- name: Install build dependencies
run: sudo apt install ${{ matrix.build_deps }}
if: matrix.build_deps != ''
- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@stable
id: toolchain
- run: rustup override set ${{steps.toolchain.outputs.name}}
- name: Install test dependencies
run: cargo install rage
- name: Remove lockfile to build with latest dependencies
run: rm Cargo.lock
- run: cargo fetch
- name: Build tests
run: cargo build --verbose --tests
- name: Run tests
run: cargo test
- name: Verify working directory is clean (excluding lockfile)
run: git diff --exit-code ':!Cargo.lock'
run: cargo test --verbose
codecov:
name: Code coverage
runs-on: ubuntu-latest
container:
image: xd009642/tarpaulin:develop-nightly
options: --security-opt seccomp=unconfined
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Install build dependencies
run: apt update && apt install -y libpcsclite-dev
run: sudo apt install libpcsclite-dev
- name: Install coverage dependencies
run: cargo install cargo-tarpaulin
- name: Generate coverage report
run: >
cargo tarpaulin
--engine llvm
--timeout 180
--out xml
run: cargo tarpaulin --engine llvm --all-features --release --timeout 600 --out Xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4.5.0
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
uses: codecov/codecov-action@v3.1.1
doc-links:
name: Intra-doc links
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Install build dependencies
run: sudo apt install libpcsclite-dev
- run: cargo fetch
@@ -111,8 +62,9 @@ jobs:
fmt:
name: Rustfmt
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Check formatting
run: cargo fmt -- --check
+2 -1
View File
@@ -6,9 +6,10 @@ on: pull_request
jobs:
clippy-beta:
name: Clippy (beta)
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@beta
id: toolchain
- run: rustup override set ${{steps.toolchain.outputs.name}}
+2 -1
View File
@@ -6,9 +6,10 @@ on: pull_request
jobs:
clippy:
name: Clippy (MSRV)
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Install build dependencies
run: sudo apt install libpcsclite-dev
- name: Run clippy
+14 -12
View File
@@ -24,7 +24,7 @@ jobs:
- windows
include:
- name: linux
os: ubuntu-20.04
os: ubuntu-18.04
build_deps: >
libpcsclite-dev
archive_name: age-plugin-yubikey.tar.gz
@@ -48,10 +48,11 @@ jobs:
asset_suffix: x86_64-darwin.tar.gz
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
id: toolchain
- run: rustup override set ${{steps.toolchain.outputs.name}}
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Add target
run: rustup target add ${{ matrix.target }}
if: matrix.target != ''
@@ -86,7 +87,7 @@ jobs:
if: matrix.name == 'windows'
- name: Upload archive to release
uses: svenstaro/upload-release-action@2.6.1
uses: svenstaro/upload-release-action@2.5.0
with:
file: ${{ matrix.archive_name }}
asset_name: age-plugin-yubikey-$tag-${{ matrix.asset_suffix }}
@@ -95,7 +96,7 @@ jobs:
deb:
name: Debian ${{ matrix.name }}
runs-on: ubuntu-20.04
runs-on: ubuntu-18.04
strategy:
matrix:
name: [linux]
@@ -106,10 +107,11 @@ jobs:
libpcsclite-dev
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
id: toolchain
- run: rustup override set ${{steps.toolchain.outputs.name}}
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Add target
run: rustup target add ${{ matrix.target }}
- name: cargo install cargo-deb
@@ -144,7 +146,7 @@ jobs:
args: --package age-plugin-yubikey --no-build --target ${{ matrix.target }}
- name: Upload Debian package to release
uses: svenstaro/upload-release-action@2.6.1
uses: svenstaro/upload-release-action@2.5.0
with:
file: target/${{ matrix.target }}/debian/*.deb
file_glob: true
+4 -5
View File
@@ -8,12 +8,11 @@ to 0.3.0 are beta releases.
## [Unreleased]
## [0.5.0] - 2024-08-04
## [0.3.4], [0.4.1] - 2026-04-08
### Fixed
- `age-plugin-yubikey` can now be compiled with Rust 1.80 and above.
### Changed
- MSRV is now 1.67.0.
- `age-plugin-yubikey` now completely ignores any identity that has unrecognised
critical extensions in its certificate, to ensure it doesn't misuse a newer
identity type.
## [0.4.0] - 2023-04-09
### Changed
Generated
+667 -769
View File
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -1,7 +1,7 @@
[package]
name = "age-plugin-yubikey"
description = "YubiKey plugin for age clients"
version = "0.5.0"
version = "0.4.1"
authors = ["Jack Grigg <thestr4d@gmail.com>"]
repository = "https://github.com/str4d/age-plugin-yubikey"
readme = "README.md"
@@ -9,7 +9,7 @@ keywords = ["age", "cli", "encryption", "yubikey"]
categories = ["command-line-utilities", "cryptography"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.67" # MSRV
rust-version = "1.65" # MSRV
[package.metadata.deb]
extended-description = """\
@@ -22,12 +22,12 @@ assets = [
]
[dependencies]
age-core = "0.10"
age-plugin = "0.5"
age-core = "0.9"
age-plugin = "0.4"
base64 = "0.21"
bech32 = "0.9"
console = { version = "0.15", default-features = false }
dialoguer = { version = "0.11", default-features = false, features = ["password"] }
dialoguer = { version = "0.10", default-features = false, features = ["password"] }
env_logger = "0.10"
gumdrop = "0.8"
hex = "0.4"
@@ -36,23 +36,23 @@ p256 = { version = "0.13", features = ["ecdh"] }
pcsc = "2.4"
rand = "0.8"
sha2 = "0.10"
which = "5"
which = "4.1"
x509 = "0.2"
x509-parser = "0.14"
yubikey = { version = "=0.8.0-pre.0", features = ["untested"] }
# Translations
i18n-embed = { version = "0.14", features = ["desktop-requester", "fluent-system"] }
i18n-embed-fl = "0.8"
i18n-embed = { version = "0.13", features = ["desktop-requester", "fluent-system"] }
i18n-embed-fl = "0.6"
lazy_static = "1"
rust-embed = "8"
rust-embed = "6"
# GnuPG coexistence
sysinfo = "0.29"
sysinfo = "0.28"
[dev-dependencies]
flate2 = "1"
man = "0.3"
tempfile = "3"
test-with = "0.11"
which = "5"
test-with = "0.9"
which = "4"
+1 -9
View File
@@ -8,13 +8,8 @@ which enables files to be encrypted to age identities stored on YubiKeys.
| Environment | CLI command |
|-------------|-------------|
| Cargo (Rust 1.67+) | `cargo install age-plugin-yubikey` |
| Cargo (Rust 1.65+) | `cargo install age-plugin-yubikey` |
| Homebrew (macOS or Linux) | `brew install age-plugin-yubikey` |
| Arch Linux | `pacman -S age-plugin-yubikey` |
| Debian | [Debian package](https://github.com/str4d/age-plugin-yubikey/releases) |
| NixOS | Add to config:<br>`environment.systemPackages = [`<br>` pkgs.age-plugin-yubikey`<br>`];`<br>Or run `nix-env -i age-plugin-yubikey` |
| Ubuntu 20.04+ | [Debian package](https://github.com/str4d/age-plugin-yubikey/releases) |
| OpenBSD | `pkg_add age-plugin-yubikey` (security/age-plugin-yubikey) |
On Windows, Linux, and macOS, you can use the
[pre-built binaries](https://github.com/str4d/age-plugin-yubikey/releases).
@@ -29,10 +24,8 @@ is installed and running.
| Environment | CLI command |
|-------------|-------------|
| Debian or Ubuntu | `sudo apt-get install pcscd` |
| Fedora | `sudo dnf install pcsc-lite` |
| OpenBSD | As ```root``` do:<br>`pkg_add pcsc-lite ccid`<br>`rcctl enable pcscd`<br>`rcctl start pcscd` |
| FreeBSD | As ```root``` do:<br>`pkg install pcsc-lite libccid`<br>`service pcscd enable`<br>`service pcscd start` |
| Arch | `sudo pacman -S pcsclite pcsc-tools yubikey-manager`<br>`sudo systemctl enable pcscd`<br>`sudo systemctl start pcscd`|
When installing via Cargo, you also need to ensure that the development headers
for the `pcsc-lite` library are available, so that the `pcsc-sys` crate can be
@@ -41,7 +34,6 @@ compiled.
| Environment | CLI command |
|-------------|-------------|
| Debian or Ubuntu | `sudo apt-get install libpcsclite-dev` |
| Fedora | `sudo dnf install pcsc-lite-devel` |
### Windows Subsystem for Linux (WSL)
+1 -1
View File
@@ -6,7 +6,7 @@ use std::io::prelude::*;
const MANPAGES_DIR: &str = "./target/manpages";
fn generate_manpage(page: String, name: &str) {
let file = File::create(format!("{MANPAGES_DIR}/{name}.1.gz"))
let file = File::create(format!("{}/{}.1.gz", MANPAGES_DIR, name))
.expect("Should be able to open file in target directory");
let mut encoder = GzEncoder::new(file, Compression::best());
encoder
-1
View File
@@ -204,7 +204,6 @@ err-invalid-flag-tui = Flag '{$flag}' cannot be used with the interactive in
err-invalid-pin-policy = Invalid PIN policy '{$policy}' (expected [{$expected}]).
err-invalid-slot = Invalid slot '{$slot}' (expected number between 1 and 20).
err-invalid-touch-policy = Invalid touch policy '{$policy}' (expected [{$expected}]).
err-io-user = Failed to get input from user: {$err}
err-io = Failed to set up {-yubikey}: {$err}
err-multiple-commands = Only one of {-cmd-generate}, {-cmd-identity}, {-cmd-list}, {-cmd-list-all} can be specified.
err-multiple-yubikeys = Multiple {-yubikeys} are plugged in. Use {-flag-serial} to select a single {-yubikey}.
+1 -1
View File
@@ -1,3 +1,3 @@
[toolchain]
channel = "1.67.0"
channel = "1.65.0"
components = ["clippy", "rustfmt"]
-8
View File
@@ -15,7 +15,6 @@ macro_rules! wlnfl {
pub enum Error {
CustomManagementKey,
Dialog(dialoguer::Error),
InvalidFlagCommand(String, String),
InvalidFlagTui(String),
InvalidPinPolicy(String),
@@ -36,12 +35,6 @@ pub enum Error {
YubiKey(yubikey::Error),
}
impl From<dialoguer::Error> for Error {
fn from(e: dialoguer::Error) -> Self {
Error::Dialog(e)
}
}
impl From<io::Error> for Error {
fn from(e: io::Error) -> Self {
Error::Io(e)
@@ -72,7 +65,6 @@ impl fmt::Debug for Error {
url = CHANGE_MGMT_KEY_URL
)?;
}
Error::Dialog(e) => wlnfl!(f, "err-io-user", err = e.to_string())?,
Error::InvalidFlagCommand(flag, command) => wlnfl!(
f,
"err-invalid-flag-command",
+37 -10
View File
@@ -15,6 +15,7 @@ use std::io;
use std::iter;
use std::thread::sleep;
use std::time::{Duration, Instant, SystemTime};
use x509_parser::der_parser::oid::Oid;
use yubikey::{
certificate::Certificate,
piv::{decrypt_data, AlgorithmId, RetiredSlotId, SlotId},
@@ -27,13 +28,16 @@ use crate::{
fl,
format::{RecipientLine, STANZA_KEY_LABEL},
p256::{Recipient, TAG_BYTES},
util::{otp_serial_prefix, Metadata},
util::{otp_serial_prefix, Metadata, POLICY_EXTENSION_OID},
IDENTITY_PREFIX,
};
const ONE_SECOND: Duration = Duration::from_secs(1);
const FIFTEEN_SECONDS: Duration = Duration::from_secs(15);
/// The set of OIDs that we understand and use when parsing YubiKey slot certificates.
const KNOWN_OIDS: &[&[u64]] = &[POLICY_EXTENSION_OID];
pub(crate) fn is_connected(reader: Reader) -> bool {
filter_connected(&reader)
}
@@ -274,10 +278,10 @@ pub(crate) fn disconnect_without_reset(yubikey: YubiKey) {
let _ = yubikey.disconnect(pcsc::Disposition::LeaveCard);
}
fn request_pin<E, E2>(
mut prompt: impl FnMut(Option<String>) -> Result<Result<SecretString, E>, E2>,
fn request_pin<E>(
mut prompt: impl FnMut(Option<String>) -> io::Result<Result<SecretString, E>>,
serial: Serial,
) -> Result<Result<SecretString, E>, E2> {
) -> io::Result<Result<SecretString, E>> {
let mut prev_error = None;
loop {
prev_error = Some(match prompt(prev_error)? {
@@ -326,7 +330,7 @@ pub(crate) fn manage(yubikey: &mut YubiKey) -> Result<(), Error> {
let pin = request_pin(
|prev_error| {
if let Some(err) = prev_error {
eprintln!("{err}");
eprintln!("{}", err);
}
Password::new()
.with_prompt(fl!("mgr-choose-new-pin"))
@@ -388,6 +392,30 @@ pub(crate) fn manage(yubikey: &mut YubiKey) -> Result<(), Error> {
Ok(())
}
/// Parses the certificate to identify the preferred recipient type it corresponds to.
pub(crate) fn identify_recipient(cert: &Certificate) -> Option<Recipient> {
let known_oids = KNOWN_OIDS
.iter()
.map(|oid| Oid::from(oid).unwrap())
.collect::<Vec<_>>();
// If the certificate contains any unrecognised critical extensions, reject it: we
// don't know how to correctly use the identity. In particular, some identities store
// parts of their private key material in certificate extensions to work around
// hardware limitations. Not understanding these extensions could lead to encrypting
// with the wrong protocol and violating security assumptions.
let (_, c) = x509_parser::parse_x509_certificate(cert.as_ref()).ok()?;
if c.tbs_certificate
.extensions()
.iter()
.any(|ext| ext.critical && !known_oids.contains(&ext.oid))
{
return None;
}
Recipient::from_certificate(cert)
}
/// Returns an iterator of keys that are occupying plugin-compatible slots, along with the
/// corresponding recipient if the key is compatible with this plugin.
pub(crate) fn list_slots(
@@ -397,8 +425,7 @@ pub(crate) fn list_slots(
// We only use the retired slots.
match key.slot() {
SlotId::Retired(slot) => {
// Only P-256 keys are compatible with us.
let recipient = Recipient::from_certificate(key.certificate());
let recipient = identify_recipient(key.certificate());
Some((key, slot, recipient))
}
_ => None,
@@ -605,9 +632,9 @@ impl Stub {
let (cert, pk) = match Certificate::read(&mut yubikey, SlotId::Retired(self.slot))
.ok()
.and_then(|cert| {
Recipient::from_certificate(&cert)
.filter(|pk| pk.tag() == self.tag)
.map(|pk| (cert, pk))
identify_recipient(&cert)
.filter(|recipient| recipient.tag() == self.tag)
.map(|r| (cert, r))
}) {
Some(pk) => pk,
None => {
+14 -8
View File
@@ -296,8 +296,8 @@ fn list(flags: PluginFlags, all: bool) -> Result<(), Error> {
flags,
all,
|_, recipient, metadata| {
println!("{metadata}");
println!("{recipient}");
println!("{}", metadata);
println!("{}", recipient);
},
)
}
@@ -329,8 +329,8 @@ fn main() -> Result<(), Error> {
if let Some(state_machine) = opts.age_plugin {
run_state_machine(
&state_machine,
Some(plugin::RecipientPlugin::default),
Some(plugin::IdentityPlugin::default),
plugin::RecipientPlugin::default,
plugin::IdentityPlugin::default,
)?;
Ok(())
} else if opts.version {
@@ -411,9 +411,9 @@ fn main() -> Result<(), Error> {
.validity()
.not_before
.to_rfc2822()
.unwrap_or_else(|e| format!("Invalid date: {e}"));
.unwrap_or_else(|e| format!("Invalid date: {}", e));
format!("{name}, created: {created}")
format!("{}, created: {}", name, created)
})
})
})
@@ -631,8 +631,14 @@ fn main() -> Result<(), Error> {
// If `rage` binary is installed, use it in examples. Otherwise default to `age`.
let age_binary = which::which("rage").map(|_| "rage").unwrap_or("age");
let encrypt_usage = format!("$ cat foo.txt | {age_binary} -r {recipient} -o foo.txt.age");
let decrypt_usage = format!("$ cat foo.txt.age | {age_binary} -d -i {file_name} > foo.txt");
let encrypt_usage = format!(
"$ cat foo.txt | {} -r {} -o foo.txt.age",
age_binary, recipient
);
let decrypt_usage = format!(
"$ cat foo.txt.age | {} -d -i {} > foo.txt",
age_binary, file_name
);
let identity_usage = format!(
"$ age-plugin-yubikey -i --serial {} --slot {} > {}",
stub.serial,
+1 -1
View File
@@ -177,7 +177,7 @@ impl Metadata {
.validity()
.not_before
.to_rfc2822()
.unwrap_or_else(|e| format!("Invalid date: {e}")),
.unwrap_or_else(|e| format!("Invalid date: {}", e)),
pin_policy,
touch_policy,
})