Use sequoia-cert-store to manage shadow CAs.

- As of 0.4.1, sequoia-cert-store includes (better versions of)
    shadow CA functionality.

  - Prefer it.
This commit is contained in:
Neal H. Walfield 2024-01-08 14:50:02 +01:00 committed by Justus Winter
parent 1bb215e67f
commit c45686c4da
No known key found for this signature in database
GPG Key ID: 686F55B4AB2B3386
6 changed files with 142 additions and 270 deletions

4
Cargo.lock generated
View File

@ -2951,9 +2951,9 @@ dependencies = [
[[package]]
name = "sequoia-cert-store"
version = "0.4.0"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f44e2775a51e844809b1f393c5098dd414f79555b61b29a98f0dede289b86515"
checksum = "9359ae25b815675b04c89216709c4c6cc6b7b4d4a0a5547f0f3635c3a1803a58"
dependencies = [
"anyhow",
"crossbeam",

View File

@ -41,7 +41,7 @@ chrono = "0.4.10"
clap = { version = "4", features = ["derive", "env", "string", "wrap_help"] }
humantime = "2"
itertools = ">=0.10, <0.13"
sequoia-cert-store = "0.4"
sequoia-cert-store = "0.4.1"
sequoia-wot = "0.9"
tempfile = "3.1"
tokio = { version = "1.13.1" }

View File

@ -23,10 +23,6 @@ pub fn dispatch(mut config: Config, c: &cli::autocrypt::Command) -> Result<()> {
match &c.subcommand {
Import(command) => {
let ca_filename = "_autocrypt.pgp";
let ca_userid = "Imported from Autocrypt";
let ca_trust_amount = 1;
let input = command.input.open()?;
let ac = autocrypt::AutocryptHeaders::from_reader(input)?;
let from = UserID::from(
@ -42,10 +38,16 @@ pub fn dispatch(mut config: Config, c: &cli::autocrypt::Command) -> Result<()> {
.then(|| a.value.clone()))
{
if let Some(cert) = h.key {
let certs = certify_downloads(
&mut config,
ca_filename, ca_userid, ca_trust_amount,
vec![cert], Some(&addr));
let certs = if let Ok((ca, _)) = config.certd_or_else()
.and_then(|certd| certd.shadow_ca_autocrypt())
{
certify_downloads(
&mut config, ca,
vec![cert], Some(&addr[..]))
} else {
vec![cert]
};
import_certs(&mut config, certs)?;
}
}

View File

@ -32,6 +32,7 @@ use net::{
};
use sequoia_cert_store as cert_store;
use cert_store::LazyCert;
use cert_store::StoreUpdate;
use cert_store::store::UserIDQueryParams;
@ -203,88 +204,6 @@ fn certify(config: &Config,
}
}
/// Gets the specified CA.
///
/// The ca is found in the specified special (e.g. `_wkd.pgp`, or
/// `_keyserver_keys.openpgp.org.pgp`). If the CA does not exist, it
/// is created with the specified User ID (e.g., `Downloaded from a
/// WKD`, or `Downloaded from keys.openpgp.org`), and the specified
/// trust amount.
fn get_ca(config: &mut Config,
ca_special: &str, ca_userid: &str, ca_trust_amount: usize)
-> Result<Cert>
{
let (created, ca) = config.get_special(ca_special, ca_userid, true)?;
if ! created {
// We didn't create it, and don't want to change how it is
// setup.
return Ok(ca);
}
// We just created the certificate. Make it a CA by having
// the local trust root certify it.
match config.local_trust_root() {
Err(err) => {
Err(anyhow::anyhow!(
"Failed to certify {:?} using the local trust root: {}",
ca_userid, err))
}
Ok(trust_root) => {
let keys = get_certification_keys(
&[trust_root], &config.policy, None, Some(config.time), None)
.context("Getting trust root's certification key")?;
assert!(
keys.len() == 1,
"Expect exactly one result from get_certification_keys()"
);
let mut signer = keys.into_iter().next().unwrap().0;
match certify(config, &mut signer, &ca, &[UserID::from(ca_userid)],
Some(config.time), 1, ca_trust_amount)
{
Err(err) => {
Err(err).context(format!(
"Error certifying {:?} with the local trust root",
ca_userid))
}
Ok(cert) => {
// Save it.
let cert_store = config.cert_store_mut_or_else()?;
cert_store.update(Arc::new(cert.clone().into()))
.with_context(|| {
format!("Saving {:?}", ca_userid)
})?;
if config.verbose {
wprintln!(
"Created the local CA {:?} for certifying \
certificates downloaded from this service. \
The CA's trust amount is set to {} of {}. \
Use `sq link add --ca '*' --amount N {}` \
to override it. Or `sq link retract {}` to \
disable it.",
ca_userid,
ca_trust_amount, sequoia_wot::FULLY_TRUSTED,
cert.fingerprint(), cert.fingerprint());
} else {
use std::sync::Once;
static MSG: Once = Once::new();
MSG.call_once(|| {
wprintln!("Note: Created a local CA to record \
provenance information.\n\
Note: See `sq link list --ca` \
and `sq link --help` for more \
information.");
});
}
Ok(cert)
}
}
}
}
}
/// Certify the certificates using the specified CA.
///
/// The certificates are certified for User IDs with the specified
@ -294,13 +213,13 @@ fn get_ca(config: &mut Config,
///
/// If a certificate cannot be certified for whatever reason, a
/// diagnostic is emitted, and the certificate is returned as is.
pub fn certify_downloads(config: &mut Config,
ca_special: &str, ca_userid: &str, ca_trust_amount: usize,
certs: Vec<Cert>, email: Option<&str>)
pub fn certify_downloads<'store>(config: &mut Config<'store>,
ca: Arc<LazyCert<'store>>,
certs: Vec<Cert>, email: Option<&str>)
-> Vec<Cert>
{
let mut ca = || -> Result<_> {
let ca = get_ca(config, ca_special, ca_userid, ca_trust_amount)?;
let ca = || -> Result<_> {
let ca = ca.to_cert()?;
let keys = get_certification_keys(
&[ca], &config.policy, None, Some(config.time), None)?;
@ -460,20 +379,98 @@ impl fmt::Display for Method {
}
impl Method {
fn ca(&self, config: &Config) -> Option<(String, String, usize)> {
match self {
Method::KeyServer(url) => keyserver_ca(config, url),
Method::WKD => Some((
WKD_CA_FILENAME.into(),
WKD_CA_USERID.into(),
WKD_CA_TRUST_AMOUNT,
)),
Method::DANE => Some((
DANE_CA_FILENAME.into(),
DANE_CA_USERID.into(),
DANE_CA_TRUST_AMOUNT,
)),
// Returns the CA's certificate.
//
// This doesn't return an error, because not all methods have
// shadow CAs, and a missing CA is not a hard error.
fn ca<'store>(&self, config: &Config<'store>) -> Option<Arc<LazyCert<'store>>> {
let ca = || -> Result<_> {
let certd = config.certd_or_else()?;
let (cert, created) = match self {
Method::KeyServer(url) => {
let result = certd.shadow_ca_keyserver(url)?;
match result {
Some((cert, created)) => (cert, created),
None => {
if config.verbose {
wprintln!(
"Not recording provenance information: \
{} is not known to be a verifying \
keyserver",
url);
}
return Ok(None);
}
}
}
Method::WKD => certd.shadow_ca_wkd()?,
Method::DANE => certd.shadow_ca_dane()?,
};
// Check that the data is a valid certificate. If not,
// bail sooner rather than later.
let _ = cert.to_cert()?;
Ok(Some((cert, created)))
};
let (cert, created) = match ca() {
Ok(Some((cert, created))) => (cert, created),
Ok(None) => return None,
Err(err) => {
let print_err = || {
wprintln!(
"Not recording provenance information: {}",
err);
};
if config.verbose {
print_err();
} else {
use std::sync::Once;
static MSG: Once = Once::new();
MSG.call_once(print_err);
}
return None;
}
};
if ! created {
// We didn't create it.
return Some(cert);
}
if config.verbose {
let invalid = UserID::from(&b"invalid data"[..]);
wprintln!(
"Created the local CA {:?} for certifying \
certificates downloaded from this service. \
Use `sq link add --ca '*' --amount N {}` \
to change how much it is trusted. Or \
`sq link retract {}` to disable it.",
if let Ok(cert) = cert.to_cert() {
best_effort_primary_uid(
cert, &config.policy, None)
} else {
&invalid
},
cert.fingerprint(), cert.fingerprint());
} else {
use std::sync::Once;
static MSG: Once = Once::new();
MSG.call_once(|| {
wprintln!("Note: Created a local CA to record \
provenance information.\n\
Note: See `sq link list --ca` \
and `sq link --help` for more \
information.");
});
}
Some(cert)
}
}
@ -506,16 +503,10 @@ impl Response {
Ok(cert) => if output.is_some() {
certs.push(cert);
} else {
if let Some((ca_filename, ca_userid,
ca_trust_amount)) =
response.method.ca(&config)
if let Some(ca) = response.method.ca(&config)
{
certs.append(&mut certify_downloads(
&mut config,
ca_filename.as_str(),
ca_userid.as_str(),
ca_trust_amount,
vec![cert], None));
&mut config, ca, vec![cert], None));
} else {
certs.push(cert);
}
@ -612,55 +603,6 @@ pub fn dispatch_fetch(config: Config, c: cli::network::fetch::Command)
})
}
/// Gets the filename for the CA's key and the default User ID.
fn keyserver_ca(config: &Config, uri: &str) -> Option<(String, String, usize)> {
if let Some(server) = uri.strip_prefix("hkps://") {
// We only certify the certificate if the transport was
// encrypted and authenticated.
let server = server.strip_suffix("/").unwrap_or(server);
// A basic sanity check on the name, which we are about to
// use as a filename: it can't start with a dot, no
// slashes, and no colons are allowed.
if server.chars().next() == Some('.')
|| server.contains('/')
|| server.contains('\\')
|| server.contains(':') {
return None;
}
let mut server = server.to_ascii_lowercase();
// Only record provenance information for certifying
// keyservers. Anything else doesn't make sense.
match &server[..] {
"keys.openpgp.org" => (),
"keys.mailvelope.com" => (),
"mail-api.proton.me" | "api.protonmail.ch" => (),
_ => {
config.info(format_args!(
"Not recording provenance information, {} is not \
known to be a verifying keyserver",
server));
return None;
},
}
// Unify aliases.
if &server == "api.protonmail.ch" {
server = "mail-api.proton.me".into();
}
Some((format!("_keyserver_{}.pgp", server),
format!("Downloaded from the keyserver {}", server),
KEYSERVER_CA_TRUST_AMOUNT))
} else {
None
}
}
const KEYSERVER_CA_TRUST_AMOUNT: usize = 1;
/// Figures out whether the given set of key servers is the default
/// set.
fn default_keyservers_p(servers: &[String]) -> bool {
@ -777,10 +719,6 @@ pub fn dispatch_keyserver(config: Config, c: cli::network::keyserver::Command)
Ok(())
}
const WKD_CA_FILENAME: &'static str = "_wkd.pgp";
const WKD_CA_USERID: &'static str = "Downloaded from a WKD";
const WKD_CA_TRUST_AMOUNT: usize = 1;
pub fn dispatch_wkd(config: Config, c: cli::network::wkd::Command) -> Result<()> {
let rt = tokio::runtime::Runtime::new()?;
@ -860,10 +798,6 @@ pub fn dispatch_wkd(config: Config, c: cli::network::wkd::Command) -> Result<()>
Ok(())
}
const DANE_CA_FILENAME: &'static str = "_dane.pgp";
const DANE_CA_USERID: &'static str = "Downloaded from DANE";
const DANE_CA_TRUST_AMOUNT: usize = 1;
pub fn dispatch_dane(config: Config, c: cli::network::dane::Command) -> Result<()> {
let rt = tokio::runtime::Runtime::new()?;

View File

@ -333,6 +333,7 @@ pub fn add(mut config: Config, c: link::AddCommand)
-> Result<()>
{
let trust_root = config.local_trust_root()?;
let trust_root = trust_root.to_cert()?;
let cert = config.lookup_one(&c.certificate, None, true)?;
@ -611,6 +612,7 @@ pub fn retract(mut config: Config, c: link::RetractCommand)
-> Result<()>
{
let trust_root = config.local_trust_root()?;
let trust_root = trust_root.to_cert()?;
let trust_root_kh = trust_root.key_handle();
let cert = config.lookup_one(&c.certificate, None, true)?;
@ -762,13 +764,14 @@ pub fn retract(mut config: Config, c: link::RetractCommand)
Ok(())
}
pub fn list(mut config: Config, c: link::ListCommand)
pub fn list(config: Config, c: link::ListCommand)
-> Result<()>
{
let mut cert_store = config.cert_store_or_else()?;
cert_store.prefetch_all();
let trust_root = config.local_trust_root()?;
let trust_root = trust_root.to_cert()?;
let trust_root_key = trust_root.primary_key().key().role_as_unspecified();
let cert_store = config.cert_store_or_else()?;

119
src/sq.rs
View File

@ -13,10 +13,9 @@ use std::cell::OnceCell;
use std::collections::btree_map::{BTreeMap, Entry};
use std::fmt;
use std::io;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::time::{Duration, SystemTime};
use std::time::SystemTime;
use std::sync::Arc;
use sequoia_openpgp as openpgp;
@ -36,12 +35,11 @@ use openpgp::packet::signature::subpacket::NotationDataFlags;
use openpgp::serialize::Serialize;
use openpgp::cert::prelude::*;
use openpgp::policy::{Policy, StandardPolicy as P};
use openpgp::serialize::SerializeInto;
use openpgp::types::KeyFlags;
use openpgp::types::RevocationStatus;
use openpgp::types::SignatureType;
use sequoia_cert_store as cert_store;
use cert_store::LazyCert;
use cert_store::Store;
use cert_store::store::StoreError;
use cert_store::store::StoreUpdate;
@ -524,6 +522,25 @@ impl<'store> Config<'store> {
}))
}
/// Returns a reference to the underlying certificate directory,
/// if it is configured.
///
/// If the cert direcgory is disabled, returns an error.
fn certd_or_else(&self)
-> Result<&cert_store::store::certd::CertD<'store>>
{
const NO_CERTD_ERR: &str =
"A local trust root and other special certificates are \
only available when using an OpenPGP certificate \
directory";
let cert_store = self.cert_store_or_else()
.with_context(|| NO_CERTD_ERR.to_string())?;
cert_store.certd()
.ok_or_else(|| anyhow::anyhow!(NO_CERTD_ERR))
}
/// Looks up an identifier.
///
/// This matches on both the primary key and the subkeys.
@ -881,95 +898,11 @@ impl<'store> Config<'store> {
})
}
/// Returns a special, creating it if necessary.
///
/// Returns whether a key was created, and the key.
fn get_special(&mut self, name: &str, userid: &str, create: bool)
-> Result<(bool, Cert)>
{
let certd = if let Some(certd) = self.cert_store_or_else()?.certd() {
certd.certd()
} else {
return Err(anyhow::anyhow!(
"A local trust root and other special certificates are \
only available when using an OpenPGP certificate \
directory"));
};
// Make sure the name is actually a special name. (CertD::get
// will also accepts fingerprints.)
let filename = certd.get_path_by_special(name)?;
// Read it.
let cert_bytes = certd.get(&name)
.with_context(|| {
format!(
"Looking up {} ({}) in the certificate directory",
name, userid)
})?
.map(|(_tag, bytes)| bytes);
let mut created = false;
let special: Cert = if let Some(cert_bytes) = cert_bytes {
Cert::from_bytes(&cert_bytes)
.with_context(|| format!(
"Parsing {} ({}) in the certificate directory",
name, userid))?
} else if ! create {
return Err(anyhow::anyhow!(
"Special certificate {} ({}) does not exist",
name, userid));
} else {
// The special doesn't exist, but we should create it.
let cert_builder = CertBuilder::new()
.set_primary_key_flags(KeyFlags::empty().set_certification())
// Set it in the past so that it is possible to use
// the CA when the reference time is in the past. Feb
// 2002.
.set_creation_time(
SystemTime::UNIX_EPOCH + Duration::new(1014235320, 0))
// CAs should *not* expire.
.set_validity_period(None)
.add_userid_with(
UserID::from(userid),
SignatureBuilder::new(SignatureType::GenericCertification)
.set_exportable_certification(false)?,
)?;
let (special, _) = cert_builder.generate()?;
let special_bytes = special.as_tsk().to_vec()?;
// XXX: Because we don't lock the cert-d, there is a
// (tiny) chance that we lost the race and the file will
// now exist. In that case, we really should try
// rereading it.
let mut f = std::fs::File::options()
.read(true).write(true).create_new(true)
.open(&filename)
.with_context(|| format!("Creating {:?}", &filename))?;
f.write_all(&special_bytes)
.with_context(|| format!("Writing {:?}", &filename))?;
created = true;
// We also need to insert the trust root into the certificate
// store, just without the secret key material.
let cert_store = self.cert_store_mut_or_else()?;
cert_store.update(Arc::new(special.clone().into()))
.with_context(|| format!("Inserting {}", name))?;
special
};
Ok((created, special))
}
const TRUST_ROOT: &'static str = "trust-root";
/// Returns the local trust root, creating it if necessary.
fn local_trust_root(&mut self) -> Result<Cert> {
self.get_special(Self::TRUST_ROOT, "Local Trust Root", true)
.map(|(_created, cert)| cert)
fn local_trust_root(&self) -> Result<Arc<LazyCert<'store>>> {
self.certd_or_else()?.trust_root().map(|(cert, _created)| {
cert
})
}
/// Returns the trust roots, including the cert store's trust
@ -980,7 +913,7 @@ impl<'store> Config<'store> {
.ok()
.and_then(|cert_store| cert_store.certd())
.and_then(|certd| {
match certd.certd().get(Self::TRUST_ROOT) {
match certd.certd().get(cert_store::store::openpgp_cert_d::TRUST_ROOT) {
Ok(Some((_tag, cert_bytes))) => Some(cert_bytes),
// Not found.
Ok(None) => None,