Add new subcommand sq pki authorize.
- Previously `sq pki certify` could create certifications, and mark a certificate as a trusted introducer (when the user set `--depth` to be greater than zero). Anecdotal evidence indicates that combining these two actions in a single command is confusing. - Split the latter functionality off, and put it in a new subcommand, `sq pki authorize`. - See https://gitlab.com/sequoia-pgp/sequoia-sq/-/issues/249#note_1865470753
This commit is contained in:
parent
bea0a5b732
commit
22284ed9b1
4
NEWS
4
NEWS
@ -65,6 +65,10 @@
|
||||
- `sq pki certify`'s positional argument for specifying the
|
||||
certificate to certify must now be specified using a named
|
||||
argument, `--cert` or `--cert-file`.
|
||||
- Previously `sq pki certify` could create certifications, and mark
|
||||
a certificate as a trusted introducer (when the user set
|
||||
`--depth` to be greater than zero). The latter functionality has
|
||||
been split off to the new subcommand `sq pki authorize`.
|
||||
|
||||
* Changes in 0.38.0
|
||||
** Notable changes
|
||||
|
@ -11,6 +11,7 @@ use openpgp::packet::UserID;
|
||||
|
||||
use crate::cli::types::TrustAmount;
|
||||
|
||||
pub mod authorize;
|
||||
pub mod certify;
|
||||
pub mod link;
|
||||
|
||||
@ -55,6 +56,7 @@ pub enum Subcommands {
|
||||
Lookup(LookupCommand),
|
||||
Identify(IdentifyCommand),
|
||||
Certify(certify::Command),
|
||||
Authorize(authorize::Command),
|
||||
Link(link::Command),
|
||||
List(ListCommand),
|
||||
Path(PathCommand),
|
||||
|
223
src/cli/pki/authorize.rs
Normal file
223
src/cli/pki/authorize.rs
Normal file
@ -0,0 +1,223 @@
|
||||
//! Command-line parser for `sq pki authorize`.
|
||||
|
||||
use clap::ArgGroup;
|
||||
use clap::Parser;
|
||||
|
||||
use sequoia_openpgp as openpgp;
|
||||
use openpgp::KeyHandle;
|
||||
|
||||
use crate::cli::THIRD_PARTY_CERTIFICATION_VALIDITY_DURATION;
|
||||
use crate::cli::THIRD_PARTY_CERTIFICATION_VALIDITY_IN_YEARS;
|
||||
|
||||
use crate::cli::types::CertDesignators;
|
||||
use crate::cli::types::ClapData;
|
||||
use crate::cli::types::Expiration;
|
||||
use crate::cli::types::FileOrStdin;
|
||||
use crate::cli::types::FileOrStdout;
|
||||
use crate::cli::types::TrustAmount;
|
||||
use crate::cli::types::cert_designator::CertFileArgs;
|
||||
use crate::cli::types::cert_designator::CertPrefix;
|
||||
use crate::cli::types::cert_designator::NoPrefix;
|
||||
use crate::cli::types::cert_designator::OneValue;
|
||||
use crate::cli::types::cert_designator::OptionalValue;
|
||||
use crate::cli::types::cert_designator::UserIDEmailArgs;
|
||||
|
||||
use crate::cli::examples::*;
|
||||
|
||||
const AUTHORIZE_EXAMPLES: Actions = Actions {
|
||||
actions: &[
|
||||
],
|
||||
};
|
||||
test_examples!(sq_pki_authorize, AUTHORIZE_EXAMPLES);
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(
|
||||
name = "authorize",
|
||||
about = "Mark a certificate as a trusted introducer",
|
||||
long_about = format!(
|
||||
"Mark a certificate as a trusted introducer.
|
||||
|
||||
Creates a certification that says that the issuer considers the \
|
||||
certificate to be a trusted introducer. Trusted introducer is another \
|
||||
word for certification authority (CA). When a user relies on a \
|
||||
trusted introducer, the user considers certifications made by the \
|
||||
trusted introducer to be valid. A trusted introducer can also \
|
||||
designate further trusted introducers.
|
||||
|
||||
As is, a trusted introducer has a lot of power. This power can be \
|
||||
limited in several ways.
|
||||
|
||||
- The ability to specify further introducers can be constrained \
|
||||
using the `--depth` parameter.
|
||||
|
||||
- The degree to which an introducer is trusted can be changed using \
|
||||
the `--amount` parameter.
|
||||
|
||||
- The user IDs that an introducer can certify can be constrained by \
|
||||
a regular expression using the `--regex` parameter.
|
||||
|
||||
These mechanisms allow Alice to say that she is willing to rely on the \
|
||||
CA for example.org, but only for user IDs that have an email address \
|
||||
for example.org, for instance.
|
||||
|
||||
By default a delegation expires after {} years. Use the `--expiration` \
|
||||
argument to override this.
|
||||
|
||||
This subcommand respects the reference time set by the top-level \
|
||||
`--time` argument. It sets the certification's creation time to the \
|
||||
reference time.
|
||||
",
|
||||
THIRD_PARTY_CERTIFICATION_VALIDITY_IN_YEARS,
|
||||
),
|
||||
after_help = AUTHORIZE_EXAMPLES,
|
||||
)]
|
||||
#[clap(group(ArgGroup::new("certifier_input").args(&["certifier_file", "certifier"]).required(true)))]
|
||||
#[clap(group(ArgGroup::new("constraint").args(&["regex", "unconstrained"]).required(true)))]
|
||||
pub struct Command {
|
||||
#[clap(
|
||||
long,
|
||||
value_name = "KEY",
|
||||
help = "Create the certification using KEY.",
|
||||
)]
|
||||
pub certifier: Option<KeyHandle>,
|
||||
#[clap(
|
||||
long,
|
||||
value_name = "KEY-FILE",
|
||||
help = "Create the certification using KEY-FILE.",
|
||||
)]
|
||||
pub certifier_file: Option<FileOrStdin>,
|
||||
|
||||
#[command(flatten)]
|
||||
pub cert: CertDesignators<CertFileArgs, CertPrefix, OneValue>,
|
||||
|
||||
#[command(flatten)]
|
||||
pub userids: CertDesignators<UserIDEmailArgs, NoPrefix, OptionalValue>,
|
||||
#[clap(
|
||||
long,
|
||||
help = "Add the given user ID if it doesn't exist.",
|
||||
long_help =
|
||||
"Add the given user ID if it doesn't exist in the certificate.",
|
||||
)]
|
||||
pub add_userid: bool,
|
||||
|
||||
#[clap(
|
||||
long = "amount",
|
||||
value_name = "AMOUNT",
|
||||
default_value = "full",
|
||||
help = "Set the amount of trust",
|
||||
long_help =
|
||||
"Set the amount of trust. Values between 1 and 120 are meaningful. \
|
||||
120 means fully trusted. Values less than 120 indicate the degree \
|
||||
of trust. 60 is usually used for partially trusted.",
|
||||
)]
|
||||
pub amount: TrustAmount<u8>,
|
||||
|
||||
#[clap(
|
||||
long = "depth",
|
||||
value_name = "TRUST_DEPTH",
|
||||
default_value = "1",
|
||||
help = "Set the trust depth",
|
||||
long_help =
|
||||
"Set the trust depth (sometimes referred to as the trust level). \
|
||||
1 means CERTIFICATE is a trusted introducer (default), 2 means \
|
||||
CERTIFICATE is a meta-trusted introducer and can authorize \
|
||||
another trusted introducer, etc.",
|
||||
)]
|
||||
pub depth: u8,
|
||||
|
||||
#[clap(
|
||||
long = "regex",
|
||||
value_name = "REGEX",
|
||||
help = "Add a regular expression to constrain the introducer",
|
||||
long_help = "\
|
||||
Add a regular expression to constrain the introducer.
|
||||
|
||||
Add a regular expression to constrain what certifications are \
|
||||
respected. A certification made by the certificate is only respected \
|
||||
if it is over a user ID that matches one of the specified regular \
|
||||
expression. Multiple regular expressions may be specified. In that \
|
||||
case, at least one must match.",
|
||||
)]
|
||||
pub regex: Vec<String>,
|
||||
#[clap(
|
||||
long,
|
||||
conflicts_with = "regex",
|
||||
help = "Don't constrain the introducer",
|
||||
long_help = "\
|
||||
Don't constrain the introducer.
|
||||
|
||||
Normally an introducer is constrained so that only certain user IDs \
|
||||
are respected, e.g., those that have an email address for a certain \
|
||||
domain name. This option authorizes an introducer without \
|
||||
constraining it in this way. Because this grants the introducer a lot \
|
||||
of power, you have to opt in to this behavior explicitly.",
|
||||
)]
|
||||
pub unconstrained: bool,
|
||||
|
||||
#[clap(
|
||||
long = "local",
|
||||
help = "Make the certification a local certification",
|
||||
long_help =
|
||||
"Make the certification a local \
|
||||
certification. Normally, local \
|
||||
certifications are not exported.",
|
||||
)]
|
||||
pub local: bool,
|
||||
#[clap(
|
||||
long = "non-revocable",
|
||||
help = "Mark the certification as being non-revocable",
|
||||
long_help =
|
||||
"Mark the certification as being non-revocable. \
|
||||
That is, you cannot later revoke this \
|
||||
certification. This should normally only \
|
||||
be used with an expiration.",
|
||||
)]
|
||||
pub non_revocable: bool,
|
||||
|
||||
#[clap(
|
||||
long = "expiration",
|
||||
value_name = "EXPIRATION",
|
||||
default_value_t =
|
||||
Expiration::Duration(THIRD_PARTY_CERTIFICATION_VALIDITY_DURATION),
|
||||
help =
|
||||
"Define EXPIRATION for the certification as ISO 8601 formatted string or \
|
||||
custom duration.",
|
||||
long_help =
|
||||
"Define EXPIRATION for the certification as ISO 8601 formatted string or \
|
||||
custom duration. \
|
||||
If an ISO 8601 formatted string is provided, the validity period \
|
||||
reaches from the reference time (may be set using `--time`) to \
|
||||
the provided time. \
|
||||
Custom durations starting from the reference time may be set using \
|
||||
`N[ymwds]`, for N years, months, weeks, days, or seconds. \
|
||||
The special keyword `never` sets an unlimited expiry.",
|
||||
)]
|
||||
pub expiration: Expiration,
|
||||
|
||||
#[clap(
|
||||
long,
|
||||
value_names = &["NAME", "VALUE"],
|
||||
number_of_values = 2,
|
||||
help = "Add a notation to the certification.",
|
||||
long_help = "Add a notation to the certification. \
|
||||
A user-defined notation's name must be of the form \
|
||||
`name@a.domain.you.control.org`. If the notation's name starts \
|
||||
with a !, then the notation is marked as being critical. If a \
|
||||
consumer of a signature doesn't understand a critical notation, \
|
||||
then it will ignore the signature. The notation is marked as \
|
||||
being human readable."
|
||||
)]
|
||||
pub notation: Vec<String>,
|
||||
|
||||
#[clap(
|
||||
help = FileOrStdout::HELP_OPTIONAL,
|
||||
long,
|
||||
value_name = FileOrStdout::VALUE_NAME,
|
||||
)]
|
||||
pub output: Option<FileOrStdout>,
|
||||
#[clap(
|
||||
long,
|
||||
help = "Emit binary data",
|
||||
)]
|
||||
pub binary: bool,
|
||||
}
|
@ -131,37 +131,6 @@ pub struct Command {
|
||||
)]
|
||||
pub amount: TrustAmount<u8>,
|
||||
|
||||
#[clap(
|
||||
long = "depth",
|
||||
value_name = "TRUST_DEPTH",
|
||||
default_value = "0",
|
||||
help = "Set the trust depth",
|
||||
long_help =
|
||||
"Set the trust depth (sometimes referred to as the trust level). \
|
||||
0 means a normal certification of <CERTIFICATE, USERID>. \
|
||||
1 means CERTIFICATE is also a trusted introducer, 2 means \
|
||||
CERTIFICATE is a meta-trusted introducer, etc.",
|
||||
)]
|
||||
pub depth: u8,
|
||||
|
||||
#[clap(
|
||||
long = "regex",
|
||||
value_name = "REGEX",
|
||||
requires = "depth",
|
||||
help = "Add a regular expression to constrain \
|
||||
what a trusted introducer can certify",
|
||||
long_help =
|
||||
"Add a regular expression to constrain \
|
||||
what a trusted introducer can certify. \
|
||||
The regular expression must match \
|
||||
the certified User ID in all intermediate \
|
||||
introducers, and the certified certificate. \
|
||||
Multiple regular expressions may be \
|
||||
specified. In that case, at least \
|
||||
one must match.",
|
||||
)]
|
||||
pub regex: Vec<String>,
|
||||
|
||||
#[clap(
|
||||
long = "expiration",
|
||||
value_name = "EXPIRATION",
|
||||
|
@ -14,6 +14,7 @@ use sequoia_wot as wot;
|
||||
use wot::store::Backend;
|
||||
use wot::store::Store;
|
||||
|
||||
pub mod authorize;
|
||||
pub mod certify;
|
||||
pub mod link;
|
||||
pub mod output;
|
||||
@ -656,6 +657,9 @@ pub fn dispatch(sq: Sq, cli: cli::pki::Command) -> Result<()> {
|
||||
Subcommands::Certify(command) =>
|
||||
self::certify::certify(sq, command)?,
|
||||
|
||||
Subcommands::Authorize(command) =>
|
||||
self::authorize::authorize(sq, command)?,
|
||||
|
||||
Subcommands::Link(command) =>
|
||||
self::link::link(sq, command)?,
|
||||
}
|
||||
|
161
src/commands/pki/authorize.rs
Normal file
161
src/commands/pki/authorize.rs
Normal file
@ -0,0 +1,161 @@
|
||||
use sequoia_openpgp as openpgp;
|
||||
use openpgp::packet::UserID;
|
||||
use openpgp::Result;
|
||||
use openpgp::types::KeyFlags;
|
||||
|
||||
use crate::Sq;
|
||||
use crate::cli::pki::authorize;
|
||||
use crate::cli::types::FileStdinOrKeyHandle;
|
||||
use crate::cli::types::cert_designator::CertDesignator;
|
||||
use crate::commands::FileOrStdout;
|
||||
use crate::parse_notations;
|
||||
|
||||
pub fn authorize(sq: Sq, mut c: authorize::Command)
|
||||
-> Result<()>
|
||||
{
|
||||
let certifier: FileStdinOrKeyHandle = if let Some(file) = c.certifier_file {
|
||||
assert!(c.certifier.is_none());
|
||||
file.into()
|
||||
} else if let Some(kh) = c.certifier {
|
||||
kh.into()
|
||||
} else {
|
||||
panic!("clap enforces --certifier or --certifier-file is set");
|
||||
};
|
||||
|
||||
let certifier = sq.lookup_one(
|
||||
certifier, Some(KeyFlags::empty().set_certification()), true)?;
|
||||
|
||||
let (cert, from_file) = sq.resolve_cert(&c.cert, sequoia_wot::FULLY_TRUSTED)?;
|
||||
if from_file {
|
||||
// If the cert is read from a file, we default to stdout.
|
||||
// (None means write to the cert store.)
|
||||
if c.output.is_none() {
|
||||
c.output = Some(FileOrStdout::new(None));
|
||||
}
|
||||
}
|
||||
|
||||
let vc = cert.with_policy(sq.policy, Some(sq.time))?;
|
||||
|
||||
// Find the matching User ID.
|
||||
let mut userids = Vec::new();
|
||||
|
||||
// Don't stop at the first error.
|
||||
let mut missing = false;
|
||||
let mut bad = None;
|
||||
|
||||
for designator in c.userids.iter() {
|
||||
match designator {
|
||||
CertDesignator::UserID(userid) => {
|
||||
let userid = UserID::from(&userid[..]);
|
||||
|
||||
// If --add-userid is specified, we use the user ID as
|
||||
// is. Otherwise, we make sure there is a matching
|
||||
// self-signed user ID.
|
||||
if c.add_userid {
|
||||
userids.push(userid.clone());
|
||||
} else if let Some(_) = vc.userids()
|
||||
.find(|ua| {
|
||||
ua.userid() == &userid
|
||||
})
|
||||
{
|
||||
userids.push(userid.clone());
|
||||
} else {
|
||||
wprintln!("{:?} is not a self-signed user ID.",
|
||||
String::from_utf8_lossy(userid.value()));
|
||||
missing = true;
|
||||
}
|
||||
}
|
||||
CertDesignator::Email(email) => {
|
||||
// Validate the email address.
|
||||
let userid = match UserID::from_address(None, None, email) {
|
||||
Ok(userid) => userid,
|
||||
Err(err) => {
|
||||
wprintln!("{:?} is not a valid email address: {}",
|
||||
email, err);
|
||||
bad = Some(err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Extract a normalized version for comparison
|
||||
// purposes.
|
||||
let email_normalized = match userid.email_normalized() {
|
||||
Ok(Some(email)) => email,
|
||||
Ok(None) => {
|
||||
wprintln!("{:?} is not a valid email address", email);
|
||||
bad = Some(anyhow::anyhow!(format!(
|
||||
"{:?} is not a valid email address", email)));
|
||||
continue;
|
||||
}
|
||||
Err(err) => {
|
||||
wprintln!("{:?} is not a valid email address: {}",
|
||||
email, err);
|
||||
bad = Some(err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Find any the matching self-signed user IDs.
|
||||
let mut found = false;
|
||||
for ua in vc.userids() {
|
||||
if Some(&email_normalized)
|
||||
== ua.email_normalized().unwrap_or(None).as_ref()
|
||||
{
|
||||
userids.push(ua.userid().clone());
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ! found {
|
||||
if c.add_userid {
|
||||
// Add the bare email address.
|
||||
userids.push(userid);
|
||||
} else {
|
||||
eprintln!("The email address {:?} does not match any \
|
||||
user IDs.",
|
||||
email);
|
||||
missing = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => unreachable!("enforced by clap"),
|
||||
}
|
||||
}
|
||||
|
||||
if missing || userids.is_empty() {
|
||||
// Use all self-signed User IDs.
|
||||
userids = vc.userids()
|
||||
.map(|ua| ua.userid().clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if userids.is_empty() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"{} has no self-signed user IDs, and you didn't provide \
|
||||
an alternate user ID",
|
||||
vc.fingerprint()));
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(err) = bad {
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
let notations = parse_notations(&c.notation)?;
|
||||
|
||||
crate::common::pki::certify::certify(
|
||||
&sq,
|
||||
true, // Always recreate.
|
||||
&certifier,
|
||||
&cert,
|
||||
&userids[..],
|
||||
c.add_userid,
|
||||
true, // User supplied user IDs.
|
||||
&[(c.amount, c.expiration)],
|
||||
c.depth,
|
||||
&c.regex[..],
|
||||
c.local,
|
||||
c.non_revocable,
|
||||
¬ations[..],
|
||||
c.output,
|
||||
c.binary)
|
||||
}
|
@ -154,8 +154,8 @@ pub fn certify(sq: Sq, mut c: certify::Command)
|
||||
c.add_userid,
|
||||
true, // User supplied user IDs.
|
||||
&[(c.amount, c.expiration)],
|
||||
c.depth,
|
||||
&c.regex[..],
|
||||
0,
|
||||
&[][..],
|
||||
c.local,
|
||||
c.non_revocable,
|
||||
¬ations[..],
|
||||
|
@ -175,6 +175,7 @@ pub fn certify(sq: &Sq,
|
||||
-> Result<()>
|
||||
{
|
||||
assert!(templates.len() > 0);
|
||||
assert!(userids.len() > 0);
|
||||
make_qprintln!(sq.quiet);
|
||||
|
||||
if trust_depth == 0 && !regex.is_empty() {
|
||||
|
@ -21,6 +21,7 @@ mod integration {
|
||||
mod sq_key_userid;
|
||||
mod sq_pki;
|
||||
mod sq_pki_certify;
|
||||
mod sq_pki_authorize;
|
||||
mod sq_pki_link;
|
||||
mod sq_sign;
|
||||
mod sq_toolbox_keyring_filter;
|
||||
|
@ -1386,6 +1386,108 @@ impl Sq {
|
||||
.expect("success")
|
||||
}
|
||||
|
||||
/// Try to make an authorization.
|
||||
///
|
||||
/// If `output_file` is `Some`, then the output is written to that
|
||||
/// file. Otherwise, the default behavior is followed.
|
||||
pub fn pki_authorize_p<'a, H, C, Q>(&self, extra_args: &[&str],
|
||||
certifier: H,
|
||||
cert: C,
|
||||
userids: &[&str],
|
||||
output_file: Q,
|
||||
success: bool)
|
||||
-> Result<Cert>
|
||||
where H: Into<FileOrKeyHandle>,
|
||||
C: Into<FileOrKeyHandle>,
|
||||
Q: Into<Option<&'a Path>>,
|
||||
{
|
||||
let certifier = certifier.into();
|
||||
let cert = cert.into();
|
||||
let output_file = output_file.into();
|
||||
|
||||
let mut cmd = self.command();
|
||||
cmd.args([ "pki", "authorize" ]);
|
||||
for arg in extra_args {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
match &certifier {
|
||||
FileOrKeyHandle::FileOrStdin(file) => {
|
||||
cmd.arg("--certifier-file").arg(file);
|
||||
}
|
||||
FileOrKeyHandle::KeyHandle((_kh, s)) => {
|
||||
cmd.arg("--certifier").arg(s);
|
||||
}
|
||||
}
|
||||
match &cert {
|
||||
FileOrKeyHandle::FileOrStdin(file) => {
|
||||
cmd.arg("--cert-file").arg(file);
|
||||
}
|
||||
FileOrKeyHandle::KeyHandle((_kh, s)) => {
|
||||
cmd.arg("--cert").arg(s);
|
||||
}
|
||||
}
|
||||
for userid in userids {
|
||||
cmd.arg("--userid").arg(userid);
|
||||
}
|
||||
|
||||
if let Some(output_file) = output_file {
|
||||
cmd.arg("--overwrite").arg("--output").arg(output_file);
|
||||
}
|
||||
|
||||
let output = self.run(cmd, Some(success));
|
||||
if output.status.success() {
|
||||
if let Some(output_file) = output_file {
|
||||
// The output was explicitly written to a file.
|
||||
if output_file == &PathBuf::from("-") {
|
||||
Ok(Cert::from_bytes(&output.stdout)
|
||||
.expect("can parse certificate"))
|
||||
} else {
|
||||
Ok(Cert::from_file(&output_file)
|
||||
.expect("can parse certificate"))
|
||||
}
|
||||
} else {
|
||||
match cert {
|
||||
FileOrKeyHandle::FileOrStdin(_) => {
|
||||
// When the cert is from a file, the output is
|
||||
// written to stdout by default.
|
||||
Ok(Cert::from_bytes(&output.stdout)
|
||||
.with_context(|| {
|
||||
format!("Importing result from the file {:?}",
|
||||
cert)
|
||||
})
|
||||
.expect("can parse certificate"))
|
||||
}
|
||||
FileOrKeyHandle::KeyHandle((kh, _s)) => {
|
||||
// When the cert is from the cert store, the
|
||||
// output is written to the cert store by
|
||||
// default.
|
||||
Ok(self.cert_export(kh.clone()))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Err(anyhow::anyhow!(format!(
|
||||
"Failed (expected):\n{}",
|
||||
String::from_utf8_lossy(&output.stderr))))
|
||||
}
|
||||
}
|
||||
|
||||
/// Authorize a certificate.
|
||||
pub fn pki_authorize<'a, H, C, Q>(&self, extra_args: &[&str],
|
||||
certifier: H,
|
||||
cert: C,
|
||||
userids: &[&str],
|
||||
output_file: Q)
|
||||
-> Cert
|
||||
where H: Into<FileOrKeyHandle>,
|
||||
C: Into<FileOrKeyHandle>,
|
||||
Q: Into<Option<&'a Path>>,
|
||||
{
|
||||
self.pki_authorize_p(
|
||||
extra_args, certifier, cert, userids, output_file, true)
|
||||
.expect("success")
|
||||
}
|
||||
|
||||
/// Add a link for the binding.
|
||||
pub fn pki_link_add_maybe(&self, extra_args: &[&str],
|
||||
cert: KeyHandle, userid: &str)
|
||||
@ -1416,6 +1518,29 @@ impl Sq {
|
||||
self.pki_link_add_maybe(args, cert, userid).expect("success")
|
||||
}
|
||||
|
||||
/// Authenticate a binding.
|
||||
pub fn pki_authenticate(&self, extra_args: &[&str],
|
||||
cert: &str, userid: &str)
|
||||
-> Result<()>
|
||||
{
|
||||
let mut cmd = self.command();
|
||||
cmd.args([ "pki", "authenticate" ]);
|
||||
for arg in extra_args {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
cmd.arg(cert);
|
||||
cmd.arg(userid);
|
||||
|
||||
let output = self.run(cmd, None);
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow::anyhow!(format!(
|
||||
"Command failed:\n{}",
|
||||
String::from_utf8_lossy(&output.stderr))))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sign<'a, H, Q>(&self,
|
||||
signer: H,
|
||||
password_file: Option<&Path>,
|
||||
|
182
tests/integration/sq_pki_authorize.rs
Normal file
182
tests/integration/sq_pki_authorize.rs
Normal file
@ -0,0 +1,182 @@
|
||||
use super::common::Sq;
|
||||
|
||||
#[test]
|
||||
fn sq_pki_authorize_then_authenticate() {
|
||||
let mut sq = Sq::new();
|
||||
|
||||
let otto_somewhere_com = "<otto@somewhere.com>";
|
||||
let (otto, otto_pgp, _otto_rev)
|
||||
= sq.key_generate(&[], &[otto_somewhere_com]);
|
||||
sq.key_import(&otto_pgp);
|
||||
|
||||
let ca_example_org = "<ca@example.org>";
|
||||
let (ca, ca_pgp, _ca_rev)
|
||||
= sq.key_generate(&[], &[ca_example_org]);
|
||||
sq.key_import(&ca_pgp);
|
||||
|
||||
let alice_example_org = "<alice@example.org>";
|
||||
let (alice, alice_pgp, _alice_rev)
|
||||
= sq.key_generate(&[], &[alice_example_org]);
|
||||
sq.key_import(&alice_pgp);
|
||||
|
||||
let bob_example_org = "<bob@example.org>";
|
||||
let bob_other_org = "<bob@other.org>";
|
||||
let (bob, bob_pgp, _bob_rev)
|
||||
= sq.key_generate(&[], &[bob_example_org, bob_other_org]);
|
||||
sq.key_import(&bob_pgp);
|
||||
|
||||
sq.tick(1);
|
||||
|
||||
// The ca certifies alice's and bob's certificates for each of
|
||||
// their user IDs.
|
||||
let certification = sq.scratch_file(None);
|
||||
sq.pki_certify(&[],
|
||||
ca.key_handle(), alice.key_handle(),
|
||||
&[ alice_example_org ],
|
||||
certification.as_path());
|
||||
sq.cert_import(&certification);
|
||||
|
||||
let certification = sq.scratch_file(None);
|
||||
sq.pki_certify(&[],
|
||||
ca.key_handle(), bob.key_handle(),
|
||||
&[ bob_example_org, bob_other_org ],
|
||||
certification.as_path());
|
||||
sq.cert_import(certification);
|
||||
|
||||
// Check whether we can authenticate alice's and bob's
|
||||
// certificates for their user ID using otto as the trust root.
|
||||
let check = |sq: &Sq,
|
||||
can_authenticate_alice,
|
||||
can_authenticate_bob1,
|
||||
can_authenticate_bob2|
|
||||
{
|
||||
for (cert, userid, can_authenticate) in &[
|
||||
(&alice, alice_example_org, can_authenticate_alice),
|
||||
(&bob, bob_example_org, can_authenticate_bob1),
|
||||
(&bob, bob_other_org, can_authenticate_bob2),
|
||||
]
|
||||
{
|
||||
let r = sq.pki_authenticate(
|
||||
&["--trust-root", &otto.fingerprint().to_string() ],
|
||||
&cert.fingerprint().to_string(),
|
||||
userid);
|
||||
|
||||
match (can_authenticate, r.is_ok()) {
|
||||
(true, false) => {
|
||||
panic!("Expected to authenticated {}, but didn't.",
|
||||
userid);
|
||||
}
|
||||
(false, true) => {
|
||||
panic!("Expected to NOT authenticated {}, but did.",
|
||||
userid);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// No delegation yet.
|
||||
println!("CA: not authorized");
|
||||
check(
|
||||
&sq,
|
||||
false, // Alice.
|
||||
false, // bob@example.org
|
||||
false);// bob@other.org
|
||||
|
||||
// Otto authorizes completely to the CA.
|
||||
let certification = sq.scratch_file(None);
|
||||
sq.tick(1);
|
||||
sq.pki_authorize(&["--unconstrained"],
|
||||
otto.key_handle(), ca.key_handle(),
|
||||
&[],
|
||||
certification.as_path());
|
||||
sq.cert_import(certification);
|
||||
|
||||
println!("CA: authorized, and unconstrained");
|
||||
check(
|
||||
&sq,
|
||||
true, // alice@example.org
|
||||
true, // bob@example.org
|
||||
true);// bob@other.org
|
||||
|
||||
// Otto authorizes to the CA with the regex contraint "example".
|
||||
let certification = sq.scratch_file(None);
|
||||
sq.tick(1);
|
||||
sq.pki_authorize(&["--regex", "example"],
|
||||
otto.key_handle(), ca.key_handle(),
|
||||
&[],
|
||||
certification.as_path());
|
||||
sq.cert_import(certification);
|
||||
|
||||
println!("CA: authorized for \"example\"");
|
||||
check(
|
||||
&sq,
|
||||
true, // alice@example.org
|
||||
true, // bob@example.org
|
||||
false);// bob@other.org
|
||||
|
||||
// Otto authorizes to the CA with the regex contraint "other".
|
||||
let certification = sq.scratch_file(None);
|
||||
sq.tick(1);
|
||||
sq.pki_authorize(&["--regex", "other"],
|
||||
otto.key_handle(), ca.key_handle(),
|
||||
&[],
|
||||
certification.as_path());
|
||||
sq.cert_import(certification);
|
||||
|
||||
println!("CA: authorized for \"other\"");
|
||||
check(
|
||||
&sq,
|
||||
false, // alice@example.org
|
||||
false, // bob@example.org
|
||||
true); // bob@other.org
|
||||
|
||||
// Otto authorizes to the CA with the regex contraint "bob".
|
||||
let certification = sq.scratch_file(None);
|
||||
sq.tick(1);
|
||||
sq.pki_authorize(&["--regex", "bob"],
|
||||
otto.key_handle(), ca.key_handle(),
|
||||
&[],
|
||||
certification.as_path());
|
||||
sq.cert_import(certification);
|
||||
|
||||
println!("CA: authorized for \"bob\"");
|
||||
check(
|
||||
&sq,
|
||||
false, // alice@example.org
|
||||
true, // bob@example.org
|
||||
true); // bob@other.org
|
||||
|
||||
// Otto authorizes to the CA with the regex contraint "bob" or "alice".
|
||||
let certification = sq.scratch_file(None);
|
||||
sq.tick(1);
|
||||
sq.pki_authorize(&["--regex", "bob", "--regex", "alice"],
|
||||
otto.key_handle(), ca.key_handle(),
|
||||
&[],
|
||||
certification.as_path());
|
||||
sq.cert_import(certification);
|
||||
|
||||
println!("CA: authorized for \"bob\" or \"alice\"");
|
||||
check(
|
||||
&sq,
|
||||
true, // alice@example.org
|
||||
true, // bob@example.org
|
||||
true);// bob@other.org
|
||||
|
||||
|
||||
// Otto authorizes to the CA with the regex contraint "zoo".
|
||||
let certification = sq.scratch_file(None);
|
||||
sq.tick(1);
|
||||
sq.pki_authorize(&["--regex", "zoo"],
|
||||
otto.key_handle(), ca.key_handle(),
|
||||
&[],
|
||||
certification.as_path());
|
||||
sq.cert_import(certification);
|
||||
|
||||
println!("CA: authorized for \"zoo\"");
|
||||
check(
|
||||
&sq,
|
||||
false, // alice@example.org
|
||||
false, // bob@example.org
|
||||
false);// bob@other.org
|
||||
}
|
@ -117,7 +117,7 @@ fn sq_pki_certify() -> Result<()> {
|
||||
// Have alice certify <bob@example.org> for 0xB0B.
|
||||
sq.tick(1);
|
||||
let bob_pgp_new = sq.scratch_file(None);
|
||||
let cert = sq.pki_certify(
|
||||
let cert = sq.pki_authorize(
|
||||
&["--depth", "10",
|
||||
"--amount", "5",
|
||||
"--regex", "a",
|
||||
|
@ -136,7 +136,7 @@ fn sq_retract(sq: &Sq, cert: &str, userids: &[&str])
|
||||
// The certification is imported into the cert store.
|
||||
fn sq_certify(sq: &Sq,
|
||||
certifier: &str, cert: &str, userid: &str,
|
||||
trust_amount: Option<usize>, depth: Option<usize>)
|
||||
trust_amount: Option<usize>)
|
||||
{
|
||||
let mut extra_args = Vec::new();
|
||||
let trust_amount_;
|
||||
@ -145,6 +145,32 @@ fn sq_certify(sq: &Sq,
|
||||
trust_amount_ = format!("{}", trust_amount);
|
||||
extra_args.push(&trust_amount_);
|
||||
}
|
||||
|
||||
let certification = sq.scratch_file(Some(&format!(
|
||||
"certification {} {} by {}", cert, userid, certifier)[..]));
|
||||
|
||||
let cert = if let Ok(kh) = cert.parse::<KeyHandle>() {
|
||||
kh.into()
|
||||
} else {
|
||||
FileOrKeyHandle::FileOrStdin(cert.into())
|
||||
};
|
||||
|
||||
sq.pki_certify(&extra_args, certifier, cert, &[userid],
|
||||
Some(certification.as_path()));
|
||||
sq.cert_import(&certification);
|
||||
}
|
||||
|
||||
fn sq_authorize(sq: &Sq,
|
||||
certifier: &str, cert: &str, userid: &str,
|
||||
trust_amount: Option<usize>, depth: Option<usize>)
|
||||
{
|
||||
let mut extra_args = vec![ "--unconstrained" ];
|
||||
let trust_amount_;
|
||||
if let Some(trust_amount) = trust_amount {
|
||||
extra_args.push("--amount");
|
||||
trust_amount_ = format!("{}", trust_amount);
|
||||
extra_args.push(&trust_amount_);
|
||||
}
|
||||
let depth_;
|
||||
if let Some(depth) = depth {
|
||||
extra_args.push("--depth");
|
||||
@ -161,8 +187,8 @@ fn sq_certify(sq: &Sq,
|
||||
FileOrKeyHandle::FileOrStdin(cert.into())
|
||||
};
|
||||
|
||||
sq.pki_certify(&extra_args, certifier, cert, &[userid],
|
||||
Some(certification.as_path()));
|
||||
sq.pki_authorize(&extra_args, certifier, cert, &[userid],
|
||||
Some(certification.as_path()));
|
||||
sq.cert_import(&certification);
|
||||
}
|
||||
|
||||
@ -244,12 +270,12 @@ fn sq_pki_link_add_retract() -> Result<()> {
|
||||
|
||||
// Have Alice certify Bob as a trusted introducer and have Bob
|
||||
// certify Carol.
|
||||
sq_certify(&sq, &alice.key_file,
|
||||
&bob.cert.fingerprint().to_string(), bob_userid,
|
||||
None, Some(1));
|
||||
sq_authorize(&sq, &alice.key_file,
|
||||
&bob.cert.fingerprint().to_string(), bob_userid,
|
||||
None, Some(1));
|
||||
sq_certify(&sq, &bob.key_file,
|
||||
&carol.cert.fingerprint().to_string(), carol_userid,
|
||||
None, None);
|
||||
None);
|
||||
|
||||
// We should be able to verify Alice's, Bob's and Carol's
|
||||
// signatures Alice as the trust root. And Bob's and Carols' with
|
||||
@ -312,7 +338,7 @@ fn sq_pki_link_add_retract() -> Result<()> {
|
||||
// verify.
|
||||
sq_certify(&sq, &bob.key_file,
|
||||
&dave.cert.fingerprint().to_string(), dave_userid,
|
||||
None, None);
|
||||
None);
|
||||
|
||||
sq_verify(&sq, None, &[], &[], &alice.sig_file, 1, 0);
|
||||
sq_verify(&sq, None, &[], &[], &bob.sig_file, 1, 0);
|
||||
|
Loading…
Reference in New Issue
Block a user