Support addressing recipients by email address and User ID
- Extend `sq encrypt` with the `--recipient-email` and `--recipient-userid` arguments to allow the caller to designate a certificate by email address or User ID, respectively. An email address or User ID is considered to designate a certificate, if the binding between the email address or User ID and the certificate can be authenticated using the web of trust. - Add support for the web of trust using the `sequoia-wot` crate. - Add a top-level option, `--trust-root`, to allow the user to specify trust roots.
This commit is contained in:
parent
62e6b4cb8b
commit
6c7b0de5c0
74
Cargo.lock
generated
74
Cargo.lock
generated
@ -477,6 +477,16 @@ dependencies = [
|
|||||||
"os_str_bytes",
|
"os_str_bytes",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_mangen"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cc872a4bca8ddf10be882b81d36f1c2817e43c5c59862ac25f401af581dc4181"
|
||||||
|
dependencies = [
|
||||||
|
"clap 4.0.32",
|
||||||
|
"roff",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cmac"
|
name = "cmac"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@ -818,6 +828,12 @@ version = "0.3.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
|
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dot-writer"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d1b11bd5e7e98406c6ff39fbc94d6e910a489b978ce7f17c19fce91a1195b7a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dyn-clone"
|
name = "dyn-clone"
|
||||||
version = "1.0.5"
|
version = "1.0.5"
|
||||||
@ -920,6 +936,16 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "enumber"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fa35b49b30d8f4219e279f22c4b7c899aa7f98f475da4eff84b75f17ba11ed19"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "env_logger"
|
name = "env_logger"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@ -2821,6 +2847,20 @@ dependencies = [
|
|||||||
"xxhash-rust",
|
"xxhash-rust",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sequoia-policy-config"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "12f41f8b29fdc21666e6a49d7d7a9c4396f83b11052de0e5434b35aebf302075"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"chrono",
|
||||||
|
"sequoia-openpgp",
|
||||||
|
"serde",
|
||||||
|
"thiserror",
|
||||||
|
"toml",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sequoia-sq"
|
name = "sequoia-sq"
|
||||||
version = "0.28.0"
|
version = "0.28.0"
|
||||||
@ -2843,6 +2883,7 @@ dependencies = [
|
|||||||
"sequoia-cert-store",
|
"sequoia-cert-store",
|
||||||
"sequoia-net",
|
"sequoia-net",
|
||||||
"sequoia-openpgp",
|
"sequoia-openpgp",
|
||||||
|
"sequoia-wot",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"subplot-build",
|
"subplot-build",
|
||||||
@ -2852,6 +2893,30 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sequoia-wot"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eb08c5484e6265aa8cd19d523c09e88f575fc040bf9e6dfdbbeeff3ed0ba07d0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"chrono",
|
||||||
|
"clap 4.0.32",
|
||||||
|
"clap_complete",
|
||||||
|
"clap_mangen",
|
||||||
|
"crossbeam",
|
||||||
|
"dot-writer",
|
||||||
|
"enumber",
|
||||||
|
"lazy_static",
|
||||||
|
"num_cpus",
|
||||||
|
"openpgp-cert-d",
|
||||||
|
"sequoia-cert-store",
|
||||||
|
"sequoia-openpgp",
|
||||||
|
"sequoia-policy-config",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.137"
|
version = "1.0.137"
|
||||||
@ -3430,6 +3495,15 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.5.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower-service"
|
name = "tower-service"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
|
@ -34,13 +34,13 @@ dirs = "4"
|
|||||||
sequoia-openpgp = { version = "1.13", default-features = false, features = ["compression-deflate"] }
|
sequoia-openpgp = { version = "1.13", default-features = false, features = ["compression-deflate"] }
|
||||||
sequoia-autocrypt = { version = "0.25", default-features = false, optional = true }
|
sequoia-autocrypt = { version = "0.25", default-features = false, optional = true }
|
||||||
sequoia-net = { version = "0.26", default-features = false }
|
sequoia-net = { version = "0.26", default-features = false }
|
||||||
#sequoia-wot = { version = "0.6" }
|
|
||||||
anyhow = "1.0.18"
|
anyhow = "1.0.18"
|
||||||
chrono = "0.4.10"
|
chrono = "0.4.10"
|
||||||
clap = { version = "4", features = ["derive", "env", "wrap_help"] }
|
clap = { version = "4", features = ["derive", "env", "wrap_help"] }
|
||||||
itertools = "0.10"
|
itertools = "0.10"
|
||||||
once_cell = "1.17"
|
once_cell = "1.17"
|
||||||
sequoia-cert-store = "0.2"
|
sequoia-cert-store = "0.2"
|
||||||
|
sequoia-wot = "0.7"
|
||||||
tempfile = "3.1"
|
tempfile = "3.1"
|
||||||
term_size = "0.3"
|
term_size = "0.3"
|
||||||
tokio = { version = "1.13.1" }
|
tokio = { version = "1.13.1" }
|
||||||
|
5
NEWS
5
NEWS
@ -34,6 +34,11 @@
|
|||||||
option replaces the various subcommand's `--time` argument as
|
option replaces the various subcommand's `--time` argument as
|
||||||
well as `sq key generate` and `sq key userid add`'s
|
well as `sq key generate` and `sq key userid add`'s
|
||||||
`--creation-time` arguments.
|
`--creation-time` arguments.
|
||||||
|
- Add top-level option, `--trust-root`, to allow the user to
|
||||||
|
specify trust roots.
|
||||||
|
- Extend `sq encrypt` to allow addressing recipients by User ID
|
||||||
|
(`--recipient-userid`) or email address (`--recipient-email`).
|
||||||
|
Only User IDs that can be fully authenticated are considered.
|
||||||
* Deprecated functionality
|
* Deprecated functionality
|
||||||
- `sq key generate --creation-time TIME` is deprecated in favor of
|
- `sq key generate --creation-time TIME` is deprecated in favor of
|
||||||
`sq key generate --time TIME`.
|
`sq key generate --time TIME`.
|
||||||
|
249
src/sq.rs
249
src/sq.rs
@ -27,6 +27,7 @@ use openpgp::{
|
|||||||
};
|
};
|
||||||
use openpgp::{armor, Cert};
|
use openpgp::{armor, Cert};
|
||||||
use openpgp::crypto::Password;
|
use openpgp::crypto::Password;
|
||||||
|
use openpgp::Fingerprint;
|
||||||
use openpgp::packet::prelude::*;
|
use openpgp::packet::prelude::*;
|
||||||
use openpgp::parse::{Parse, PacketParser, PacketParserResult};
|
use openpgp::parse::{Parse, PacketParser, PacketParserResult};
|
||||||
use openpgp::packet::signature::subpacket::NotationData;
|
use openpgp::packet::signature::subpacket::NotationData;
|
||||||
@ -35,10 +36,15 @@ use openpgp::serialize::{Serialize, stream::{Message, Armorer}};
|
|||||||
use openpgp::cert::prelude::*;
|
use openpgp::cert::prelude::*;
|
||||||
use openpgp::policy::StandardPolicy as P;
|
use openpgp::policy::StandardPolicy as P;
|
||||||
use openpgp::types::KeyFlags;
|
use openpgp::types::KeyFlags;
|
||||||
|
use openpgp::types::RevocationStatus;
|
||||||
|
|
||||||
use sequoia_cert_store as cert_store;
|
use sequoia_cert_store as cert_store;
|
||||||
use cert_store::Store;
|
use cert_store::Store;
|
||||||
use cert_store::store::StoreError;
|
use cert_store::store::StoreError;
|
||||||
|
use cert_store::store::UserIDQueryParams;
|
||||||
|
|
||||||
|
use sequoia_wot as wot;
|
||||||
|
use wot::store::Store as _;
|
||||||
|
|
||||||
use clap::FromArgMatches;
|
use clap::FromArgMatches;
|
||||||
use crate::sq_cli::packet;
|
use crate::sq_cli::packet;
|
||||||
@ -572,9 +578,9 @@ impl<'store> Config<'store> {
|
|||||||
|
|
||||||
if let Some(keyflags) = keyflags.as_ref() {
|
if let Some(keyflags) = keyflags.as_ref() {
|
||||||
certs.retain(|cert| {
|
certs.retain(|cert| {
|
||||||
// XXX: Respect any subcommand-specific
|
let vc = match cert.with_policy(
|
||||||
// reference time.
|
&self.policy, self.time)
|
||||||
let vc = match cert.with_policy(&self.policy, None) {
|
{
|
||||||
Ok(vc) => vc,
|
Ok(vc) => vc,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let err = err.context(
|
let err = err.context(
|
||||||
@ -636,6 +642,235 @@ impl<'store> Config<'store> {
|
|||||||
Ok(results)
|
Ok(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Looks up certificates by User ID or email address.
|
||||||
|
///
|
||||||
|
/// This only returns certificates that can be authenticate for
|
||||||
|
/// the specified User ID (or email address, if `email` is true).
|
||||||
|
/// If no certificate can be authenticated for some User ID,
|
||||||
|
/// returns an error. If multiple certificates can be
|
||||||
|
/// authenticated for a given User ID or email address, then
|
||||||
|
/// returns them all.
|
||||||
|
fn lookup_by_userid(&self, trust_roots: &[Fingerprint],
|
||||||
|
userid: &[String], email: bool)
|
||||||
|
-> Result<Vec<Cert>>
|
||||||
|
{
|
||||||
|
if userid.is_empty() {
|
||||||
|
return Ok(Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
let cert_store = self.cert_store_or_else()?;
|
||||||
|
|
||||||
|
// Build a WoT network.
|
||||||
|
|
||||||
|
let cert_store = wot::store::CertStore::from_store(
|
||||||
|
cert_store, &self.policy, self.time);
|
||||||
|
let n = wot::Network::new(&cert_store)?;
|
||||||
|
let mut q = wot::QueryBuilder::new(&n);
|
||||||
|
q.roots(wot::Roots::new(trust_roots.iter().cloned()));
|
||||||
|
let q = q.build();
|
||||||
|
|
||||||
|
let mut results: Vec<Cert> = Vec::new();
|
||||||
|
// We try hard to not just stop at the first error, but lint
|
||||||
|
// the input so that the user gets as much feedback as
|
||||||
|
// possible. The first error that we encounter is saved here,
|
||||||
|
// and returned. The rest are printed directly.
|
||||||
|
let mut error: Option<anyhow::Error> = None;
|
||||||
|
|
||||||
|
// Iterate over each User ID address, find any certificates
|
||||||
|
// associated with the User ID, validate the certificates, and
|
||||||
|
// finally authenticate them for the User ID.
|
||||||
|
for userid in userid.iter() {
|
||||||
|
let matches: Vec<(Fingerprint, UserID)> = if email {
|
||||||
|
if let Err(err) = UserIDQueryParams::is_email(userid) {
|
||||||
|
eprintln!("{:?} is not a valid email address", userid);
|
||||||
|
if error.is_none() {
|
||||||
|
error = Some(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all certificates that are associated with the email
|
||||||
|
// address.
|
||||||
|
cert_store.lookup_synopses_by_email(userid)
|
||||||
|
} else {
|
||||||
|
let userid = UserID::from(&userid[..]);
|
||||||
|
cert_store.lookup_synopses_by_userid(userid.clone())
|
||||||
|
.into_iter()
|
||||||
|
.map(|fpr| (fpr, userid.clone()))
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
if matches.is_empty() {
|
||||||
|
if error.is_none() {
|
||||||
|
error = Some(anyhow::anyhow!(
|
||||||
|
"No certificates are associated with {:?}",
|
||||||
|
userid));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Entry {
|
||||||
|
fpr: Fingerprint,
|
||||||
|
userid: UserID,
|
||||||
|
cert: Result<Cert>,
|
||||||
|
}
|
||||||
|
let entries = matches.into_iter().map(|(fpr, userid)| {
|
||||||
|
// We've got a match, or two, or three... Lookup the certs.
|
||||||
|
let cert = match cert_store.lookup_by_cert_fpr(&fpr) {
|
||||||
|
Ok(cert) => cert,
|
||||||
|
Err(err) => {
|
||||||
|
let err = err.context(format!(
|
||||||
|
"Error fetching {} ({:?})",
|
||||||
|
fpr, String::from_utf8_lossy(userid.value())));
|
||||||
|
return Entry { fpr, userid, cert: Err(err), };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse the LazyCerts.
|
||||||
|
let cert = match cert.into_owned().into_cert() {
|
||||||
|
Ok(cert) => cert,
|
||||||
|
Err(err) => {
|
||||||
|
let err = err.context(format!(
|
||||||
|
"Error parsing {} ({:?})",
|
||||||
|
fpr, String::from_utf8_lossy(userid.value())));
|
||||||
|
return Entry { fpr, userid, cert: Err(err), };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check the certs for validity.
|
||||||
|
let vc = match cert.with_policy(&self.policy, self.time) {
|
||||||
|
Ok(vc) => vc,
|
||||||
|
Err(err) => {
|
||||||
|
let err = err.context(format!(
|
||||||
|
"Certificate {} ({:?}) is invalid",
|
||||||
|
fpr, String::from_utf8_lossy(userid.value())));
|
||||||
|
return Entry { fpr, userid, cert: Err(err) };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = vc.alive() {
|
||||||
|
let err = err.context(format!(
|
||||||
|
"Certificate {} ({:?}) is invalid",
|
||||||
|
fpr, String::from_utf8_lossy(userid.value())));
|
||||||
|
return Entry { fpr, userid, cert: Err(err), };
|
||||||
|
}
|
||||||
|
|
||||||
|
if let RevocationStatus::Revoked(_) = vc.revocation_status() {
|
||||||
|
let err = anyhow::anyhow!(
|
||||||
|
"Certificate {} ({:?}) is revoked",
|
||||||
|
fpr, String::from_utf8_lossy(userid.value()));
|
||||||
|
return Entry { fpr, userid, cert: Err(err), };
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ua) = vc.userids().find(|ua| {
|
||||||
|
ua.userid() == &userid
|
||||||
|
})
|
||||||
|
{
|
||||||
|
if let RevocationStatus::Revoked(_) = ua.revocation_status() {
|
||||||
|
let err = anyhow::anyhow!(
|
||||||
|
"User ID {:?} on certificate {} is revoked",
|
||||||
|
String::from_utf8_lossy(userid.value()), fpr);
|
||||||
|
return Entry { fpr, userid, cert: Err(err), };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate the bindings.
|
||||||
|
let paths = q.authenticate(
|
||||||
|
&userid, cert.fingerprint(),
|
||||||
|
// XXX: Make this user configurable.
|
||||||
|
wot::FULLY_TRUSTED);
|
||||||
|
let r = if paths.amount() < wot::FULLY_TRUSTED {
|
||||||
|
Err(anyhow::anyhow!(
|
||||||
|
"{}, {:?} cannot be authenticated at the \
|
||||||
|
required level ({} of {}). After checking \
|
||||||
|
that {} really controls {}, you could certify \
|
||||||
|
their certificate by running \
|
||||||
|
`sq certify MY_KEY.pgp {} {}`.",
|
||||||
|
cert.fingerprint(),
|
||||||
|
String::from_utf8_lossy(userid.value()),
|
||||||
|
paths.amount(), wot::FULLY_TRUSTED,
|
||||||
|
String::from_utf8_lossy(userid.value()),
|
||||||
|
cert.fingerprint(),
|
||||||
|
cert.fingerprint(),
|
||||||
|
String::from_utf8_lossy(userid.value())))
|
||||||
|
} else {
|
||||||
|
Ok(cert)
|
||||||
|
};
|
||||||
|
|
||||||
|
Entry { fpr, userid, cert: r, }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Partition into good (successfully authenticated) and
|
||||||
|
// bad (an error occurred).
|
||||||
|
let (good, bad): (Vec<Entry>, _)
|
||||||
|
= entries.partition(|entry| entry.cert.is_ok());
|
||||||
|
|
||||||
|
if good.is_empty() {
|
||||||
|
// We've only got errors.
|
||||||
|
|
||||||
|
let err = if bad.is_empty() {
|
||||||
|
// We got nothing :/.
|
||||||
|
if email {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"No known certificates have the email address {:?}",
|
||||||
|
userid)
|
||||||
|
} else {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"No known certificates have the User ID {:?}",
|
||||||
|
userid)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if email {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"None of the certificates with the email \
|
||||||
|
address {:?} can be authenticated using \
|
||||||
|
the configured trust model",
|
||||||
|
userid)
|
||||||
|
} else {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"None of the certificates with the User ID \
|
||||||
|
{:?} can be authenticated using \
|
||||||
|
the configured trust model",
|
||||||
|
userid)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
eprintln!("{:?}:\n", err);
|
||||||
|
if error.is_none() {
|
||||||
|
error = Some(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the errors.
|
||||||
|
for (i, Entry { fpr, userid, cert }) in bad.into_iter().enumerate() {
|
||||||
|
eprintln!("{}. When considering {} ({}):",
|
||||||
|
i + 1, fpr,
|
||||||
|
String::from_utf8_lossy(userid.value()));
|
||||||
|
let err = match cert {
|
||||||
|
Ok(_) => unreachable!(),
|
||||||
|
Err(err) => err,
|
||||||
|
};
|
||||||
|
|
||||||
|
print_error_chain(&err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We have at least one authenticated certificate.
|
||||||
|
// Silently ignore any errors.
|
||||||
|
results.extend(
|
||||||
|
good.into_iter().filter_map(|Entry { cert, .. }| {
|
||||||
|
cert.ok()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(error) = error {
|
||||||
|
Err(error)
|
||||||
|
} else {
|
||||||
|
Ok(results)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Looks up a certificate.
|
/// Looks up a certificate.
|
||||||
///
|
///
|
||||||
/// Like `lookup`, but looks up a certificate, which must be
|
/// Like `lookup`, but looks up a certificate, which must be
|
||||||
@ -765,6 +1000,14 @@ fn main() -> Result<()> {
|
|||||||
true,
|
true,
|
||||||
false)
|
false)
|
||||||
.context("--recipient-cert")?);
|
.context("--recipient-cert")?);
|
||||||
|
recipients.extend(
|
||||||
|
config.lookup_by_userid(&c.trust_roots,
|
||||||
|
&command.recipients_email, true)
|
||||||
|
.context("--recipient-email")?);
|
||||||
|
recipients.extend(
|
||||||
|
config.lookup_by_userid(&c.trust_roots,
|
||||||
|
&command.recipients_userid, false)
|
||||||
|
.context("--recipient-userid")?);
|
||||||
let mut input = open_or_stdin(command.io.input.as_deref())?;
|
let mut input = open_or_stdin(command.io.input.as_deref())?;
|
||||||
|
|
||||||
let output = config.create_or_stdout_pgp(
|
let output = config.create_or_stdout_pgp(
|
||||||
|
@ -45,6 +45,21 @@ pub struct Command {
|
|||||||
help = "Emits binary data",
|
help = "Emits binary data",
|
||||||
)]
|
)]
|
||||||
pub binary: bool,
|
pub binary: bool,
|
||||||
|
|
||||||
|
#[clap(
|
||||||
|
long = "recipient-email",
|
||||||
|
value_name = "EMAIL",
|
||||||
|
help = "Encrypts to all certificates that can be authenticated \
|
||||||
|
for the specified email address",
|
||||||
|
)]
|
||||||
|
pub recipients_email: Vec<String>,
|
||||||
|
#[clap(
|
||||||
|
long = "recipient-userid",
|
||||||
|
value_name = "USERID",
|
||||||
|
help = "Encrypts to all certificates that can be authenticated \
|
||||||
|
for the specified User ID",
|
||||||
|
)]
|
||||||
|
pub recipients_userid: Vec<String>,
|
||||||
#[clap(
|
#[clap(
|
||||||
long = "recipient-cert",
|
long = "recipient-cert",
|
||||||
value_name = "FINGERPRINT|KEYID",
|
value_name = "FINGERPRINT|KEYID",
|
||||||
@ -57,6 +72,7 @@ pub struct Command {
|
|||||||
help = "Encrypts to all certificates in CERT_RING_FILE",
|
help = "Encrypts to all certificates in CERT_RING_FILE",
|
||||||
)]
|
)]
|
||||||
pub recipients_file: Vec<String>,
|
pub recipients_file: Vec<String>,
|
||||||
|
|
||||||
#[clap(
|
#[clap(
|
||||||
long = "signer-file",
|
long = "signer-file",
|
||||||
value_name = "KEY_FILE",
|
value_name = "KEY_FILE",
|
||||||
|
@ -6,6 +6,9 @@ use clap::{Command, CommandFactory, Parser, Subcommand};
|
|||||||
#[cfg(feature = "autocrypt")]
|
#[cfg(feature = "autocrypt")]
|
||||||
pub mod autocrypt;
|
pub mod autocrypt;
|
||||||
|
|
||||||
|
use sequoia_openpgp as openpgp;
|
||||||
|
use openpgp::Fingerprint;
|
||||||
|
|
||||||
pub mod armor;
|
pub mod armor;
|
||||||
pub mod certify;
|
pub mod certify;
|
||||||
pub mod dane;
|
pub mod dane;
|
||||||
@ -149,6 +152,15 @@ $ sq --time 20130721T0550+0200 verify msg.pgp
|
|||||||
",
|
",
|
||||||
)]
|
)]
|
||||||
pub time: Option<String>,
|
pub time: Option<String>,
|
||||||
|
#[clap(
|
||||||
|
long = "trust-root",
|
||||||
|
value_name = "FINGERPRINT|KEYID",
|
||||||
|
help = "Considers the specified certificate to be a trust root",
|
||||||
|
long_help = "Considers the specified certificate to be a trust root. \
|
||||||
|
Trust roots are used by trust models, e.g., the web of \
|
||||||
|
trust, to authenticate certificates and User IDs."
|
||||||
|
)]
|
||||||
|
pub trust_roots: Vec<Fingerprint>,
|
||||||
#[clap(subcommand)]
|
#[clap(subcommand)]
|
||||||
pub subcommand: SqSubcommands,
|
pub subcommand: SqSubcommands,
|
||||||
}
|
}
|
||||||
|
@ -87,4 +87,211 @@ mod integration {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sq_encrypt_recipient_userid() -> Result<()>
|
||||||
|
{
|
||||||
|
let dir = TempDir::new()?;
|
||||||
|
|
||||||
|
let certd = dir.path().join("cert.d").display().to_string();
|
||||||
|
std::fs::create_dir(&certd).expect("mkdir works");
|
||||||
|
|
||||||
|
let alice_pgp = dir.path().join("alice.pgp").display().to_string();
|
||||||
|
let bob_pgp = dir.path().join("bob.pgp").display().to_string();
|
||||||
|
|
||||||
|
// Generate the keys.
|
||||||
|
let mut cmd = Command::cargo_bin("sq")?;
|
||||||
|
cmd.args(["--cert-store", &certd,
|
||||||
|
"key", "generate",
|
||||||
|
"--expires", "never",
|
||||||
|
"--userid", "<alice@example.org>",
|
||||||
|
"--export", &alice_pgp]);
|
||||||
|
cmd.assert().success();
|
||||||
|
let alice = Cert::from_file(&alice_pgp)?;
|
||||||
|
|
||||||
|
let bob_userids = &[
|
||||||
|
"<bob@some.org>",
|
||||||
|
"Bob <bob@other.org>",
|
||||||
|
"<bob@other.org>",
|
||||||
|
];
|
||||||
|
let bob_emails = &[
|
||||||
|
"bob@some.org",
|
||||||
|
"bob@other.org",
|
||||||
|
];
|
||||||
|
|
||||||
|
let bob_certified_userids = &[
|
||||||
|
"Bob <bob@other.org>",
|
||||||
|
];
|
||||||
|
let bob_certified_emails = &[
|
||||||
|
"bob@other.org",
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("sq")?;
|
||||||
|
cmd.args(["--cert-store", &certd,
|
||||||
|
"key", "generate",
|
||||||
|
"--expires", "never",
|
||||||
|
"--export", &bob_pgp]);
|
||||||
|
for userid in bob_userids.iter() {
|
||||||
|
cmd.args(["--userid", userid]);
|
||||||
|
}
|
||||||
|
cmd.assert().success();
|
||||||
|
let bob = Cert::from_file(&bob_pgp)?;
|
||||||
|
|
||||||
|
// Import the certificates.
|
||||||
|
let mut cmd = Command::cargo_bin("sq")?;
|
||||||
|
cmd.args(["--cert-store", &certd,
|
||||||
|
"import", &alice_pgp]);
|
||||||
|
cmd.assert().success();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("sq")?;
|
||||||
|
cmd.args(["--cert-store", &certd,
|
||||||
|
"import", &bob_pgp]);
|
||||||
|
cmd.assert().success();
|
||||||
|
|
||||||
|
const MESSAGE: &[u8] = &[0x42; 24 * 1024 + 23];
|
||||||
|
let encrypt = |trust_roots: &[&str],
|
||||||
|
recipients: &[(&str, &str)],
|
||||||
|
decryption_keys: &[&str]|
|
||||||
|
{
|
||||||
|
let mut cmd = Command::cargo_bin("sq").unwrap();
|
||||||
|
cmd.args(["--cert-store", &certd]);
|
||||||
|
for trust_root in trust_roots {
|
||||||
|
cmd.args(["--trust-root", trust_root]);
|
||||||
|
}
|
||||||
|
cmd.arg("encrypt");
|
||||||
|
|
||||||
|
// Make a string for debugging.
|
||||||
|
let mut cmd_display = "sq encrypt".to_string();
|
||||||
|
|
||||||
|
for (option, recipient) in recipients.iter() {
|
||||||
|
cmd.args([option, recipient]);
|
||||||
|
|
||||||
|
cmd_display.push_str(" ");
|
||||||
|
cmd_display.push_str(option);
|
||||||
|
cmd_display.push_str(" ");
|
||||||
|
cmd_display.push_str(recipient);
|
||||||
|
}
|
||||||
|
cmd.write_stdin(MESSAGE);
|
||||||
|
|
||||||
|
let output = cmd.output().expect("success");
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
|
||||||
|
if decryption_keys.is_empty() {
|
||||||
|
assert!(! output.status.success(),
|
||||||
|
"'{}' should have failed\nstdout:\n{}\nstderr:\n{}",
|
||||||
|
cmd_display, stdout, stderr);
|
||||||
|
} else {
|
||||||
|
assert!(output.status.success(),
|
||||||
|
"'{}' should have succeeded\nstdout:\n{}\nstderr:\n{}",
|
||||||
|
cmd_display, stdout, stderr);
|
||||||
|
|
||||||
|
for key in decryption_keys.iter() {
|
||||||
|
let mut cmd = Command::cargo_bin("sq").unwrap();
|
||||||
|
cmd.args(["--no-cert-store",
|
||||||
|
"decrypt",
|
||||||
|
"--recipient-file",
|
||||||
|
&key])
|
||||||
|
.write_stdin(stdout.as_bytes());
|
||||||
|
|
||||||
|
let output = cmd.output().expect("success");
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
assert!(output.status.success(),
|
||||||
|
"'{}' decryption should succeed\nstdout:\n{}\nstderr:\n{}",
|
||||||
|
cmd_display, stdout, stderr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Encryption by fingerprint should work.
|
||||||
|
encrypt(&[],
|
||||||
|
&[("--recipient-cert", &bob.fingerprint().to_string())],
|
||||||
|
&[&bob_pgp]);
|
||||||
|
|
||||||
|
// Encryption by email address and user id should fail if the
|
||||||
|
// binding can't be authenticated.
|
||||||
|
for email in bob_emails.iter() {
|
||||||
|
encrypt(&[],
|
||||||
|
&[("--recipient-email", email)],
|
||||||
|
&[]);
|
||||||
|
}
|
||||||
|
for userid in bob_userids.iter() {
|
||||||
|
encrypt(&[],
|
||||||
|
&[("--recipient-userid", userid)],
|
||||||
|
&[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alice certifies Bob's certificate.
|
||||||
|
for userid in bob_certified_userids {
|
||||||
|
let mut cmd = Command::cargo_bin("sq")?;
|
||||||
|
cmd.args(["--cert-store", &certd,
|
||||||
|
"certify", &alice_pgp, &bob_pgp, userid]);
|
||||||
|
|
||||||
|
let output = cmd.output().expect("success");
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
|
||||||
|
assert!(output.status.success(),
|
||||||
|
"'sq certify {} ...' should have succeeded\
|
||||||
|
\nstdout:\n{}\nstderr:\n{}",
|
||||||
|
userid, stdout, stderr);
|
||||||
|
let mut cmd = Command::cargo_bin("sq")?;
|
||||||
|
cmd.args(["--cert-store", &certd,
|
||||||
|
"import"])
|
||||||
|
.write_stdin(stdout.as_bytes());
|
||||||
|
cmd.assert().success();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Still don't use a trust root. This should still fail.
|
||||||
|
for email in bob_emails.iter() {
|
||||||
|
encrypt(&[],
|
||||||
|
&[("--recipient-email", email)],
|
||||||
|
&[]);
|
||||||
|
}
|
||||||
|
for userid in bob_userids.iter() {
|
||||||
|
encrypt(&[],
|
||||||
|
&[("--recipient-userid", userid)],
|
||||||
|
&[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make Alice the trust root. This should succeed.
|
||||||
|
for email in bob_emails.iter() {
|
||||||
|
if bob_certified_emails.contains(email) {
|
||||||
|
encrypt(&[&alice.fingerprint().to_string()],
|
||||||
|
&[("--recipient-email", email)],
|
||||||
|
&[ &bob_pgp ]);
|
||||||
|
} else {
|
||||||
|
encrypt(&[&alice.fingerprint().to_string()],
|
||||||
|
&[("--recipient-email", email)],
|
||||||
|
&[]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for userid in bob_userids.iter() {
|
||||||
|
if bob_certified_userids.contains(userid) {
|
||||||
|
encrypt(&[&alice.fingerprint().to_string()],
|
||||||
|
&[("--recipient-userid", userid)],
|
||||||
|
&[ &bob_pgp ]);
|
||||||
|
} else {
|
||||||
|
encrypt(&[&alice.fingerprint().to_string()],
|
||||||
|
&[("--recipient-userid", userid)],
|
||||||
|
&[]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make Bob a trust root. This should succeed for all
|
||||||
|
// self-signed user ids.
|
||||||
|
for email in bob_emails.iter() {
|
||||||
|
encrypt(&[&bob.fingerprint().to_string()],
|
||||||
|
&[("--recipient-email", email)],
|
||||||
|
&[&bob_pgp]);
|
||||||
|
}
|
||||||
|
for userid in bob_userids.iter() {
|
||||||
|
encrypt(&[&bob.fingerprint().to_string()],
|
||||||
|
&[("--recipient-userid", userid)],
|
||||||
|
&[&bob_pgp]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user