//! Configures a `StandardPolicy` using a configuration file.
//!
//! Sequoia's [`StandardPolicy`] can be configured using Rust.  As
//! with most things, Sequoia's low-level library avoids imposing a
//! policy on users of the library, like where a configuration file
//! should be or even what format it should have.  When necessary, it
//! is up to the application to provide an interface, and to configure
//! the policy appropriately.
//!
//! [`StandardPolicy`]: https://docs.rs/sequoia-openpgp/1.10.0/sequoia_openpgp/policy/struct.StandardPolicy.html
//!
//! This library provides a high-level interface that parses a
//! configuration file, and returns a configured `StandardPolicy`.
//!
//! # Format
//!
//! The file format is based on [TOML].  It contains several sections,
//! one for hash algorithms, one for asymmetric algorithms, etc.
//!
//! [TOML]: https://toml.io/
//!
//! ## Forward Compatibility
//!
//! This parser is strict.  Unknown sections and unknown keys cause a
//! parse error.  To allow configuration files a degree of backwards
//! compatibility, it is possible to set the per-map `ignore_invalid`
//! key to the key or the list of keys that should be ignored if they
//! are not recognized.  For instance, if Sequoia adds support for
//! `SHA4`, then you could do the following to unconditionally reject
//! `SHA4` while ensuring that the configuration is still readable by
//! older versions of Sequoia that don't know about `SHA4`:
//!
//! ```text
//! [hash_algorithms]
//! ignore_invalid = [ "sha4" ]
//! sha4 = never
//! ```
//!
//! (The same principle applies to sections.)
//!
//! ## Cutoff Times
//!
//! Most settings take a so-called cutoff time.  The cutoff time is
//! the time at which an algorithm (e.g., the broken [SHA-1] hash
//! algorithm) or a data structure (e.g. the obsolete [SED packet])
//! should no longer be considered valid.  Using a cutoff time
//! provides more nuance than simply marking an algorithm as invalid.
//! In particular, it allows sun setting algorithms that have been
//! weakened, but are not yet completely broken, and using data that
//! has been saved to a trusted medium before its security was broken.
//!
//! Cutoff times are expressed using TOML's `datetime` datatype, which
//! is an [RFC 3339] formatted timestamp.  The following variants are
//! valid:
//!
//! [SHA-1]: https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh-07#section-14.1
//! [SED packet]: https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh-07#section-5.7
//!
//! Offset datetime:
//!
//! ```text
//! [hash_algorithms]
//! sha1 = 2010-01-01T00:00:00Z
//! ```
//!
//! Local datetime:
//!
//! ```text
//! [hash_algorithms]
//! sha1 = 2010-01-01T00:00:00+00:00
//! ```
//!
//! Local date (interpreted as midnight in UTC):
//!
//! ```text
//! [hash_algorithms]
//! sha1 = 2010-01-01
//! ```
//!
//! The local time format is not supported as it doesn't make sense in
//! this context.
//!
//! [RFC 3339]: https://www.rfc-editor.org/rfc/rfc3339
//!
//! Two special values are also supported: "always" means that the
//! algorithm or data structure should always be considered valid, and
//! "never" means that the algorithm or data structure should never be
//! considered valid.  These values are checked for in a
//! case-sensitive manner.
//!
//! ```text
//! [hash_algorithms]
//! sha1 = "never"
//! ```
//!
//! ## Sections
//!
//! ### Hash Functions
//!
//! Hash algorithms are used to ensure multiple properties.  Of
//! particular relevant in the context of OpenPGP are collision
//! resistance, and second preimage resistance.  In some contexts like
//! self signatures, only second preimage resistance is required.  In
//! other contexts, both are required.  As collision resistance is
//! much easier to attack, these two properties can be set separately.
//!
//! You configure the collision resistance cutoff by setting either
//! the `ALGO` key or or the `ALGO.collision_resistance` key.  And,
//! you configure the second preimage resistance cutoff by setting the
//! `ALGO.second_preimage_resistance` key.
//!
//! A hash algorithm's key is the lower-case version of the value
//! returned by the [`Display` name][hash-display].  For instance,
//! SHA1 is "sha1".
//!
//! [hash-display]: https://docs.rs/sequoia-openpgp/1.10.0/sequoia_openpgp/types/enum.HashAlgorithm.html#impl-Display
//!
//! ```toml
//! [hash_algorithms]
//! md5 = "never"
//! sha1.second_preimage_resistance = 2030-01-01
//! sha1.collision_resistance = 2022-01-01
//! sha512 = "always"
//! ```
//!
//! ### Symmetric Algorithms
//!
//! Like hash algorithms, [symmetric algorithms] can be rejected
//! outright or have a cutoff date.  They don't have any subkeys like
//! `collision_resistance`, so there is only one way to set the
//! cutoff: using the `ALGO` key.
//!
//! [symmetric algorithms]: https://docs.rs/sequoia-openpgp/1.10.0/sequoia_openpgp/types/enum.SymmetricAlgorithm.html#impl-Display
//!
//! The unencrypted variant, the unknown variants, and the private
//! variants cannot currently be set.
//!
//! ```toml
//! [symmetric_algorithms]
//! cast5 = "never"
//! aes128 = "always"
//! ```
//!
//! ### Asymmetric Algorithms
//!
//! Like symmetric algorithms, [asymmetric algorithms] can be rejected
//! outright or have a cutoff date.
//!
//! [asymmetric algorithms]: https://docs.sequoia-pgp.org/sequoia_openpgp/policy/enum.AsymmetricAlgorithm.html#impl-Display
//!
//! The unknown variants, and the private variants cannot currently be
//! set.
//!
//! ```toml
//! [asymmetric_algorithms]
//! rsa1024 = "never"
//! rsa2048 = 2028-01-01
//! ```
//!
//! ### AEAD Algorithms
//!
//! Like symmetric algorithms, [AEAD algorithms] can be rejected
//! outright or have a cutoff date.
//!
//! [AEAD algorithms]: https://docs.rs/sequoia-openpgp/1.10.0/sequoia_openpgp/types/enum.AEADAlgorithm.html#impl-Display
//!
//! The unknown variants, and the private variants cannot currently be
//! set.
//!
//! ```toml
//! [aead_algorithms]
//! eax = "never"
//! ocb = "always"
//! ```
//!
//! ### Packets
//!
//! Like symmetric algorithms, specified packets can be rejected
//! outright or have a cutoff date.
//!
//! The [SED packet] is, for instance, considered broken, and messages
//! that use it should generally be rejected unless they are known to
//! not be from an attacker, e.g., because they were stored on a
//! trusted medium before the attack was feasible.
//!
//! The packets are named after the [names of the Packet variants].
//!
//! [names of the Packet variants]: https://docs.rs/sequoia-openpgp/1.10.0/sequoia_openpgp/packet/enum.Tag.html#variants
//!
//! The reserved packet, the unknown variants, and the private
//! variants cannot currently be set.
//!
//! ```toml
//! [packets]
//! sed = "never"
//! seip = 2028-01-01
//! ```
//!
//! ## Examples
//!
//! ```rust
//! use sequoia_openpgp as openpgp;
//! use openpgp::policy::HashAlgoSecurity;
//! use openpgp::types::HashAlgorithm;
//!
//! use sequoia_policy_config::ConfiguredStandardPolicy;
//!
//! # fn main() -> openpgp::Result<()> {
//! let mut p = ConfiguredStandardPolicy::new();
//! p.from_bytes(b"[hash_algorithms]
//!     sha1.collision_resistance = \"never\"")?;
//! let p = &p.build();
//!
//! assert_eq!(p.hash_cutoff(HashAlgorithm::SHA1,
//!                         HashAlgoSecurity::CollisionResistance),
//!            Some(std::time::UNIX_EPOCH));
//! # Ok(()) }
//! ```

use std::collections::HashSet;
use std::string::String;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;

use sequoia_openpgp as openpgp;
use openpgp::packet::Tag;
use openpgp::policy::AsymmetricAlgorithm;
use openpgp::policy::StandardPolicy;
use openpgp::policy::HashAlgoSecurity;
use openpgp::types::AEADAlgorithm;
use openpgp::types::HashAlgorithm;
use openpgp::types::SymmetricAlgorithm;

use anyhow;
use anyhow::Context as _;

use chrono::Date;
use chrono::DateTime;
use chrono::NaiveDate;
use chrono::Utc;

use toml::value;
use toml::Value;

#[cfg(test)]
mod testdata;

pub struct ConfiguredStandardPolicy<'a> {
    policy: StandardPolicy<'a>,
}

type Result<T, E=anyhow::Error> = std::result::Result<T, E>;

/// Errors used in this crate.
///
/// 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 timestamp.
///
/// None is always.
type Timestamp = Option<SystemTime>;

/// Always accept.
const ALWAYS: Timestamp = None;

/// Never accept.
///
/// By setting the acceptance time to the unix epoch, we never accept
/// anything.
const NEVER: Timestamp = Some(UNIX_EPOCH);

// Parses a timestamp stored in a toml value.
//
// Recognizes the special values "always" and "never".
fn parse_time(t: &Value) -> Result<Timestamp> {
    let t = match t {
        Value::String(t) => t.clone(),
        Value::Datetime(t) => t.to_string(),
        _ => Err(Error::ParseError(
            format!("timestamp is not a string ({:?})", t)))?,
    };

    Ok(match &t[..] {
        "always" => ALWAYS,
        "never" => NEVER,
        t => {
            let t = if let Ok(p) = DateTime::parse_from_rfc3339(t) {
                p.into()
            } else {
                let d = NaiveDate::parse_from_str(&t, "%Y-%m-%d")
                    .context(format!("Parsing timestamp {}", t))?;
                let d: Date<Utc> = Date::from_utc(d, Utc);
                let t = d.and_hms(0, 0, 0);
                t.into()
            };

            Some(t)
        }
    })
}

// Returns an error if a key is unknown.
//
// known_keys better be lowercase.
fn check_sections(path: Option<&str>,
                  section: &value::Map<String, Value>,
                  known_keys: &[&str]) -> Result<()> {
    let prefix = if let Some(path) = path {
        format!("{}.", path)
    } else {
        "".to_string()
    };

    let keys: HashSet<String> = section
        .keys()
        .cloned()
        .collect();

    // The set of allowed keys are the known keys, plus
    // "ignore_invalid", and the value of "ignore_invalid".
    let mut allowed_keys: Vec<String> = known_keys.iter()
        .map(|k| k.to_string())
        .collect();
    if let Some(ignore) = section.get("ignore_invalid") {
        allowed_keys.push("ignore_invalid".to_string());
        match ignore {
            Value::String(k) => allowed_keys.push(k.clone()),
            Value::Array(ks) => {
                for k in ks {
                    if let Value::String(k) = k {
                        allowed_keys.push(k.clone());
                    } 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(&HashSet::<String>::from_iter(allowed_keys.into_iter()))
        .cloned()
        .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());
    }

    Ok(())
}

const HASH_ALGO_MAP: [(&str, HashAlgorithm); 7] = [
    ("md5", HashAlgorithm::MD5),
    ("sha1", HashAlgorithm::SHA1),
    ("ripemd160", HashAlgorithm::RipeMD),
    ("sha256", HashAlgorithm::SHA256),
    ("sha384", HashAlgorithm::SHA384),
    ("sha512", HashAlgorithm::SHA512),
    ("sha224", HashAlgorithm::SHA224)
];

const ASYMM_ALGO_MAP: [(&str, AsymmetricAlgorithm); 18] = [
    ("rsa1024", AsymmetricAlgorithm::RSA1024),
    ("rsa2048", AsymmetricAlgorithm::RSA2048),
    ("rsa3072", AsymmetricAlgorithm::RSA3072),
    ("rsa4096", AsymmetricAlgorithm::RSA4096),
    ("elgamal1024", AsymmetricAlgorithm::ElGamal1024),
    ("elgamal2048", AsymmetricAlgorithm::ElGamal2048),
    ("elgamal3072", AsymmetricAlgorithm::ElGamal3072),
    ("elgamal4096", AsymmetricAlgorithm::ElGamal4096),
    ("dsa1024", AsymmetricAlgorithm::DSA1024),
    ("dsa2048", AsymmetricAlgorithm::DSA2048),
    ("dsa3072", AsymmetricAlgorithm::DSA3072),
    ("dsa4096", AsymmetricAlgorithm::DSA4096),
    ("nistp256", AsymmetricAlgorithm::NistP256),
    ("nistp384", AsymmetricAlgorithm::NistP384),
    ("nistp521", AsymmetricAlgorithm::NistP521),
    ("brainpoolp256", AsymmetricAlgorithm::BrainpoolP256),
    ("brainpoolp512", AsymmetricAlgorithm::BrainpoolP512),
    ("cv25519", AsymmetricAlgorithm::Cv25519),
];

const SYMM_ALGO_MAP: [(&str, SymmetricAlgorithm); 11] = [
    ("idea", SymmetricAlgorithm::IDEA),
    ("tripledes", SymmetricAlgorithm::TripleDES),
    ("cast5", SymmetricAlgorithm::CAST5),
    ("blowfish", SymmetricAlgorithm::Blowfish),
    ("aes128", SymmetricAlgorithm::AES128),
    ("aes192", SymmetricAlgorithm::AES192),
    ("aes256", SymmetricAlgorithm::AES256),
    ("twofish", SymmetricAlgorithm::Twofish),
    ("camellia128", SymmetricAlgorithm::Camellia128),
    ("camellia192", SymmetricAlgorithm::Camellia192),
    ("camellia256", SymmetricAlgorithm::Camellia256),
];

const AEAD_ALGO_MAP: [(&str, AEADAlgorithm); 2] = [
    ("eax", AEADAlgorithm::EAX),
    ("ocb", AEADAlgorithm::OCB),
];

const PACKET_MAP: [(&str, Tag); 18] = [
    ("pkesk", Tag::PKESK),
    ("signature", Tag::Signature),
    ("skesk", Tag::SKESK),
    ("onepasssig", Tag::OnePassSig),
    ("secretkey", Tag::SecretKey),
    ("publickey", Tag::PublicKey),
    ("secretsubkey", Tag::SecretSubkey),
    ("compresseddata", Tag::CompressedData),
    ("sed", Tag::SED),
    ("marker", Tag::Marker),
    ("literal", Tag::Literal),
    ("trust", Tag::Trust),
    ("userid", Tag::UserID),
    ("publicsubkey", Tag::PublicSubkey),
    ("userattribute", Tag::UserAttribute),
    ("seip", Tag::SEIP),
    ("mdc", Tag::MDC),
    ("aed", Tag::AED),
];

impl<'a> ConfiguredStandardPolicy<'a> {
    /// Returns a new `ConfiguredStandardPolicy` using the provided
    /// `StandardPolicy`.
    pub fn from_policy(policy: StandardPolicy<'a>) -> Self {
        ConfiguredStandardPolicy {
            policy
        }
    }

    /// Returns a new `ConfiguredStandardPolicy` with a default
    /// `StandardPolicy`.
    pub fn new() -> Self {
        Self::from_policy(StandardPolicy::new())
    }

    /// Returns a new `ConfiguredStandardPolicy`.
    ///
    /// The `StandardPolicy` is created using [`StandardPolicy::at`].
    ///
    /// [`StandardPolicy::at`]: https://docs.rs/sequoia-openpgp/1.10.0/sequoia_openpgp/policy/struct.StandardPolicy.html#method.at
    pub fn at<T>(t: T) -> Self
    where T: Into<SystemTime>
    {
        Self::from_policy(StandardPolicy::at(t))
    }

    /// Configures the policy according to the configuration data.
    pub fn from_bytes<B>(&mut self, config: B) -> Result<()>
    where B: AsRef<[u8]>
    {
        let config = config.as_ref();
        let config = std::str::from_utf8(config)?.parse::<toml::Value>()?;

        let config = if let Value::Table(config) = config {
            config
        } else {
            return Err(Error::ParseError(
                format!("Expected toml sections")).into());
        };

        check_sections(
            None,
            &config,
            &[
                "hash_algorithms",
                "asymmetric_algorithms",
                "symmetric_algorithms",
                "aead_algorithms",
                "packets",
            ])?;

        macro_rules! doit {
            ($section_name:literal, $algos:ident, $hash_security:literal,
             $call:expr) =>
            {
                if let Some(section) = config.get($section_name) {
                    let section = if let Value::Table(section) = section {
                        section
                    } else {
                        return Err(Error::ParseError(
                            format!("{} is not a map", $section_name)).into());
                    };

                    check_sections(
                        Some($section_name),
                        &section,
                        &$algos.iter().map(|(n, _)| *n).collect::<Vec<_>>()[..])?;

                    // Iterate over the keys/value pairs.
                    for (k, v) in section {
                        let algo = $algos.iter().find_map(|(id, algo)| {
                            if k == *id {
                                Some(algo)
                            } else {
                                None
                            }
                        });
                        let algo = if let Some(algo) = algo {
                            (*algo).clone()
                        } else {
                            // It's unknown, but in "ignore_invalid".
                            continue;
                        };

                        // (collision resistance, second preimage resistance)
                        let t: (Option<Timestamp>, Option<Timestamp>) = match v {
                            Value::Datetime(_) => (Some(parse_time(v)?), None),
                            Value::String(_) => (Some(parse_time(v)?), None),
                            Value::Table(m) if $hash_security => {
                                check_sections(
                                    Some(&format!("{}.{}", $section_name, k)),
                                    &m,
                                    &[
                                        "second_preimage_resistance",
                                        "collision_resistance",
                                    ])?;
                                (m.get("collision_resistance")
                                     .map(parse_time).transpose()?,
                                 m.get("second_preimage_resistance")
                                     .map(parse_time).transpose()?)
                            }
                            v => {
                                return Err(Error::ParseError(format!(
                                    "{}.{}: invalid value ({:?}), expected \
                                     a valid timestamp, always, or never",
                                    $section_name, k, v)).into());
                            }
                        };

                        $call(algo, t);
                    }
                }
            }
        }

        doit!("hash_algorithms",
              HASH_ALGO_MAP,
              true,
              |algo, t: (Option<Timestamp>, Option<Timestamp>)| {
                  if let Some(t) = t.0 {
                      self.policy.reject_hash_property_at(
                          algo, HashAlgoSecurity::CollisionResistance, t);
                  }
                  if let Some(t) = t.1 {
                      self.policy.reject_hash_property_at(
                          algo, HashAlgoSecurity::SecondPreImageResistance, t);
                  }
              });

        doit!("asymmetric_algorithms",
              ASYMM_ALGO_MAP,
              false,
              |algo, t: (Option<Timestamp>, Option<Timestamp>)| {
                  if let Some(t) = t.0 {
                      self.policy.reject_asymmetric_algo_at(
                          algo, t);
                  }
              });

        doit!("symmetric_algorithms",
              SYMM_ALGO_MAP,
              false,
              |algo, t: (Option<Timestamp>, Option<Timestamp>)| {
                  if let Some(t) = t.0 {
                      self.policy.reject_symmetric_algo_at(
                          algo, t);
                  }
              });

        doit!("aead_algorithms",
              AEAD_ALGO_MAP,
              false,
              |algo, t: (Option<Timestamp>, Option<Timestamp>)| {
                  if let Some(t) = t.0 {
                      self.policy.reject_aead_algo_at(
                          algo, t);
                  }
              });

        doit!("packets",
              PACKET_MAP,
              false,
              |algo, t: (Option<Timestamp>, Option<Timestamp>)| {
                  if let Some(t) = t.0 {
                      self.policy.reject_packet_tag_at(
                          algo, t);
                  }
              });

        Ok(())
    }

    /// Returns the configured policy.
    pub fn build(self) -> StandardPolicy<'a> {
        self.policy
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use std::time::Duration;

    use openpgp::Cert;
    use openpgp::parse::Parse;

    // Check that invalid sections cause an error, unless they are
    // explicitly ignored.
    #[test]
    fn invalid_section() -> Result<()> {
        let mut p = ConfiguredStandardPolicy::new();

        assert!(p.from_bytes(b"x=1").is_err());

        // A known section.
        p.from_bytes(b"[hash_algorithms]
sha1 = \"always\"
").expect("valid");

        // A known section and an unknown section.
        assert!(p.from_bytes(b"[hash_algorithms]
sha1 = \"always\"

[x]
blah = 1
").is_err());

        // A known section, and an unknown section that we should
        // ignore.
        p.from_bytes(b"ignore_invalid = \"x\"

[hash_algorithms]
sha1 = \"always\"

[x]
blah = 1
").expect("valid");

        p.from_bytes(b"ignore_invalid = [ \"x\", \"y\" ]

[hash_algorithms]
sha1 = \"always\"

[x]
blah = 1

[y]
blah = 1
").expect("valid");

        // Section names are case sensitive.
        assert!(p.from_bytes(b"[HASH_ALGORITHMS]
sha1 = \"never\"
").is_err());
        assert!(p.from_bytes(b"[Hash_Algorithms]
sha1 = \"never\"
").is_err());

        // Underscores can't be replaced by spaces.
        assert!(p.from_bytes(b"[HASH_ALGORITHMS]
sha1 = \"never\"
").is_err());

        Ok(())
    }

    // Make sure invalid keys cause an error, unless they are ignored.
    #[test]
    fn invalid_keys() -> Result<()> {
        let mut p = ConfiguredStandardPolicy::new();

        // A known section with known keys.
        p.from_bytes(b"[hash_algorithms]
sha1 = \"always\"
sha224 = \"never\"
").expect("valid");

        // A known section with unknown keys.
        assert!(p.from_bytes(b"[hash_algorithms]
sha1 = \"always\"
sha99 = \"never\"
").is_err());

        // A known section with unknown keys, the ignore_invalid
        // directive is in the wrong place.
        assert!(p.from_bytes(b"
ignore_invalid = \"SHA99\"

[hash_algorithms]
sha1 = \"always\"
SHA99 = \"never\"
").is_err());

        // A known section with unknown, but ignored keys.
        p.from_bytes(b"[hash_algorithms]
ignore_invalid = \"SHA99\"
sha1 = \"always\"
SHA99 = \"never\"
").expect("valid");

        // A known section with unknown keys, which are incorrectly
        // ignore (wrong case).
        assert!(p.from_bytes(b"
[hash_algorithms]
ignore_invalid = \"SHA99\"
sha1 = \"always\"
sha99 = \"never\"
").is_err());

        Ok(())
    }

    /// Set one property at a time via a config file.  Check that is
    /// has been set.  Where appropriate, check that related
    /// properties have not been changed relative to the standard
    /// policy.
    #[test]
    fn get_set_one() -> Result<()> {
        let sp = StandardPolicy::new();

        let times: [(&str, Timestamp); 5] = [
            ("never", NEVER),
            ("always", ALWAYS),
            // $ date -u +%s --date='20200101'
            // 1577836800
            ("2020-01-01T00:00:00Z",
             Some(UNIX_EPOCH + Duration::new(1577836800, 0))),
            // $ date -u +%s --date='20500101'
            // 2524608000
            ("2050-01-01T00:00:00+00:00",
             Some(UNIX_EPOCH + Duration::new(2524608000, 0))),
            // $ date -u +%s --date='21050101'
            // 4260211200
            ("2105-01-01",
             Some(UNIX_EPOCH + Duration::new(4260211200, 0))),
        ];

        macro_rules! check_hash {
            ($id:expr, $algo:expr, $prop:expr, $time:expr, $config:expr) => {
                let mut p = ConfiguredStandardPolicy::new();
                p.from_bytes($config).expect("valid");
                let p = p.build();
                assert_eq!(p.hash_cutoff(*$algo, $prop),
                           $time.1,
                           "algo: {}, t: {}", $id, $time.0);
                // Check that the other security property didn't change.
                let other = if $prop == HashAlgoSecurity::CollisionResistance {
                    HashAlgoSecurity::SecondPreImageResistance
                } else {
                    HashAlgoSecurity::CollisionResistance
                };

                assert_eq!(
                    sp.hash_cutoff(*$algo, other), p.hash_cutoff(*$algo, other),
                    "algo: {}, t: {}", $id, $time.0);
            }
        }

        for time in times.iter() {
            for (id, algo) in HASH_ALGO_MAP.iter() {
                // Collision resistance (implicit).
                check_hash!(id, algo, HashAlgoSecurity::CollisionResistance,
                            time,
                            format!(
"[hash_algorithms]
{} = \"{}\"
",
                                id, time.0));

                // Collision resistance (explicit).
                check_hash!(id, algo, HashAlgoSecurity::CollisionResistance,
                            time,
                            format!(
"[hash_algorithms]
{}.collision_resistance = \"{}\"
",
                                id, time.0));

                // Second preimage resistance.
                check_hash!(id, algo, HashAlgoSecurity::SecondPreImageResistance,
                            time,
                            format!(
"[hash_algorithms]
{}.second_preimage_resistance = \"{}\"
",
                                id, time.0));

                // Different ways of naming the key.
                check_hash!(id, algo, HashAlgoSecurity::CollisionResistance,
                            time,
                            format!(
"[hash_algorithms.{}]
collision_resistance = \"{}\"
",
                                id, time.0));

                check_hash!(id, algo, HashAlgoSecurity::CollisionResistance,
                            time,
                            format!(
"hash_algorithms.{}.collision_resistance = \"{}\"
",
                                id, time.0));
            }
        }

        macro_rules! check {
            ($id:expr, $algo:expr, $time:expr, $config:expr, $get:ident) => {
                let mut p = ConfiguredStandardPolicy::new();
                p.from_bytes($config).expect("valid");
                let p = p.build();
                assert_eq!(p.$get($algo.clone()),
                           $time.1,
                           "algo: {}, t: {}", $id, $time.0);
            }
        }

        for time in times.iter() {
            for (id, algo) in ASYMM_ALGO_MAP.iter() {
                check!(id, algo, time,
                       format!(
"[asymmetric_algorithms]
{} = \"{}\"
",
                           id, time.0),
                       asymmetric_algo_cutoff);
            }
        }

        for time in times.iter() {
            for (id, algo) in SYMM_ALGO_MAP.iter() {
                check!(id, algo, time,
                       format!(
"[symmetric_algorithms]
{} = \"{}\"
",
                           id, time.0),
                       symmetric_algo_cutoff);
            }
        }

        for time in times.iter() {
            for (id, algo) in AEAD_ALGO_MAP.iter() {
                check!(id, algo, time,
                       format!(
"[aead_algorithms]
{} = \"{}\"
",
                           id, time.0),
                       aead_algo_cutoff);
            }
        }

        for time in times.iter() {
            for (id, algo) in PACKET_MAP.iter() {
                check!(id, algo, time,
                       format!(
"[packets]
{} = \"{}\"
",
                           id, time.0),
                       packet_tag_cutoff);
            }
        }

        Ok(())
    }

    // Convenience function for using a timestamp.
    fn ts(t: &str) -> Option<SystemTime> {
        parse_time(&Value::String(t.into())).expect("valid timestamp")
    }

    /// Check a simple config file.
    #[test]
    fn simple_config() -> Result<()> {
        let _sp = StandardPolicy::new();

        // Something simple.
        let mut p = ConfiguredStandardPolicy::new();
        p.from_bytes(b"[hash_algorithms]
sha1 = \"always\"
sha224 = 2017-03-04T13:25:35Z
sha512.second_preimage_resistance = \"always\"
sha512.collision_resistance = 2050-01-01

[symmetric_algorithms]
cast5 = \"always\"
aes128 = \"2040-01-01\"
aes256 = \"2050-01-01\"
").expect("valid config");
        let p = p.build();

        assert_eq!(
            p.hash_cutoff(HashAlgorithm::SHA1,
                          HashAlgoSecurity::CollisionResistance),
            ts("always"));
        assert_eq!(
            p.hash_cutoff(HashAlgorithm::SHA224,
                          HashAlgoSecurity::CollisionResistance),
            ts("2017-03-04T13:25:35Z"));

        assert_eq!(
            p.hash_cutoff(HashAlgorithm::SHA512,
                          HashAlgoSecurity::CollisionResistance),
            ts("2050-01-01"));
        assert_eq!(
            p.hash_cutoff(HashAlgorithm::SHA512,
                          HashAlgoSecurity::SecondPreImageResistance),
            ts("always"));

        assert_eq!(
            p.symmetric_algo_cutoff(SymmetricAlgorithm::CAST5),
            ts("always"));
        assert_eq!(
            p.symmetric_algo_cutoff(SymmetricAlgorithm::AES128),
            ts("2040-01-01"));
        assert_eq!(
            p.symmetric_algo_cutoff(SymmetricAlgorithm::AES256),
            ts("2050-01-01"));

        Ok(())
    }

    // Configure everything, then read it out again.
    #[test]
    fn all_config() -> Result<()> {
        let epoch = NaiveDate::from_ymd(2020, 1, 1);
        let mut d = epoch;
        let mut date = || -> String {
            let s = d.format("%Y-%m-%d");
            d = d.succ();
            s.to_string()
        };

        let mut config = String::new();
        config.push_str("[hash_algorithms]\n");
        for (id, algo) in HASH_ALGO_MAP {
            config.push_str(
                &format!("{} = {} # {}\n", id, date(), algo));
        }
        config.push_str("\n[asymmetric_algorithms]\n");
        for (id, algo) in ASYMM_ALGO_MAP {
            config.push_str(
                &format!("{} = {} # {}\n", id, date(), algo));
        }
        config.push_str("\n[symmetric_algorithms]\n");
        for (id, algo) in SYMM_ALGO_MAP {
            config.push_str(
                &format!("{} = {} # {}\n", id, date(), algo));
        }
        config.push_str("\n[aead_algorithms]\n");
        for (id, algo) in AEAD_ALGO_MAP {
            config.push_str(
                &format!("{} = {} # {}\n", id, date(), algo));
        }
        config.push_str("\n[packets]\n");
        for (id, algo) in PACKET_MAP {
            config.push_str(
                &format!("{} = {} # {}\n", id, date(), algo));
        }

        let mut p = ConfiguredStandardPolicy::new();
        p.from_bytes(config.clone())
            .expect(&format!("valid config: {}", config));
        let p = p.build();

        // Reset the date to the epoch.
        let mut d = epoch;
        let mut date = || -> String {
            let s = d.format("%Y-%m-%d");
            d = d.succ();
            s.to_string()
        };

        // Make sure everything we set, was really set.
        for (_, algo) in HASH_ALGO_MAP {
            assert_eq!(
                p.hash_cutoff(algo, HashAlgoSecurity::CollisionResistance),
                ts(&date()));
        }
        for (_, algo) in ASYMM_ALGO_MAP {
            assert_eq!(p.asymmetric_algo_cutoff(algo), ts(&date()));
        }
        for (_, algo) in SYMM_ALGO_MAP {
            assert_eq!(p.symmetric_algo_cutoff(algo), ts(&date()));
        }
        for (_, algo) in AEAD_ALGO_MAP {
            assert_eq!(p.aead_algo_cutoff(algo), ts(&date()));
        }
        for (_, algo) in PACKET_MAP {
            assert_eq!(p.packet_tag_cutoff(algo), ts(&date()));
        }

        Ok(())
    }

    #[test]
    fn check_sig() -> Result<()> {
        let alice = testdata::file("alice-secret.asc");

        let cert = Cert::from_bytes(alice).expect("valid cert");

        let now = ts("2022-10-30").unwrap();

        // Should be valid according to the standard policy.
        let sp = &StandardPolicy::at(now);
        let _cert = cert.with_policy(sp, None).expect("valid under standard policy");

        // If SHA512 has no collision resistance, the cert is still valid.
        let mut p = ConfiguredStandardPolicy::at(now);
        p.from_bytes(b"[hash_algorithms]
sha512.collision_resistance = \"never\"")
            .expect(&format!("valid config"));
        let p = p.build();
        let _cert = cert.with_policy(&p, None)
            .expect("valid under: SHA512 has no collision resistance");

        // If SHA512 has no 2nd preimage resistance, the cert is not valid.
        let mut p = ConfiguredStandardPolicy::at(now);
        p.from_bytes(b"[hash_algorithms]
sha512.second_preimage_resistance = \"never\"")
            .expect(&format!("valid config"));
        let p = p.build();
        let _cert = assert!(
            cert.with_policy(&p, None).is_err(),
            "invalid under: SHA512 has no 2nd preimage resistance");

        // If SHA512's 2nd preimage resistance is cutoff, it should be
        // invalid.
        let mut p = ConfiguredStandardPolicy::at(now);
        p.from_bytes(&format!("[hash_algorithms]
sha512.second_preimage_resistance = 2022-10-24"))
            .expect(&format!("valid config"));
        let p = p.build();
        let _cert = assert!(
            cert.with_policy(&p, None).is_err(),
            "should be invalid under: SHA512 2nd preimage resistance cut off");

        // If SHA512's 2nd preimage resistance will be cutoff, it
        // should be invalid.
        let mut p = ConfiguredStandardPolicy::at(ts("2022-10-26").unwrap());
        p.from_bytes(&format!("[hash_algorithms]
sha512.second_preimage_resistance = 2022-10-27"))
            .expect(&format!("valid config"));
        let p = p.build();
        let _cert = cert.with_policy(&p, ts("2022-10-26")).expect(
            "valid under: SHA512 2nd preimage resistance not yet cut off");

        let mut p = ConfiguredStandardPolicy::at(ts("2022-10-29").unwrap());
        p.from_bytes(&format!("[hash_algorithms]
sha512.second_preimage_resistance = 2022-10-27"))
            .expect(&format!("valid config"));
        let p = p.build();
        let _cert = assert!(
            cert.with_policy(&p, ts("2022-10-28")).is_err(),
            "invalid under: SHA512 2nd preimage resistance cut off");

        Ok(())
    }
}
