Skip to content

Commit

Permalink
age-plugin: Add labels extension to recipient-v1
Browse files Browse the repository at this point in the history
  • Loading branch information
str4d committed Aug 11, 2024
1 parent 62082a6 commit 608e19f
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 24 deletions.
3 changes: 3 additions & 0 deletions age-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to Rust's notion of
to 1.0.0 are beta releases.

## [Unreleased]
### Changed
- `age::plugin::Connection::unidir_receive` now takes an additional argument to
enable handling an optional fourth command.

## [0.10.0] - 2024-02-04
### Added
Expand Down
28 changes: 21 additions & 7 deletions age-core/src/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,11 @@ impl std::error::Error for Error {}
/// should explicitly handle.
pub type Result<T> = io::Result<std::result::Result<T, Error>>;

type UnidirResult<A, B, C, E> = io::Result<(
type UnidirResult<A, B, C, D, E> = io::Result<(
std::result::Result<Vec<A>, Vec<E>>,
std::result::Result<Vec<B>, Vec<E>>,
Option<std::result::Result<Vec<C>, Vec<E>>>,
Option<std::result::Result<Vec<D>, Vec<E>>>,
)>;

/// A connection to a plugin binary.
Expand Down Expand Up @@ -205,23 +206,26 @@ impl<R: Read, W: Write> Connection<R, W> {
///
/// # Arguments
///
/// `command_a`, `command_b`, and (optionally) `command_c` are the known commands that
/// are expected to be received. All other received commands (including grease) will
/// be ignored.
pub fn unidir_receive<A, B, C, E, F, G, H>(
/// `command_a`, `command_b`, and (optionally) `command_c` and `command_d` are the
/// known commands that are expected to be received. All other received commands
/// (including grease) will be ignored.
pub fn unidir_receive<A, B, C, D, E, F, G, H, I>(
&mut self,
command_a: (&str, F),
command_b: (&str, G),
command_c: (Option<&str>, H),
) -> UnidirResult<A, B, C, E>
command_d: (Option<&str>, I),
) -> UnidirResult<A, B, C, D, E>
where
F: Fn(Stanza) -> std::result::Result<A, E>,
G: Fn(Stanza) -> std::result::Result<B, E>,
H: Fn(Stanza) -> std::result::Result<C, E>,
I: Fn(Stanza) -> std::result::Result<D, E>,
{
let mut res_a = Ok(vec![]);
let mut res_b = Ok(vec![]);
let mut res_c = Ok(vec![]);
let mut res_d = Ok(vec![]);

for stanza in iter::repeat_with(|| self.receive()).take_while(|res| match res {
Ok(stanza) => stanza.tag != COMMAND_DONE,
Expand Down Expand Up @@ -255,10 +259,19 @@ impl<R: Read, W: Write> Connection<R, W> {
if stanza.tag.as_str() == tag {
validate(command_c.1(stanza), &mut res_c)
}
} else if let Some(tag) = command_d.0 {
if stanza.tag.as_str() == tag {
validate(command_d.1(stanza), &mut res_d)
}
}
}

Ok((res_a, res_b, command_c.0.map(|_| res_c)))
Ok((
res_a,
res_b,
command_c.0.map(|_| res_c),
command_d.0.map(|_| res_d),
))
}

/// Runs a bidirectional phase as the controller.
Expand Down Expand Up @@ -485,6 +498,7 @@ mod tests {
("test", Ok),
("other", |_| Err(())),
(None, |_| Ok(())),
(None, |_| Ok(())),
)
.unwrap();
assert_eq!(
Expand Down
7 changes: 7 additions & 0 deletions age-plugin/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ to 1.0.0 are beta releases.
- `impl age_plugin::identity::IdentityPluginV1 for std::convert::Infallible`
- `impl age_plugin::recipient::RecipientPluginV1 for std::convert::Infallible`

### Changed
- `age_plugin::recipient::RecipientPluginV1` has a new `labels` method. Existing
implementations of the trait should either return `HashSet::new()` to maintain
existing compatibility, or return labels that apply the desired constraints.
- `age_plugin::run_state_machine` now supports the `recipient-v1` labels
extension.

### Fixed
- `age_plugin::run_state_machine` now takes an `impl age_plugin::PluginHandler`
argument, instead of its previous arguments.
Expand Down
3 changes: 2 additions & 1 deletion age-plugin/src/identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ pub(crate) fn run_v1<P: IdentityPluginV1>(mut plugin: P) -> io::Result<()> {

// Phase 1: receive identities and stanzas
let (identities, recipient_stanzas) = {
let (identities, stanzas, _) = conn.unidir_receive(
let (identities, stanzas, _, _) = conn.unidir_receive(
(ADD_IDENTITY, |s| match (&s.args[..], &s.body[..]) {
([identity], []) => Ok(identity.clone()),
_ => Err(Error::Internal {
Expand Down Expand Up @@ -255,6 +255,7 @@ pub(crate) fn run_v1<P: IdentityPluginV1>(mut plugin: P) -> io::Result<()> {
}
}),
(None, |_| Ok(())),
(None, |_| Ok(())),
)?;

// Now that we have the full list of identities, parse them as Bech32 and add them
Expand Down
100 changes: 84 additions & 16 deletions age-plugin/src/recipient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use age_core::{
use base64::{prelude::BASE64_STANDARD_NO_PAD, Engine};
use bech32::FromBase32;

use std::collections::HashSet;
use std::convert::Infallible;
use std::io;

Expand All @@ -16,7 +17,9 @@ use crate::{Callbacks, PLUGIN_IDENTITY_PREFIX, PLUGIN_RECIPIENT_PREFIX};
const ADD_RECIPIENT: &str = "add-recipient";
const ADD_IDENTITY: &str = "add-identity";
const WRAP_FILE_KEY: &str = "wrap-file-key";
const EXTENSION_LABELS: &str = "extension-labels";
const RECIPIENT_STANZA: &str = "recipient-stanza";
const LABELS: &str = "labels";

/// The interface that age implementations will use to interact with an age plugin.
///
Expand All @@ -39,6 +42,35 @@ pub trait RecipientPluginV1 {
/// Returns an error if the identity is unknown or invalid.
fn add_identity(&mut self, index: usize, plugin_name: &str, bytes: &[u8]) -> Result<(), Error>;

/// Returns labels that constrain how the stanzas produced by [`Self::wrap_file_keys`]
/// may be combined with those from other recipients.
///
/// Encryption will succeed only if every recipient returns the same set of labels.
/// Subsets or partial overlapping sets are not allowed; all sets must be identical.
/// Labels are compared exactly, and are case-sensitive.
///
/// Label sets can be used to ensure a recipient is only encrypted to alongside other
/// recipients with equivalent properties, or to ensure a recipient is always used
/// alone. A recipient with no particular properties to enforce should return an empty
/// label set.
///
/// Labels can have any value, but usually take one of several forms:
/// - *Common public label* - used by multiple recipients to permit their stanzas to
/// be used only together. Examples include:
/// - `postquantum` - indicates that the recipient stanzas being generated are
/// postquantum-secure, and that they can only be combined with other stanzas
/// that are also postquantum-secure.
/// - *Common private label* - used by recipients created by the same private entity
/// to permit their recipient stanzas to be used only together. For example,
/// private recipients used in a corporate environment could all send the same
/// private label in order to prevent compliant age clients from simultaneously
/// wrapping file keys with other recipients.
/// - *Random label* - used by recipients that want to ensure their stanzas are not
/// used with any other recipient stanzas. This can be used to produce a file key
/// that is only encrypted to a single recipient stanza, for example to preserve
/// its authentication properties.
fn labels(&mut self) -> HashSet<String>;

/// Wraps each `file_key` to all recipients and identities previously added via
/// `add_recipient` and `add_identity`.
///
Expand All @@ -65,6 +97,11 @@ impl RecipientPluginV1 for Infallible {
Ok(())
}

fn labels(&mut self) -> HashSet<String> {
// This is never executed.
HashSet::new()
}

fn wrap_file_keys(
&mut self,
_: Vec<FileKey>,
Expand Down Expand Up @@ -215,8 +252,8 @@ pub(crate) fn run_v1<P: RecipientPluginV1>(mut plugin: P) -> io::Result<()> {
let mut conn = Connection::accept();

// Phase 1: collect recipients, and file keys to be wrapped
let ((recipients, identities), file_keys) = {
let (recipients, identities, file_keys) = conn.unidir_receive(
let ((recipients, identities), file_keys, labels_supported) = {
let (recipients, identities, file_keys, labels_supported) = conn.unidir_receive(
(ADD_RECIPIENT, |s| match (&s.args[..], &s.body[..]) {
([recipient], []) => Ok(recipient.clone()),
_ => Err(Error::Internal {
Expand All @@ -243,6 +280,7 @@ pub(crate) fn run_v1<P: RecipientPluginV1>(mut plugin: P) -> io::Result<()> {
})
.map(FileKey::from)
}),
(Some(EXTENSION_LABELS), |_| Ok(())),
)?;
(
match (recipients, identities) {
Expand All @@ -263,6 +301,13 @@ pub(crate) fn run_v1<P: RecipientPluginV1>(mut plugin: P) -> io::Result<()> {
}]),
r => r,
},
match &labels_supported.unwrap() {
Ok(v) if v.is_empty() => Ok(false),
Ok(v) if v.len() == 1 => Ok(true),
_ => Err(vec![Error::Internal {
message: format!("Received more than one {} command", EXTENSION_LABELS),
}]),
},
)
};

Expand Down Expand Up @@ -327,23 +372,46 @@ pub(crate) fn run_v1<P: RecipientPluginV1>(mut plugin: P) -> io::Result<()> {
|index, plugin_name, bytes| plugin.add_identity(index, plugin_name, &bytes),
);

let required_labels = plugin.labels();

let labels = match (labels_supported, required_labels.is_empty()) {
(Ok(true), _) | (Ok(false), true) => Ok(required_labels),
(Ok(false), false) => Err(vec![Error::Internal {
message: "Plugin requires labels but client does not support them".into(),
}]),
(Err(errors), true) => Err(errors),
(Err(mut errors), false) => {
errors.push(Error::Internal {
message: "Plugin requires labels but client does not support them".into(),
});
Err(errors)
}
};

// Phase 2: wrap the file keys or return errors
conn.bidir_send(|mut phase| {
let (expected_stanzas, file_keys) = match (recipients, identities, file_keys) {
(Ok(recipients), Ok(identities), Ok(file_keys)) => (recipients + identities, file_keys),
(recipients, identities, file_keys) => {
for error in recipients
.err()
.into_iter()
.chain(identities.err())
.chain(file_keys.err())
.flatten()
{
error.send(&mut phase)?;
let (expected_stanzas, file_keys, labels) =
match (recipients, identities, file_keys, labels) {
(Ok(recipients), Ok(identities), Ok(file_keys), Ok(labels)) => {
(recipients + identities, file_keys, labels)
}
return Ok(());
}
};
(recipients, identities, file_keys, labels) => {
for error in recipients
.err()
.into_iter()
.chain(identities.err())
.chain(file_keys.err())
.chain(labels.err())
.flatten()
{
error.send(&mut phase)?;
}
return Ok(());
}
};

let labels = labels.iter().map(|s| s.as_str()).collect::<Vec<_>>();
phase.send(LABELS, &labels, &[])?.unwrap();

match plugin.wrap_file_keys(file_keys, BidirCallbacks(&mut phase))? {
Ok(files) => {
Expand Down

0 comments on commit 608e19f

Please sign in to comment.