Implement sq encrypt --for-self.

- This adds a mechanism to add a list of certificates presumably
    owned by the user to the recipients using the `--for-self` flag.
    This makes sure the encrypted message can be decrypted again.

  - Fixes #461.
This commit is contained in:
Justus Winter 2024-11-28 18:07:10 +01:00
parent abafa552f0
commit b88367ce36
No known key found for this signature in database
GPG Key ID: 686F55B4AB2B3386
6 changed files with 181 additions and 16 deletions

2
NEWS
View File

@ -4,6 +4,8 @@
* Changes in 0.41.0
** New functionality
- `sq encrypt --for-self` now adds the certs configured under
`encrypt.for-self` to the list of recipients.
** Notable changes
- `sq pki link add`, `sq pki link authorize`, and `sq pki link
retract` gain a new parameter, `--cert-special`, which allows

View File

@ -63,13 +63,18 @@ pub type Augmentations = BTreeMap<&'static str, String>;
/// Includes values from the config file in help messages.
pub fn augment_help(key: &'static str, text: &str) -> String {
if let Some(a) = AUGMENTATIONS.get().and_then(|a| a.get(key)) {
if let Some(a) = get_augmentation(key) {
format!("{}\n\n[config: {}] (overrides default)", text, a)
} else {
text.into()
}
}
/// Returns the value of an augmentation, if any.
pub fn get_augmentation(key: &'static str) -> Option<&str> {
AUGMENTATIONS.get().and_then(|a| a.get(key).map(|v| v.as_str()))
}
/// Includes values from the config file in help messages.
pub fn set_augmentations(augmentations: Augmentations) {
AUGMENTATIONS.set(augmentations)

View File

@ -13,6 +13,9 @@ use crate::cli::types::cert_designator::*;
use crate::cli::examples;
use examples::*;
/// Key for the help augmentation.
pub const ENCRYPT_FOR_SELF: &str = "encrypt.for-self";
const ENCRYPT_EXAMPLES: Actions = Actions {
actions: &[
Action::Setup(Setup {
@ -83,7 +86,7 @@ pub struct Command {
pub binary: bool,
#[command(flatten)]
pub recipients: CertDesignators<CertUserIDEmailFileWithPasswordArgs,
pub recipients: CertDesignators<CertUserIDEmailFileSelfWithPasswordArgs,
RecipientPrefix>,
#[clap(

View File

@ -11,6 +11,8 @@ use sequoia_openpgp as openpgp;
use openpgp::KeyHandle;
use openpgp::packet::UserID;
use crate::cli::config;
use crate::cli::encrypt::ENCRYPT_FOR_SELF;
use crate::cli::types::SpecialName;
/// The prefix for the designators.
@ -129,6 +131,9 @@ pub type WithPasswordArgs = typenum::U128;
/// Adds a `--special` argument.
pub type SpecialArg = typenum::U256;
/// Adds a `--self` argument.
pub type SelfArg = typenum::U512;
/// Enables --file, --cert, --userid, --email, --domain, and --grep
/// (i.e., not --with-password, --with-password-file, --special).
#[allow(dead_code)]
@ -179,6 +184,12 @@ pub type CertUserIDEmailFileWithPasswordArgs
as std::ops::BitOr<FileArg>>::Output
as std::ops::BitOr<WithPasswordArgs>>::Output;
/// Enables --cert, --userid, --email, --file, --self, --with-password
/// and --with-password-file (i.e., not --domain, --grep, or
/// --special).
pub type CertUserIDEmailFileSelfWithPasswordArgs =
<CertUserIDEmailFileWithPasswordArgs as std::ops::BitOr<SelfArg>>::Output;
/// Enables --cert, and --file (i.e., not --userid, --email, --domain,
/// --grep, --with-password, --with-password-file, or --special).
pub type CertFileArgs = <CertArg as std::ops::BitOr<FileArg>>::Output;
@ -334,6 +345,15 @@ pub enum CertDesignator {
///
/// `--special`.
Special(SpecialName),
/// Use the configured set of certificates presumably belonging to
/// oneself.
///
/// This is used to add ones own certificates as encryption
/// recipients.
///
/// `--self`.
Self_,
}
impl CertDesignator {
@ -362,6 +382,7 @@ impl CertDesignator {
Domain(_domain) => format!("--{}domain", prefix),
Grep(_pattern) => format!("--{}grep", prefix),
Special(_special) => format!("--{}special", prefix),
Self_ => format!("--{}self", prefix),
}
}
@ -382,6 +403,7 @@ impl CertDesignator {
Domain(domain) => format!("{} {:?}", argument_name, domain),
Grep(pattern) => format!("{} {:?}", argument_name, pattern),
Special(special) => format!("{} {:?}", argument_name, special),
Self_ => argument_name,
}
}
@ -504,6 +526,7 @@ where
let grep_arg = (arguments & GrepArg::to_usize()) > 0;
let with_password_args = (arguments & WithPasswordArgs::to_usize()) > 0;
let special_arg = (arguments & SpecialArg::to_usize()) > 0;
let self_arg = (arguments & SelfArg::to_usize()) > 0;
let options = Options::to_usize();
let one_value = (options & OneValue::to_usize()) > 0;
@ -698,6 +721,35 @@ where
arg_group = arg_group.arg(full_name);
}
if self_arg {
let full_name = full_name("self");
let long_help = format!(
"Encrypt the message for yourself
This adds the certificates listed in the configuration file under \
`{}` to the list of recipients. \
This can be used to make sure that you yourself can decrypt the message.
{}
",
ENCRYPT_FOR_SELF,
if let Some(certs) = config::get_augmentation(ENCRYPT_FOR_SELF) {
format!("The following certs will be added: {}.", certs)
} else {
"Currently, the list of certificates to be added is empty."
.into()
});
cmd = cmd.arg(
clap::Arg::new(&full_name)
.long(&full_name)
.action(clap::ArgAction::SetTrue)
.help(Doc::help(
"self",
"Encrypt the message for yourself"))
.long_help(long_help));
arg_group = arg_group.arg(full_name);
}
if with_password_args {
let full_name = "with-password";
let arg = clap::Arg::new(full_name)
@ -775,6 +827,7 @@ where
let grep_arg = (arguments & GrepArg::to_usize()) > 0;
let with_password_args = (arguments & WithPasswordArgs::to_usize()) > 0;
let special_arg = (arguments & SpecialArg::to_usize()) > 0;
let self_arg = (arguments & SelfArg::to_usize()) > 0;
let mut designators = Vec::new();
@ -863,6 +916,10 @@ where
}
}
if self_arg && matches.get_flag(&format!("{}self", prefix)) {
designators.push(CertDesignator::Self_);
}
// eprintln!("{:?}", designators);
self.designators = designators;
@ -900,6 +957,7 @@ mod test {
($t:ty,
$cert:expr, $userid:expr, $email:expr,
$domain:expr, $grep:expr, $file:expr,
$self: expr,
$special:expr,
$with_password:expr) =>
{{
@ -1045,6 +1103,20 @@ mod test {
}
// Check if --self is recognized.
let m = command.clone().try_get_matches_from(vec![
"prog",
"--self",
]);
if $self {
let m = m.expect("valid arguments");
let c = CLI::from_arg_matches(&m).expect("ok");
assert_eq!(c.certs.designators.len(), 1);
} else {
assert!(m.is_err());
}
// Check if --special is recognized.
let m = command.clone().try_get_matches_from(vec![
"prog",
@ -1091,22 +1163,25 @@ mod test {
}
check!(CertUserIDEmailDomainGrepArgs,
true, true, true, true, true, false, false, false);
true, true, true, true, true, false, false, false, false);
check!(CertUserIDEmailFileArgs,
true, true, true, false, false, true, false, false);
true, true, true, false, false, true, false, false, false);
check!(CertUserIDEmailFileWithPasswordArgs,
true, true, true, false, false, true, false, true);
true, true, true, false, false, true, false, false, true);
check!(CertUserIDEmailFileSelfWithPasswordArgs,
true, true, true, false, false, true, true, false, true);
// No Args.
check!(typenum::U0,false, false, false, false, false, false, false, false);
check!(CertArg, true, false, false, false, false, false, false, false);
check!(UserIDArg, false, true, false, false, false, false, false, false);
check!(EmailArg, false, false, true, false, false, false, false, false);
check!(DomainArg, false, false, false, true, false, false, false, false);
check!(GrepArg, false, false, false, false, true, false, false, false);
check!(FileArg, false, false, false, false, false, true, false, false);
check!(SpecialArg, false, false, false, false, false, false, true, false);
check!(typenum::U0,false, false, false, false, false, false, false, false, false);
check!(CertArg, true, false, false, false, false, false, false, false, false);
check!(UserIDArg, false, true, false, false, false, false, false, false, false);
check!(EmailArg, false, false, true, false, false, false, false, false, false);
check!(DomainArg, false, false, false, true, false, false, false, false, false);
check!(GrepArg, false, false, false, false, true, false, false, false, false);
check!(FileArg, false, false, false, false, false, true, false, false, false);
check!(SelfArg, false, false, false, false, false, false, true, false, false);
check!(SpecialArg, false, false, false, false, false, false, false, true, false);
check!(WithPasswordArgs,
false, false, false, false, false, false, false, true);
false, false, false, false, false, false, false, false, true);
}
#[test]

View File

@ -19,7 +19,10 @@ use toml_edit::{
Value,
};
use sequoia_openpgp::policy::StandardPolicy;
use sequoia_openpgp::{
Fingerprint,
policy::StandardPolicy,
};
use sequoia_net::reqwest::Url;
use sequoia_directories::{Component, Home};
use sequoia_policy_config::ConfiguredStandardPolicy;
@ -36,6 +39,7 @@ use crate::{
/// It is available as `Sq::config`, with suitable accessors that
/// handle the precedence of the various sources.
pub struct Config {
encrypt_for_self: Vec<Fingerprint>,
policy_path: Option<PathBuf>,
policy_inline: Option<Vec<u8>>,
cipher_suite: Option<sequoia_openpgp::cert::CipherSuite>,
@ -45,6 +49,7 @@ pub struct Config {
impl Default for Config {
fn default() -> Self {
Config {
encrypt_for_self: vec![],
policy_path: None,
policy_inline: None,
cipher_suite: None,
@ -54,6 +59,12 @@ impl Default for Config {
}
impl Config {
/// Returns the certificates that should be added to the list of
/// recipients if `encrypt --for-self` is given.
pub fn encrypt_for_self(&self) -> &[Fingerprint] {
&self.encrypt_for_self
}
/// Returns the cryptographic policy.
///
/// We read in the default policy configuration, the configuration
@ -137,6 +148,9 @@ impl ConfigFile {
# Configuration template for sq <SQ-VERSION>
<SQ-CONFIG-PATH-HINT>
[encrypt]
#for-self = [\"fingerprint of your key\"]
[key.generate]
#cipher-suite = <DEFAULT-CIPHER-SUITE>
@ -525,11 +539,59 @@ type Schema = &'static [(&'static str, Applicator)];
/// Schema for the toplevel.
const TOP_LEVEL_SCHEMA: Schema = &[
("encrypt", apply_encrypt),
("key", apply_key),
("network", apply_network),
("policy", apply_policy),
];
/// Schema for the `encrypt` section.
const ENCRYPT_SCHEMA: Schema = &[
("for-self", apply_encrypt_for_self),
];
/// Validates the `encrypt` section.
fn apply_encrypt(config: &mut Option<&mut Config>, cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let section = item.as_table_like()
.ok_or_else(|| Error::bad_item_type(path, item, "table"))?;
apply_schema(config, cli, Some(path), section.iter(), ENCRYPT_SCHEMA)?;
Ok(())
}
/// Validates the `encrypt.for-self` value.
fn apply_encrypt_for_self(config: &mut Option<&mut Config>,
cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let list = item.as_array()
.ok_or_else(|| Error::bad_item_type(path, item, "array"))?;
let mut strs = Vec::new();
let mut values = Vec::new();
for (i, server) in list.iter().enumerate() {
let s = server.as_str()
.ok_or_else(|| Error::bad_value_type(&format!("{}.{}", path, i),
server, "string"))?;
strs.push(s);
values.push(s.parse::<Fingerprint>()?);
}
if let Some(cli) = cli {
cli.insert("encrypt.for-self", strs.join(" "));
}
if let Some(config) = config {
config.encrypt_for_self = values;
}
Ok(())
}
/// Schema for the `key` section.
const KEY_SCHEMA: Schema = &[
("generate", apply_key_generate),

View File

@ -48,6 +48,7 @@ use wot::store::Store as _;
use sequoia_keystore as keystore;
use keystore::Protection;
use crate::cli;
use crate::cli::types::CertDesignators;
use crate::cli::types::FileStdinOrKeyHandle;
use crate::cli::types::KeyDesignators;
@ -2079,7 +2080,24 @@ impl<'store: 'rstore, 'rstore> Sq<'store, 'rstore> {
name)
}),
true);
},
cert_designator::CertDesignator::Self_ => {
if self.config.encrypt_for_self().is_empty() {
return Err(anyhow::anyhow!(
"`--for-self` is given but the list of \
certificates in `{}` is empty",
cli::encrypt::ENCRYPT_FOR_SELF));
}
for fp in self.config.encrypt_for_self() {
let cert = self.resolve_cert(
&openpgp::KeyHandle::from(fp.clone()).into(), 0)?.0;
ret(designator,
Ok(Arc::new(cert.into())),
true);
}
},
}
}