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:
parent
477f255f84
commit
c9bde7fe47
10
NEWS
10
NEWS
@ -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
|
||||
|
@ -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>,
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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]
|
||||
|
81
src/cli/types/special_names.rs
Normal file
81
src/cli/types/special_names.rs
Normal 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");
|
||||
}
|
||||
}
|
52
src/sq.rs
52
src/sq.rs
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user