sequoia-sq/src/config.rs
Justus Winter 01aa4dc06a
Gracefully handle missing ui section in config file.
- Fixes `sq config get` without the [ui] section in the config file,
    while making sure that `sq config get` reflects whether hints are
    displayed.

  - Fixes #495.
2024-12-09 15:21:35 +01:00

1384 lines
44 KiB
Rust

//! Configuration model and file parsing.
use std::{
collections::{BTreeSet, HashSet},
fs,
io,
path::{Path, PathBuf},
time::SystemTime,
};
use aho_corasick::AhoCorasick;
use anyhow::Context;
use clap::{ValueEnum, parser::ValueSource};
use toml_edit::{
DocumentMut,
Item,
Table,
Value,
};
use sequoia_openpgp::{
Fingerprint,
policy::StandardPolicy,
};
use sequoia_net::reqwest::Url;
use sequoia_directories::{Component, Home};
use sequoia_policy_config::ConfiguredStandardPolicy;
use crate::{
Result,
cli,
cli::config::Augmentations,
};
/// Represents configuration at runtime.
///
/// This struct is manipulated when parsing the configuration file.
/// It is available as `Sq::config`, with suitable accessors that
/// handle the precedence of the various sources.
pub struct Config {
/// Whether to be more verbose.
verbose: bool,
/// Whether to be more quiet.
quiet: bool,
/// Whether to show hints.
hints: Option<bool>,
/// The set of encryption certs selected using `--for-self`.
encrypt_for_self: BTreeSet<Fingerprint>,
/// The default profile for encryption containers.
encrypt_profile: Option<cli::types::Profile>,
/// The set of signing keys selected using `--signer-self`.
sign_signer_self: BTreeSet<Fingerprint>,
/// The default certification key selected using
/// `--certifier-self`.
pki_vouch_certifier_self: Option<Fingerprint>,
/// The default validity period for third-party certifications.
pki_vouch_expiration: Option<cli::types::Expiration>,
policy_path: Option<PathBuf>,
policy_inline: Option<Vec<u8>>,
/// The default cipher suite for newly generated keys.
cipher_suite: Option<sequoia_openpgp::cert::CipherSuite>,
/// The default profile for newly generated keys.
key_generate_profile: Option<cli::types::Profile>,
/// The set of keyservers to use.
key_servers: Option<Vec<Url>>,
/// Iterations for network search.
network_search_iterations: u8,
/// Whether network search should use WKD.
network_search_wkd: bool,
/// Whether network search should use DANE.
network_search_dane: bool,
/// The location of the backend server executables.
servers_path: Option<PathBuf>,
}
impl Default for Config {
fn default() -> Self {
Config {
verbose: false,
quiet: false,
hints: None,
encrypt_for_self: Default::default(),
encrypt_profile: None,
sign_signer_self: Default::default(),
pki_vouch_certifier_self: None,
pki_vouch_expiration: None,
policy_path: None,
policy_inline: None,
cipher_suite: None,
key_generate_profile: None,
key_servers: None,
network_search_iterations: 3,
network_search_wkd: true,
network_search_dane: true,
servers_path: None,
}
}
}
impl Config {
/// Sets the verbose setting.
///
/// Handles the precedence of the various sources, but since this
/// is a global flag and accessed very often, this is a setter and
/// we do this once, when initializing the configuration object:
///
/// - If the flag is given, use the given value.
/// - If the command line flag is not given, then
/// - use the value from the configuration file (if any),
/// - or use the default value.
pub fn init_verbose(&mut self, cli: bool, source: Option<ValueSource>)
{
match source.expect("set by the cli parser") {
ValueSource::DefaultValue => {
// Use the value from the configuration file.
},
_ => self.verbose = cli,
}
}
/// Returns the verbose setting.
///
/// The precedence of the various sources has been handled at
/// initialization time.
pub fn verbose(&self) -> bool {
self.verbose
}
/// Sets the quiet setting.
///
/// Handles the precedence of the various sources, but since this
/// is a global flag and accessed very often, this is a setter and
/// we do this once, when initializing the configuration object:
///
/// - If the flag is given, use the given value.
/// - If the command line flag is not given, then
/// - use the value from the configuration file (if any),
/// - or use the default value.
pub fn init_quiet(&mut self, cli: bool, source: Option<ValueSource>)
{
match source.expect("set by the cli parser") {
ValueSource::DefaultValue => {
// Use the value from the configuration file.
},
_ => self.quiet = cli,
}
}
/// Returns the quiet setting.
///
/// The precedence of the various sources has been handled at
/// initialization time.
pub fn quiet(&self) -> bool {
self.quiet
}
/// Returns whether to show hints.
pub fn hints(&self) -> bool {
self.hints.unwrap_or(! self.quiet())
}
/// Returns the certificates that should be added to the list of
/// recipients if `encrypt --for-self` is given.
pub fn encrypt_for_self(&self) -> &BTreeSet<Fingerprint> {
&self.encrypt_for_self
}
/// Returns the default profile for encryption containers.
///
/// Handles the precedence of the various sources:
///
/// - If the flag is given, use the given value.
/// - If the command line flag is not given, then
/// - use the value from the configuration file (if any),
/// - or use the default value.
pub fn encrypt_profile(&self, cli: &cli::types::Profile,
source: Option<ValueSource>)
-> cli::types::Profile
{
match source.expect("set by the cli parser") {
ValueSource::DefaultValue =>
self.encrypt_profile.as_ref().unwrap_or(cli),
_ => cli,
}.clone()
}
/// Returns the keys that should be added to the list of
/// signers if `--signer-self` is given.
pub fn sign_signer_self(&self) -> &BTreeSet<Fingerprint> {
&self.sign_signer_self
}
/// Returns the key that should be used as certifier if
/// `--certifier-self` is given.
pub fn pki_vouch_certifier_self(&self) -> &Option<Fingerprint> {
&self.pki_vouch_certifier_self
}
/// Returns the expiration for third-party certifications.
///
/// Handles the precedence of the various sources:
///
/// - If the flag is given, use the given value.
/// - If the command line flag is not given, then
/// - use the value from the configuration file (if any),
/// - or use the default value.
pub fn pki_vouch_expiration(&self, cli: &cli::types::Expiration,
source: Option<ValueSource>)
-> cli::types::Expiration
{
match source.expect("set by the cli parser") {
ValueSource::DefaultValue =>
self.pki_vouch_expiration.as_ref().unwrap_or(cli),
_ => cli,
}.clone()
}
/// Returns the path to the referenced cryptographic policy, if
/// any.
pub fn policy_path(&self) -> Option<&Path> {
self.policy_path.as_deref()
}
/// Returns the cryptographic policy.
///
/// We read in the default policy configuration, the configuration
/// referenced in the configuration file, and the inline policy.
pub fn policy(&self, at: SystemTime)
-> Result<StandardPolicy<'static>>
{
let mut policy = ConfiguredStandardPolicy::at(at);
policy.parse_default_config()?;
if let Some(p) = &self.policy_path {
if ! policy.parse_config_file(p)? {
return Err(anyhow::anyhow!(
"referenced policy file {:?} does not exist", p));
}
}
if let Some(p) = &self.policy_inline {
policy.parse_bytes(p)?;
}
Ok(policy.build())
}
/// Returns the cipher suite for generating new keys.
///
/// Handles the precedence of the various sources:
///
/// - If the flag is given, use the given value.
/// - If the command line flag is not given, then
/// - use the value from the configuration file (if any),
/// - or use the default value.
pub fn cipher_suite(&self, cli: &cli::key::CipherSuite,
source: Option<ValueSource>)
-> sequoia_openpgp::cert::CipherSuite
{
match source.expect("set by the cli parser") {
ValueSource::DefaultValue =>
self.cipher_suite.unwrap_or_else(
|| cli.as_ciphersuite()),
_ => cli.as_ciphersuite(),
}
}
/// Returns the profile for generating new keys.
///
/// Handles the precedence of the various sources:
///
/// - If the flag is given, use the given value.
/// - If the command line flag is not given, then
/// - use the value from the configuration file (if any),
/// - or use the default value.
pub fn key_generate_profile(&self, cli: &cli::types::Profile,
source: Option<ValueSource>)
-> cli::types::Profile
{
match source.expect("set by the cli parser") {
ValueSource::DefaultValue =>
self.key_generate_profile.as_ref().unwrap_or(cli),
_ => cli,
}.clone()
}
/// Returns the key servers to query or publish.
///
/// Handles the precedence of the various sources:
///
/// - If the flag is given, use the given value.
/// - If the command line flag is not given, then
/// - use the value from the configuration file (if any),
/// - or use the default value.
pub fn key_servers<'s, S>(&'s self, cli: &'s [S],
source: Option<ValueSource>)
-> impl Iterator<Item = &'s str> + 's
where
S: AsRef<str> + 's,
{
match source.expect("set by the cli parser") {
ValueSource::DefaultValue =>
self.key_servers.as_ref()
.map(|s| Box::new(s.iter().map(|s| s.as_str()))
as Box<dyn Iterator<Item = &str>>)
.unwrap_or_else(|| Box::new(cli.iter().map(|s| s.as_ref()))
as Box<dyn Iterator<Item = &str>>),
_ => Box::new(cli.iter().map(|s| s.as_ref()))
as Box<dyn Iterator<Item = &str>>,
}
}
/// Returns the iteration count for network search.
pub fn network_search_iterations(&self) -> u8 {
self.network_search_iterations
}
/// Returns whether network search should use WKD.
pub fn network_search_wkd(&self) -> bool {
self.network_search_wkd
}
/// Returns whether network search should use DANE.
pub fn network_search_dane(&self) -> bool {
self.network_search_dane
}
/// Returns the path to the backend servers.
pub fn servers_path(&self) -> Option<&Path> {
self.servers_path.as_ref().map(|p| p.as_path())
}
}
/// Holds the document tree of the configuration file.
#[derive(Debug, Default)]
pub struct ConfigFile {
doc: DocumentMut,
}
impl ConfigFile {
/// A template for the configuration containing the default
/// values.
const TEMPLATE: &'static str = "\
# Configuration template for sq <SQ-VERSION>
<SQ-CONFIG-PATH-HINT>
[ui]
#verbosity = \"default\" # or \"verbose\" or \"quiet\"
#hints = true
[encrypt]
#for-self = [\"fingerprint of your key\"]
#profile = <DEFAULT-ENCRYPT-PROFILE>
[sign]
#signer-self = [\"fingerprint of your key\"]
[pki.vouch]
#certifier-self = \"fingerprint of your key\"
#expiration = \"<DEFAULT-PKI-VOUCH-EXPIRATION>y\"
[key.generate]
#cipher-suite = <DEFAULT-CIPHER-SUITE>
#profile = <DEFAULT-KEY-GENERATE-PROFILE>
[network]
#keyservers = <DEFAULT-KEY-SERVERS>
[network.search]
#iterations = 3
#use-wkd = true
#use-dane = true
[servers]
#path = <DEFAULT-SERVERS-PATH>
[policy]
#path = <DEFAULT-POLICY-FILE>
# The policy can be inlined, either alternatively, or additionally,
# like so:
<DEFAULT-POLICY-INLINE>
";
/// Patterns to match on in `Self::DEFAULT` to be replaced with
/// the default values.
const TEMPLATE_PATTERNS: &'static [&'static str] = &[
"<SQ-VERSION>",
"<SQ-CONFIG-PATH-HINT>",
"<DEFAULT-ENCRYPT-PROFILE>",
"<DEFAULT-PKI-VOUCH-EXPIRATION>",
"<DEFAULT-CIPHER-SUITE>",
"<DEFAULT-KEY-GENERATE-PROFILE>",
"<DEFAULT-KEY-SERVERS>",
"<DEFAULT-SERVERS-PATH>",
"<DEFAULT-POLICY-FILE>",
"<DEFAULT-POLICY-INLINE>",
];
/// Returns a configuration template with the defaults.
fn config_template(path: Option<PathBuf>) -> Result<String> {
let ac = AhoCorasick::new(Self::TEMPLATE_PATTERNS)?;
let mut p = ConfiguredStandardPolicy::new();
p.parse_default_config()?;
let mut default_policy_inline = Vec::new();
p.dump(&mut default_policy_inline,
sequoia_policy_config::DumpDefault::Template)?;
let default_policy_inline =
regex::Regex::new(r"(?m)^\[")?.replace_all(
std::str::from_utf8(&default_policy_inline)?, "[policy.");
Ok(ac.replace_all(Self::TEMPLATE, &[
&env!("CARGO_PKG_VERSION").to_string(),
&if let Some(path) = path {
format!(
"\n\
# To use it, edit it to your liking and write it to\n\
# {}",
&path.display())
} else {
"".into()
},
&format!("{:?}", cli::types::Profile::default()
.to_possible_value().unwrap().get_name()),
&cli::THIRD_PARTY_CERTIFICATION_VALIDITY_IN_YEARS.to_string(),
&format!("{:?}", cli::key::CipherSuite::default().
to_possible_value().unwrap().get_name()),
&format!("{:?}", cli::types::Profile::default()
.to_possible_value().unwrap().get_name()),
&format!("{:?}", cli::network::keyserver::DEFAULT_KEYSERVERS),
&format!("{:?}", {
sequoia_keystore::sequoia_ipc::Context::configure().build()
.map(|c| c.lib().display().to_string())
.unwrap_or_else(|_| "<unknown>".into())
}),
&format!("{:?}", Self::global_crypto_policy_file()),
&default_policy_inline.to_string(),
]))
}
/// Returns the default configuration in template form.
///
/// All the configuration options with their defaults are
/// commented out.
pub fn default_template(home: Option<&Home>) -> Result<Self> {
let template = Self::config_template(home.map(Self::file_name))?;
let doc: DocumentMut = template.parse()
.context("Parsing default configuration failed")?;
Ok(Self {
doc,
})
}
/// Returns the default configuration.
pub fn default_config(home: Option<&Home>) -> Result<Self> {
let template = Self::config_template(home.map(Self::file_name))?;
// Enable all defaults by commenting-in.
let r = regex::Regex::new(r"(?m)^#([^ ])")?;
let defaults = r.replace_all(&template, "$1");
let doc: DocumentMut = defaults.parse()
.context("Parsing default configuration failed")?;
Ok(Self {
doc,
})
}
/// Returns the path of the config file.
pub fn file_name(home: &Home) -> PathBuf {
home.config_dir(Component::Sq).join("config.toml")
}
/// Reads and validates the configuration file.
pub fn read(&mut self, home: &Home)
-> Result<Config>
{
let mut config = Config::default();
self.read_internal(home, Some(&mut config), None)?;
Ok(config)
}
/// Reads and validates the configuration file.
pub fn read_and_augment(&mut self, home: &Home) -> Result<Augmentations>
{
let mut augmentations = Augmentations::default();
self.read_internal(home, None, Some(&mut augmentations))?;
Ok(augmentations)
}
/// Reads and validates the configuration file, and optionally
/// applies them to the given configuration, and optionally
/// supplies augmentations for the help texts in the command line
/// parser.
fn read_internal(&mut self, home: &Home, mut config: Option<&mut Config>,
mut cli: Option<&mut Augmentations>)
-> Result<()>
{
let path = Self::file_name(home);
let raw = match fs::read_to_string(&path) {
Ok(r) => r,
Err(e) if e.kind() == io::ErrorKind::NotFound =>
Self::config_template(Some(path.clone()))?,
Err(e) => return Err(anyhow::Error::from(e).context(
format!("Reading configuration file {} failed",
path.display()))),
};
let doc: DocumentMut = raw.parse()
.with_context(|| format!("Parsing configuration file {} failed",
path.display()))?;
apply_schema(&mut config, &mut cli, None, doc.iter(), TOP_LEVEL_SCHEMA)
.with_context(|| format!("Parsing configuration file {} failed",
path.display()))?;
self.doc = doc;
Ok(())
}
/// Writes the configuration to the disk.
pub fn persist(&self, home: &Home) -> Result<()> {
let path = Self::file_name(home);
let dir = path.parent().unwrap();
fs::create_dir_all(dir)?;
let mut t =
tempfile::NamedTempFile::new_in(dir)?;
self.dump(&mut t)?;
t.persist(path)?;
Ok(())
}
/// Writes the configuration to the given writer.
pub fn dump(&self, sink: &mut dyn io::Write) -> Result<()> {
write!(sink, "{}", self.doc.to_string())?;
Ok(())
}
/// Verifies the configuration.
pub fn verify(&self) -> Result<()> {
let mut config = Default::default();
apply_schema(&mut Some(&mut config), &mut None, None, self.doc.iter(),
TOP_LEVEL_SCHEMA)?;
config.policy(SystemTime::now())?;
Ok(())
}
/// Augments the configuration with the effective configuration
/// and policy.
///
/// XXX: Due to the way doc.remove works, it will leave misleading
/// comments behind. Therefore, the resulting configuration is
/// not suitable for dumping, but may only be used for
/// commands::config::get.
pub fn effective_configuration(&self, sq: &crate::Sq) -> Result<Self> {
use std::io::Write;
let mut raw = Vec::new();
// First, start with our configuration, and drop most of the
// policy with the exception of the path.
let p = ConfiguredStandardPolicy::from_policy(sq.policy.clone());
let mut doc = self.doc.clone();
doc.remove("policy");
use crate::toml_edit_tree::Node;
let policy_path: crate::toml_edit_tree::Path
= "policy.path".parse().unwrap();
if let Ok(p) = self.as_item().traverse(&policy_path) {
let p =
p.as_atomic_value().unwrap().as_str().unwrap().to_string();
doc.as_table_mut().insert("policy", Item::Table(
[("path", Value::from(p))]
.into_iter().collect()));
}
write!(&mut raw, "{}", doc.to_string())?;
// Then, augment the configuration with the effective policy.
let mut default_policy_inline = Vec::new();
p.dump(&mut default_policy_inline,
sequoia_policy_config::DumpDefault::Template)?;
let default_policy_inline =
regex::Regex::new(r"(?m)^\[")?.replace_all(
std::str::from_utf8(&default_policy_inline)?, "[policy.");
write!(&mut raw, "{}", default_policy_inline)?;
// Now, parse the resulting configuration.
let mut doc: DocumentMut = std::str::from_utf8(&raw)?.parse()?;
// Tweak a few settings.
if doc.get("ui").is_none() {
doc.as_table_mut().insert("ui".into(),
Item::Table(Default::default()));
}
doc.get_mut("ui").expect("just created on demand")
.set(&"hints".into(), sq.config.hints().into())?;
// Double check that it is well-formed.
apply_schema(&mut None, &mut None, None, doc.iter(), TOP_LEVEL_SCHEMA)?;
Ok(Self {
doc,
})
}
/// Returns the path to the global cryptographic policy
/// configuration file.
pub fn global_crypto_policy_file() -> String {
std::env::var(ConfiguredStandardPolicy::ENV_VAR)
.unwrap_or_else(
|_| ConfiguredStandardPolicy::CONFIG_FILE.into())
}
/// Returns the document tree.
pub fn as_item(&self) -> &Item {
self.doc.as_item()
}
/// Returns the mutable document tree.
pub fn as_item_mut(&mut self) -> &mut Item {
self.doc.as_item_mut()
}
}
/// Validates a configuration section using a schema, and optionally
/// applies changes to the configuration and CLI augmentations.
///
/// Returns an error if a key is unknown.
///
/// known_keys better be lowercase.
fn apply_schema<'toml>(config: &mut Option<&mut Config>,
cli: &mut Option<&mut Augmentations>,
path: Option<&str>,
section: toml_edit::Iter<'toml>,
schema: Schema) -> Result<()> {
let section = section.collect::<Vec<_>>();
let known_keys: Vec<_> =
schema.iter().map(|(key, _)| *key).collect();
// Schema keys better be lowercase.
debug_assert!(known_keys.iter().all(|&s| &s.to_lowercase() == s),
"keys in schema must be lowercase");
// Schema keys better be sorted.
debug_assert!(known_keys.windows(2).all(|v| v[0] <= v[1]),
"keys in schema must be sorted");
// XXX: once [].is_sorted is stabilized:
// debug_assert!(known_keys.is_sorted(), "keys in schema must be sorted");
let prefix = if let Some(path) = path {
format!("{}.", path)
} else {
"".to_string()
};
let keys: HashSet<&str> = section
.iter().map(|(key, _value)| *key)
.collect();
// The set of allowed keys are the known keys, plus
// "ignore_invalid", and the value of "ignore_invalid".
let mut allowed_keys: Vec<&str> = known_keys.to_vec();
if let Some(ignore) = section.iter()
.find_map(|(k, v)| (*k == "ignore_invalid").then_some(*v))
{
allowed_keys.push("ignore_invalid");
match ignore {
Item::Value(Value::String(k)) =>
allowed_keys.push(k.value().as_str()),
Item::Value(Value::Array(ks)) => {
for k in ks {
if let Value::String(k) = k {
allowed_keys.push(k.value().as_str());
} else {
Err(Error::ParseError(format!(
"'{}ignore_invalid' takes a string \
or an array of strings",
prefix)))?
}
}
}
_ => {
return Err(Error::ParseError(format!(
"Invalid value for '{}ignore_invalid': {}, \
expected a string or an array of strings",
prefix, ignore)).into());
}
}
}
// Now check if there are any unknown sections.
let unknown_keys = keys
.difference(&allowed_keys.into_iter().collect())
.map(|s| *s)
.collect::<Vec<_>>();
if ! unknown_keys.is_empty() {
return Err(Error::ParseError(format!(
"{} has unknown keys: {}, valid keys are: {}",
if let Some(path) = path {
path
} else {
"top-level section"
},
unknown_keys.join(", "),
// We don't include the keys listed in ignore_invalid.
known_keys.join(", "))).into());
}
// Now validate the values.
for (key, value) in &section {
if let Ok(i) = schema.binary_search_by_key(key, |(k, _)| k) {
let apply = schema[i].1;
(apply)(config, cli, &format!("{}{}", prefix, key), value)
.with_context(|| format!("Error validating {:?}", key))?;
}
}
Ok(())
}
/// Errors used in this module.
///
/// Note: This enum cannot be exhaustively matched to allow future
/// extensions.
#[non_exhaustive]
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
pub enum Error {
/// Parse error
#[error("Parse error: {0}")]
ParseError(String),
/// A Relative Path was provided where an absolute path was expected.
#[error("Relative path not allowed: {0}")]
RelativePathError(PathBuf),
/// An algorithm is not known to this crate.
#[error("Unknown algorithm: {0}")]
UnknownAlgorithm(String),
#[error("Configuration item {0:?} is not a {1} but a {2}")]
BadType(String, &'static str, &'static str),
}
impl Error {
/// Returns an `Error::BadType` given an item.
fn bad_item_type(path: &str, i: &Item, want_type: &'static str)
-> anyhow::Error
{
Error::BadType(path.into(), want_type, i.type_name()).into()
}
/// Returns an `Error::BadType` given a value.
fn bad_value_type(path: &str, v: &Value, want_type: &'static str)
-> anyhow::Error
{
Error::BadType(path.into(), want_type, v.type_name()).into()
}
}
/// A function that validates a node in the configuration tree with
/// the given path, and optionally makes changes to the configuration
/// and CLI augmentations.
type Applicator = fn(&mut Option<&mut Config>, &mut Option<&mut Augmentations>,
&str, &Item)
-> Result<()>;
/// Ignores a node.
fn apply_nop(_: &mut Option<&mut Config>, _: &mut Option<&mut Augmentations>,
_: &str, _: &Item)
-> Result<()>
{
Ok(())
}
/// A [`Schema`] maps keys to [`Applicator`]s.
type Schema = &'static [(&'static str, Applicator)];
/// Schema for the toplevel.
const TOP_LEVEL_SCHEMA: Schema = &[
("encrypt", apply_encrypt),
("key", apply_key),
("network", apply_network),
("pki", apply_pki),
("policy", apply_policy),
("servers", apply_servers),
("sign", apply_sign),
("ui", apply_ui),
];
/// Schema for the `ui` section.
const UI_SCHEMA: Schema = &[
("hints", apply_ui_hints),
("verbosity", apply_ui_verbosity),
];
/// Validates the `ui` section.
fn apply_ui(config: &mut Option<&mut Config>, cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let section = item.as_table_like()
.ok_or_else(|| Error::bad_item_type(path, item, "table"))?;
apply_schema(config, cli, Some(path), section.iter(), UI_SCHEMA)?;
Ok(())
}
/// Validates the `ui.hints` value.
fn apply_ui_hints(config: &mut Option<&mut Config>,
_cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let s = item.as_bool()
.ok_or_else(|| Error::bad_item_type(path, item, "bool"))?;
if let Some(config) = config {
config.hints = Some(s);
}
Ok(())
}
/// Validates the `ui.verbosity` value.
fn apply_ui_verbosity(config: &mut Option<&mut Config>,
cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let s = item.as_str()
.ok_or_else(|| Error::bad_item_type(path, item, "string"))?;
let mut verbose = false;
let mut quiet = false;
match s {
"default" => (),
"verbose" => verbose = true,
"quiet" => quiet = true,
_ => return Err(anyhow::anyhow!("verbosity must be either \
\"default\", \
\"verbose\", \
or \"quiet\"")),
};
if let Some(config) = config {
config.verbose = verbose;
config.quiet = quiet;
}
if let Some(cli) = cli {
if verbose {
cli.insert("ui.verbose", "verbose".into());
}
if quiet {
cli.insert("ui.quiet", "quiet".into());
}
}
Ok(())
}
/// Schema for the `encrypt` section.
const ENCRYPT_SCHEMA: Schema = &[
("for-self", apply_encrypt_for_self),
("profile", apply_encrypt_profile),
];
/// Validates the `encrypt` section.
fn apply_encrypt(config: &mut Option<&mut Config>, cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let section = item.as_table_like()
.ok_or_else(|| Error::bad_item_type(path, item, "table"))?;
apply_schema(config, cli, Some(path), section.iter(), ENCRYPT_SCHEMA)?;
Ok(())
}
/// Validates the `encrypt.for-self` value.
fn apply_encrypt_for_self(config: &mut Option<&mut Config>,
cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let list = item.as_array()
.ok_or_else(|| Error::bad_item_type(path, item, "array"))?;
let mut strs = Vec::new();
let mut values = BTreeSet::default();
for (i, server) in list.iter().enumerate() {
let s = server.as_str()
.ok_or_else(|| Error::bad_value_type(&format!("{}.{}", path, i),
server, "string"))?;
strs.push(s);
values.insert(s.parse::<Fingerprint>()?);
}
if let Some(cli) = cli {
cli.insert("encrypt.for-self", strs.join(" "));
}
if let Some(config) = config {
config.encrypt_for_self = values;
}
Ok(())
}
/// Validates the `key.generate.profile` value.
fn apply_encrypt_profile(config: &mut Option<&mut Config>,
cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let s = item.as_str()
.ok_or_else(|| Error::bad_item_type(path, item, "string"))?;
let v = cli::types::Profile::from_str(s, false)
.map_err(|e| anyhow::anyhow!("{}", e))?;
if let Some(config) = config {
config.encrypt_profile = Some(v.clone());
}
if let Some(cli) = cli {
cli.insert(
"encrypt.profile",
v.to_possible_value().expect("just validated").get_name().into());
}
Ok(())
}
/// Schema for the `sign` section.
const SIGN_SCHEMA: Schema = &[
("signer-self", apply_sign_signer_self),
];
/// Validates the `sign` section.
fn apply_sign(config: &mut Option<&mut Config>,
cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let section = item.as_table_like()
.ok_or_else(|| Error::bad_item_type(path, item, "table"))?;
apply_schema(config, cli, Some(path), section.iter(), SIGN_SCHEMA)?;
Ok(())
}
/// Validates the `sign.signer-self` value.
fn apply_sign_signer_self(config: &mut Option<&mut Config>,
cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let list = item.as_array()
.ok_or_else(|| Error::bad_item_type(path, item, "array"))?;
let mut strs = Vec::new();
let mut values = BTreeSet::default();
for (i, server) in list.iter().enumerate() {
let s = server.as_str()
.ok_or_else(|| Error::bad_value_type(&format!("{}.{}", path, i),
server, "string"))?;
strs.push(s);
values.insert(s.parse::<Fingerprint>()?);
}
if let Some(cli) = cli {
cli.insert("sign.signer-self", strs.join(" "));
}
if let Some(config) = config {
config.sign_signer_self = values;
}
Ok(())
}
/// Schema for the `pki` section.
const PKI_SCHEMA: Schema = &[
("vouch", apply_pki_vouch),
];
/// Validates the `pki` section.
fn apply_pki(config: &mut Option<&mut Config>,
cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let section = item.as_table_like()
.ok_or_else(|| Error::bad_item_type(path, item, "table"))?;
apply_schema(config, cli, Some(path), section.iter(), PKI_SCHEMA)?;
Ok(())
}
/// Schema for the `pki.vouch` section.
const PKI_VOUCH_SCHEMA: Schema = &[
("certifier-self", apply_pki_vouch_certifier_self),
("expiration", apply_pki_vouch_expiration),
];
/// Validates the `pki.vouch` section.
fn apply_pki_vouch(config: &mut Option<&mut Config>,
cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let section = item.as_table_like()
.ok_or_else(|| Error::bad_item_type(path, item, "table"))?;
apply_schema(config, cli, Some(path), section.iter(), PKI_VOUCH_SCHEMA)?;
Ok(())
}
/// Validates the `pki.vouch.certifier-self` value.
fn apply_pki_vouch_certifier_self(config: &mut Option<&mut Config>,
cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let s = item.as_str()
.ok_or_else(|| Error::bad_item_type(path, item, "string"))?;
let fp = s.parse::<Fingerprint>()?;
if let Some(cli) = cli {
cli.insert("pki.vouch.certifier-self", fp.to_string());
}
if let Some(config) = config {
config.pki_vouch_certifier_self = Some(fp);
}
Ok(())
}
/// Validates the `pki.vouch.expiration` value.
fn apply_pki_vouch_expiration(config: &mut Option<&mut Config>,
cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let s = item.as_str()
.ok_or_else(|| Error::bad_item_type(path, item, "string"))?;
let v = s.parse::<cli::types::Expiration>()?;
if let Some(cli) = cli {
cli.insert("pki.vouch.expiration", v.to_string());
}
if let Some(config) = config {
config.pki_vouch_expiration = Some(v);
}
Ok(())
}
/// Schema for the `key` section.
const KEY_SCHEMA: Schema = &[
("generate", apply_key_generate),
];
/// Validates the `key` section.
fn apply_key(config: &mut Option<&mut Config>, cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let section = item.as_table_like()
.ok_or_else(|| Error::bad_item_type(path, item, "table"))?;
apply_schema(config, cli, Some(path), section.iter(), KEY_SCHEMA)?;
Ok(())
}
/// Schema for the `key.generate` section.
const KEY_GENERATE_SCHEMA: Schema = &[
("cipher-suite", apply_key_generate_cipher_suite),
("profile", apply_key_generate_profile),
];
/// Validates the `key.generate` section.
fn apply_key_generate(config: &mut Option<&mut Config>,
cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let section = item.as_table_like()
.ok_or_else(|| Error::bad_item_type(path, item, "table"))?;
apply_schema(config, cli, Some(path), section.iter(), KEY_GENERATE_SCHEMA)?;
Ok(())
}
/// Validates the `key.generate.cipher-suite` value.
fn apply_key_generate_cipher_suite(config: &mut Option<&mut Config>,
cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let s = item.as_str()
.ok_or_else(|| Error::bad_item_type(path, item, "string"))?;
let v = cli::key::CipherSuite::from_str(s, false)
.map_err(|e| anyhow::anyhow!("{}", e))?;
if let Some(config) = config {
config.cipher_suite = Some(v.as_ciphersuite());
}
if let Some(cli) = cli {
cli.insert(
"key.generate.cipher-suite",
v.to_possible_value().expect("just validated").get_name().into());
}
Ok(())
}
/// Validates the `key.generate.profile` value.
fn apply_key_generate_profile(config: &mut Option<&mut Config>,
cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let s = item.as_str()
.ok_or_else(|| Error::bad_item_type(path, item, "string"))?;
let v = cli::types::Profile::from_str(s, false)
.map_err(|e| anyhow::anyhow!("{}", e))?;
if let Some(config) = config {
config.key_generate_profile = Some(v.clone());
}
if let Some(cli) = cli {
cli.insert(
"key.generate.profile",
v.to_possible_value().expect("just validated").get_name().into());
}
Ok(())
}
/// Schema for the `network` section.
const NETWORK_SCHEMA: Schema = &[
("keyservers", apply_network_keyservers),
("search", apply_network_search),
];
/// Validates the `network` section.
fn apply_network(config: &mut Option<&mut Config>, cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let section = item.as_table_like()
.ok_or_else(|| Error::bad_item_type(path, item, "table"))?;
apply_schema(config, cli, Some(path), section.iter(), NETWORK_SCHEMA)?;
Ok(())
}
/// Validates the `network.keyservers` value.
fn apply_network_keyservers(config: &mut Option<&mut Config>,
cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let list = item.as_array()
.ok_or_else(|| Error::bad_item_type(path, item, "array"))?;
let mut servers_str = Vec::new();
let mut servers_url = Vec::new();
for (i, server) in list.iter().enumerate() {
let server_str = server.as_str()
.ok_or_else(|| Error::bad_value_type(&format!("{}.{}", path, i),
server, "string"))?;
let url = Url::parse(server_str)?;
let s = url.scheme();
match s {
"hkp" => (),
"hkps" => (),
_ => return Err(anyhow::anyhow!(
"must be a hkp:// or hkps:// URL: {}", url)),
}
servers_str.push(server_str);
servers_url.push(url);
}
if let Some(cli) = cli {
cli.insert("network.keyserver.servers", servers_str.join(" "));
}
if let Some(config) = config {
config.key_servers = Some(servers_url);
}
Ok(())
}
/// Schema for the `network.search` section.
const NETWORK_SEARCH_SCHEMA: Schema = &[
("iterations", apply_network_search_iterations),
("use-dane", apply_network_search_use_dane),
("use-wkd", apply_network_search_use_wkd),
];
/// Validates the `network.search` section.
fn apply_network_search(config: &mut Option<&mut Config>,
cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let section = item.as_table_like()
.ok_or_else(|| Error::bad_item_type(path, item, "table"))?;
apply_schema(config, cli, Some(path), section.iter(),
NETWORK_SEARCH_SCHEMA)?;
Ok(())
}
/// Validates the `network.search.iterations` value.
fn apply_network_search_iterations(config: &mut Option<&mut Config>,
_cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let s = item.as_integer()
.ok_or_else(|| Error::bad_item_type(path, item, "integer"))?;
if let Some(config) = config {
if s == 0 {
return Err(anyhow::anyhow!("value must be at least 1"));
}
config.network_search_iterations = s.try_into()
.map_err(|_| anyhow::anyhow!("value must not exceed 255"))?;
}
Ok(())
}
/// Validates the `network.search.use-dane` value.
fn apply_network_search_use_dane(config: &mut Option<&mut Config>,
_cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let s = item.as_bool()
.ok_or_else(|| Error::bad_item_type(path, item, "bool"))?;
if let Some(config) = config {
config.network_search_dane = s;
}
Ok(())
}
/// Validates the `network.search.use-wkd` value.
fn apply_network_search_use_wkd(config: &mut Option<&mut Config>,
_cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let s = item.as_bool()
.ok_or_else(|| Error::bad_item_type(path, item, "bool"))?;
if let Some(config) = config {
config.network_search_wkd = s;
}
Ok(())
}
/// Schema for the `policy` section.
const POLICY_SCHEMA: Schema = &[
("aead_algorithms", apply_nop),
("asymmetric_algorithms", apply_nop),
("hash_algorithms", apply_nop),
("packets", apply_nop),
("path", apply_policy_path),
("symmetric_algorithms", apply_nop),
];
/// Validates the `policy` section.
fn apply_policy(config: &mut Option<&mut Config>,
cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let section = item.as_table_like()
.ok_or_else(|| Error::bad_item_type(path, item, "table"))?;
apply_schema(config, cli, Some(path), section.iter(), POLICY_SCHEMA)?;
if let Some(config) = config {
// Extract the inline policy.
// XXX: This doesn't work because toml_edit bug
// https://github.com/toml-rs/toml/issues/785
//
//let table = section.iter().collect::<Table>();
//
// Instead, we have to use a workaround:
let mut table = Table::new();
section.iter().for_each(|(k, v)| { table.insert(k, v.clone()); });
let mut inline = DocumentMut::from(table);
inline.remove("path");
config.policy_inline = Some(inline.to_string().into_bytes());
}
Ok(())
}
/// Validates the `policy.path` value.
fn apply_policy_path(config: &mut Option<&mut Config>,
_: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let path = item.as_str()
.ok_or_else(|| Error::bad_item_type(path, item, "string"))?;
if let Some(config) = config {
config.policy_path = Some(path.into());
}
Ok(())
}
/// Schema for the `servers` section.
const SERVERS_SCHEMA: Schema = &[
("path", apply_servers_path),
];
/// Validates the `servers` section.
fn apply_servers(config: &mut Option<&mut Config>, cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let section = item.as_table_like()
.ok_or_else(|| Error::bad_item_type(path, item, "table"))?;
apply_schema(config, cli, Some(path), section.iter(), SERVERS_SCHEMA)?;
Ok(())
}
/// Validates the `servers.path` value.
fn apply_servers_path(config: &mut Option<&mut Config>,
_: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let path = item.as_str()
.ok_or_else(|| Error::bad_item_type(path, item, "string"))?;
if let Some(config) = config {
config.servers_path = Some(path.into());
}
Ok(())
}