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:
parent
1bb215e67f
commit
c45686c4da
4
Cargo.lock
generated
4
Cargo.lock
generated
@ -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",
|
||||
|
@ -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" }
|
||||
|
@ -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)?;
|
||||
}
|
||||
}
|
||||
|
@ -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()?;
|
||||
|
||||
|
@ -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
119
src/sq.rs
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user