Add support for addressing shadow CAs by symbolic names.

- Add a new paramter to `sq pki link add`, `sq pki link authorize`,
    and `sq pki link retract`, `--cert-special`, which allows addressing
    shadow CAs by symbolic names.

  - If the shadow CA doesn't exist yet, we create it.

  - This means `sq pki link authorize --cert-special keys.openpgp.org
    --all --unconstrained` can be used to fully trust the
    `keys.openpgp.org` key server, for instance.  This is more
    convenient, and especially useful for documentation.

  - Fixes #337.
This commit is contained in:
Neal H. Walfield 2024-11-28 14:20:24 +01:00
parent 477f255f84
commit c9bde7fe47
No known key found for this signature in database
GPG Key ID: 6863C9AD5B4D22D3
7 changed files with 273 additions and 23 deletions

10
NEWS
View File

@ -2,6 +2,16 @@
#+TITLE: sequoia-sq NEWS history of user-visible changes
#+STARTUP: content hidestars
* Changes in 0.41.0
** New functionality
** Notable changes
- `sq pki link add`, `sq pki link authorize`, and `sq pki link
retract` gain a new parameter, `--cert-special`, which allows
addressing shadow CAs by symbolic names. For instance, `sq pki
link authorize --cert-special keys.openpgp.org --all
--unconstrained` can be used to fully trust the keys.openpgp.org
key server. This also creates the shadow CA if it doesn't exist
yet.
* Changes in 0.40.0
** New functionality
- New subcommand `sq download`, which downloads a file and a

View File

@ -168,7 +168,7 @@ time.
pub struct AddCommand {
#[command(flatten)]
pub cert: CertDesignators<
cert_designator::CertArg,
cert_designator::CertSpecialArgs,
cert_designator::CertPrefix,
cert_designator::OneValue>,
@ -336,7 +336,7 @@ reference time.
pub struct AuthorizeCommand {
#[command(flatten)]
pub cert: CertDesignators<
cert_designator::CertArg,
cert_designator::CertSpecialArgs,
cert_designator::CertPrefix,
cert_designator::OneValue>,
@ -532,7 +532,7 @@ to force the signature to be re-created anyway.",
#[command(flatten)]
pub cert: CertDesignators<
cert_designator::CertArg,
cert_designator::CertSpecialArgs,
cert_designator::CertPrefix,
cert_designator::OneValue>,

View File

@ -39,6 +39,8 @@ pub use userid_designator::UserIDDesignators;
pub mod expiration;
pub use expiration::Expiration;
pub use expiration::ExpirationArg;
pub mod special_names;
pub use special_names::SpecialName;
pub mod time;
pub use time::Time;
pub mod version;

View File

@ -11,6 +11,8 @@ use sequoia_openpgp as openpgp;
use openpgp::KeyHandle;
use openpgp::packet::UserID;
use crate::cli::types::SpecialName;
/// The prefix for the designators.
///
/// See [`NoPrefix`], [`CertPrefix`], etc.
@ -124,8 +126,11 @@ pub type GrepArg = typenum::U64;
/// This is only used for `sq encrypt`.
pub type WithPasswordArgs = typenum::U128;
/// Adds a `--special` argument.
pub type SpecialArg = typenum::U256;
/// Enables --file, --cert, --userid, --email, --domain, and --grep
/// (i.e., not --with-password, or --with-password-file).
/// (i.e., not --with-password, --with-password-file, --special).
#[allow(dead_code)]
pub type FileCertUserIDEmailDomainGrepArgs
= <<<<<FileArg
@ -136,7 +141,7 @@ pub type FileCertUserIDEmailDomainGrepArgs
as std::ops::BitOr<GrepArg>>::Output;
/// Enables --file, --cert, --userid, --email, and --domain, (i.e.,
/// not --grep, --with-password, or --with-password-file).
/// not --grep, --with-password, --with-password-file, or --special).
#[allow(dead_code)]
pub type FileCertUserIDEmailDomainArgs
= <<<<FileArg
@ -146,7 +151,7 @@ pub type FileCertUserIDEmailDomainArgs
as std::ops::BitOr<DomainArg>>::Output;
/// Enables --cert, --userid, --email, --domain, and --grep (i.e., not
/// --file, --with-password, or --with-password-file).
/// --file, --with-password, --with-password-file, or --special).
pub type CertUserIDEmailDomainGrepArgs
= <<<<CertArg as std::ops::BitOr<UserIDArg>>::Output
as std::ops::BitOr<EmailArg>>::Output
@ -154,20 +159,20 @@ pub type CertUserIDEmailDomainGrepArgs
as std::ops::BitOr<GrepArg>>::Output;
/// Enables --cert, --userid, --email, and --file (i.e., not --domain,
/// --grep, --with-password, or --with-password-file).
/// --grep, --with-password, --with-password-file, or --special).
pub type CertUserIDEmailFileArgs
= <<<CertArg as std::ops::BitOr<UserIDArg>>::Output
as std::ops::BitOr<EmailArg>>::Output
as std::ops::BitOr<FileArg>>::Output;
/// Enables --cert, --userid, and --email (i.e., not --domain,
/// --grep, --file, --with-password, or --with-password-file).
/// Enables --cert, --userid, and --email (i.e., not --domain, --grep,
/// --file, --with-password, --with-password-file, or --special).
pub type CertUserIDEmailArgs
= <<CertArg as std::ops::BitOr<UserIDArg>>::Output
as std::ops::BitOr<EmailArg>>::Output;
/// Enables --cert, --userid, --email, --file, --with-password and
/// --with-password-file (i.e., not --domain, or --grep).
/// --with-password-file (i.e., not --domain, --grep, or --special).
pub type CertUserIDEmailFileWithPasswordArgs
= <<<<CertArg as std::ops::BitOr<UserIDArg>>::Output
as std::ops::BitOr<EmailArg>>::Output
@ -175,9 +180,12 @@ pub type CertUserIDEmailFileWithPasswordArgs
as std::ops::BitOr<WithPasswordArgs>>::Output;
/// Enables --cert, and --file (i.e., not --userid, --email, --domain,
/// --grep, --with-password, or --with-password-file).
/// --grep, --with-password, --with-password-file, or --special).
pub type CertFileArgs = <CertArg as std::ops::BitOr<FileArg>>::Output;
/// Enables --cert, and --special (i.e., not --userid, --email,
/// --domain, --grep, --with-password, or --with-password-file).
pub type CertSpecialArgs = <CertArg as std::ops::BitOr<SpecialArg>>::Output;
/// Argument parser options.
@ -319,6 +327,13 @@ pub enum CertDesignator {
///
/// `--grep`.
Grep(String),
/// Looks up certificates special name.
///
/// This maps special names like keys.openpgp.org to certificates.
///
/// `--special`.
Special(SpecialName),
}
impl CertDesignator {
@ -346,6 +361,7 @@ impl CertDesignator {
Email(_email) => format!("--{}email", prefix),
Domain(_domain) => format!("--{}domain", prefix),
Grep(_pattern) => format!("--{}grep", prefix),
Special(_special) => format!("--{}special", prefix),
}
}
@ -365,6 +381,7 @@ impl CertDesignator {
Email(email) => format!("{} {:?}", argument_name, email),
Domain(domain) => format!("{} {:?}", argument_name, domain),
Grep(pattern) => format!("{} {:?}", argument_name, pattern),
Special(special) => format!("{} {:?}", argument_name, special),
}
}
@ -486,6 +503,7 @@ where
let domain_arg = (arguments & DomainArg::to_usize()) > 0;
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 options = Options::to_usize();
let one_value = (options & OneValue::to_usize()) > 0;
@ -588,6 +606,21 @@ where
arg_group = arg_group.arg(full_name);
}
if special_arg {
let full_name = full_name("special");
cmd = cmd.arg(
clap::Arg::new(&full_name)
.long(&full_name)
.value_name("SPECIAL")
.value_parser(
clap::builder::EnumValueParser::<SpecialName>::new())
.action(action.clone())
.help(Doc::help(
"special",
"Use certificates identified by the special name")));
arg_group = arg_group.arg(full_name);
}
if userid_arg {
let full_name = full_name("userid");
cmd = cmd.arg(
@ -741,6 +774,7 @@ where
let domain_arg = (arguments & DomainArg::to_usize()) > 0;
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 mut designators = Vec::new();
@ -820,6 +854,17 @@ where
}
}
if let Some(Some(names))
= matches.try_get_many::<SpecialName>(&format!("{}special", prefix))
.ok().filter(|_| special_arg)
{
for name in names.cloned() {
designators.push(CertDesignator::Special(name));
}
}
// eprintln!("{:?}", designators);
self.designators = designators;
Ok(())
}
@ -855,6 +900,7 @@ mod test {
($t:ty,
$cert:expr, $userid:expr, $email:expr,
$domain:expr, $grep:expr, $file:expr,
$special:expr,
$with_password:expr) =>
{{
#[derive(Parser, Debug)]
@ -998,6 +1044,22 @@ mod test {
assert!(m.is_err());
}
// Check if --special is recognized.
let m = command.clone().try_get_matches_from(vec![
"prog",
"--special", "keys.openpgp.org",
"--special", "keys.mailvelope.com",
]);
if $special {
let m = m.expect("valid arguments");
let c = CLI::from_arg_matches(&m).expect("ok");
assert_eq!(c.certs.designators.len(), 2);
} else {
assert!(m.is_err());
}
// Check if --with-password is recognized.
let m = command.clone().try_get_matches_from(vec![
"prog",
@ -1029,21 +1091,22 @@ mod test {
}
check!(CertUserIDEmailDomainGrepArgs,
true, true, true, true, true, false, false);
true, true, true, true, true, false, false, false);
check!(CertUserIDEmailFileArgs,
true, true, true, false, false, true, false);
true, true, true, false, false, true, false, false);
check!(CertUserIDEmailFileWithPasswordArgs,
true, true, true, false, false, true, true);
true, true, true, false, false, true, false, true);
// No Args.
check!(typenum::U0,false, false, false, false, false, false, false);
check!(CertArg, true, false, false, false, false, false, false);
check!(UserIDArg, false, true, false, false, false, false, false);
check!(EmailArg, false, false, true, false, false, false, false);
check!(DomainArg, false, false, false, true, false, false, false);
check!(GrepArg, false, false, false, false, true, false, false);
check!(FileArg, false, false, false, false, false, true, false);
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!(WithPasswordArgs,
false, false, false, false, false, false, true);
false, false, false, false, false, false, false, true);
}
#[test]

View File

@ -0,0 +1,81 @@
use std::fmt;
/// Special names are used to identify special certificates.
///
/// Special certificates are created by `sq`. Currently, they
/// correspond to shadow CAs. First, addressing them by fingerprint
/// is annoying. But, since they are created by `sq`, they have a
/// different fingerprint on each system. This makes it possible to
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SpecialName {
PublicDirectories,
KeysOpenpgpOrg,
KeysMailvelopeCom,
ProtonMe,
WKD,
DANE,
Autocrypt,
Web,
// NB: If you add a new variant, be sure to update
// SPECIAL_VARIANTS and SPECIAL_STRINGS!
}
// Ideally SPECIAL_VARIANTS and SPECIAL_VALUES would be a slice of
// tuples. But, because clap needs a slice of names, we split it up.
const SPECIAL_VARIANTS: &'static [SpecialName] = &[
SpecialName::PublicDirectories,
SpecialName::KeysOpenpgpOrg,
SpecialName::KeysMailvelopeCom,
SpecialName::ProtonMe,
SpecialName::WKD,
SpecialName::DANE,
SpecialName::Autocrypt,
SpecialName::Web,
];
const SPECIAL_STRINGS: &'static [&'static str] = &[
"public-directories",
"keys.openpgp.org",
"keys.mailvelope.com",
"proton.me",
"wkd",
"dane",
"autocrypt",
"web",
];
impl fmt::Display for SpecialName {
fn fmt(&self, f: &mut fmt::Formatter<'_>)
-> fmt::Result
{
assert_eq!(SPECIAL_VARIANTS.len(), SPECIAL_STRINGS.len());
for (variant, string)
in SPECIAL_VARIANTS.iter().zip(SPECIAL_STRINGS.iter())
{
if variant == self {
return write!(f, "{}", string);
}
}
panic!("You didn't update SPECIAL_VARIANTS");
}
}
impl clap::ValueEnum for SpecialName {
fn value_variants<'a>() -> &'a [Self] {
SPECIAL_VARIANTS
}
fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
assert_eq!(SPECIAL_VARIANTS.len(), SPECIAL_STRINGS.len());
for (variant, string)
in SPECIAL_VARIANTS.iter().zip(SPECIAL_STRINGS.iter())
{
if variant == self {
return Some(string.into());
}
}
panic!("You didn't update SPECIAL_VARIANTS");
}
}

View File

@ -51,10 +51,11 @@ use keystore::Protection;
use crate::cli::types::CertDesignators;
use crate::cli::types::FileStdinOrKeyHandle;
use crate::cli::types::KeyDesignators;
use crate::cli::types::SpecialName;
use crate::cli::types::StdinWarning;
use crate::cli::types::paths::StateDirectory;
use crate::cli::types::cert_designator;
use crate::cli::types::key_designator;
use crate::cli::types::paths::StateDirectory;
use crate::common::password;
use crate::output::hint::Hint;
use crate::output::import::{ImportStats, ImportStatus};
@ -2027,6 +2028,55 @@ impl<'store: 'rstore, 'rstore> Sq<'store, 'rstore> {
false);
}
}
cert_designator::CertDesignator::Special(name) => {
let certd = match self.certd_or_else() {
Ok(certd) => certd,
Err(err) => {
ret(
designator,
Err(err),
true);
continue;
}
};
let result = match name {
SpecialName::PublicDirectories => {
certd.public_directory_ca()
}
SpecialName::KeysOpenpgpOrg => {
certd.shadow_ca_keys_openpgp_org()
}
SpecialName::KeysMailvelopeCom => {
certd.shadow_ca_keys_mailvelope_com()
}
SpecialName::ProtonMe => {
certd.shadow_ca_proton_me()
}
SpecialName::WKD => {
certd.shadow_ca_wkd()
}
SpecialName::DANE => {
certd.shadow_ca_dane()
}
SpecialName::Autocrypt => {
certd.shadow_ca_autocrypt()
}
SpecialName::Web => {
certd.shadow_ca_web()
}
};
ret(
designator,
result
.map(|(cert, _created)| cert)
.with_context(|| {
format!("Looking up special certificate {}",
name)
}),
true);
}
}
}

View File

@ -808,3 +808,47 @@ fn no_ambiguous_email() {
&[], alice.key_handle(),
&[UserIDArg::AddUserID("<alice@example.org>")]);
}
#[test]
fn special_names() {
// Check that --cert-special works.
let sq = Sq::new();
let check = |cmd: &str, args: &[&str], name: &str, success: bool| {
let mut c = sq.command();
c.args([ "pki", "link", cmd, "--cert-special", name ]);
c.args(args);
sq.run(c, Some(success));
};
const SPECIAL_STRINGS: &'static [&'static str] = &[
"public-directories",
"keys.openpgp.org",
"keys.mailvelope.com",
"proton.me",
"wkd",
"dane",
"autocrypt",
"web",
];
for name in SPECIAL_STRINGS.iter() {
check("add", &["--all"], name, true);
}
check("add", &["--all"], "xxx", false);
for name in SPECIAL_STRINGS.iter() {
check("retract", &[], name, true);
}
check("retract", &[], "xxx", false);
for name in SPECIAL_STRINGS.iter() {
check("authorize", &["--all", "--unconstrained"], name, true);
}
check("authorize", &["--all", "--unconstrained"], "xxx", false);
for name in SPECIAL_STRINGS.iter() {
check("retract", &[], name, true);
}
check("retract", &[], "xxx", false);
}