diff options
author | Neal H. Walfield <neal@pep.foundation> | 2022-11-17 13:45:34 +0100 |
---|---|---|
committer | Neal H. Walfield <neal@pep.foundation> | 2022-11-17 22:51:08 +0100 |
commit | d638a95af5e3505436c262f6ca09ebfa11bfe77d (patch) | |
tree | 05b531fb777befda0bff9b069873d245e392ea7f /openpgp/src/policy | |
parent | 573d02d2425d329a6c982bcf1374f577d599f83c (diff) |
openpgp: Allow setting a policy for specific versions of a packet.
- For some packets we'd like to have different policies depending on
the version. This in particular applies to Signatures: by default
we want to reject v3 signatures, but accept v4 signatures.
- By default, reject v3 signatures as of 2007.
- Fixes: #945
Diffstat (limited to 'openpgp/src/policy')
-rw-r--r-- | openpgp/src/policy/cutofflist.rs | 313 |
1 files changed, 306 insertions, 7 deletions
diff --git a/openpgp/src/policy/cutofflist.rs b/openpgp/src/policy/cutofflist.rs index d745ae7d..6bbc8eb7 100644 --- a/openpgp/src/policy/cutofflist.rs +++ b/openpgp/src/policy/cutofflist.rs @@ -1,6 +1,5 @@ use std::fmt; - -use std::ops::{Index, IndexMut}; +use std::ops::{Deref, Index, IndexMut}; use crate::{ Error, @@ -50,15 +49,37 @@ impl<'a, T> VecOrSlice<'a, T> { fn resize(&mut self, size: usize, value: T) where T: Clone { - let mut v : Vec<T> = match self { + let v = self.as_mut(); + v.resize(size, value); + } + + pub(super) fn as_mut(&mut self) -> &mut Vec<T> + where T: Clone + { + let v: Vec<T> = match self { VecOrSlice::Vec(ref mut v) => std::mem::take(v), VecOrSlice::Slice(s) => s.to_vec(), - VecOrSlice::Empty() => Vec::with_capacity(size), + VecOrSlice::Empty() => Vec::new(), }; - v.resize(size, value); - *self = VecOrSlice::Vec(v); + if let VecOrSlice::Vec(ref mut v) = self { + v + } else { + unreachable!() + } + } +} + +impl<'a, T> Deref for VecOrSlice<'a, T> { + type Target = [T]; + + fn deref(&self) -> &Self::Target { + match self { + VecOrSlice::Vec(ref v) => &v[..], + VecOrSlice::Slice(s) => s, + VecOrSlice::Empty() => &[], + } } } @@ -140,7 +161,7 @@ impl<A> CutoffList<A> if i >= self.cutoffs.len() { // We reject by default. - self.cutoffs.resize(i + 1, DEFAULT_POLICY) + self.cutoffs.resize(i + 1, DEFAULT_POLICY); } self.cutoffs[i] = cutoff; } @@ -267,3 +288,281 @@ macro_rules! a_cutoff_list { } } } + +/// A data structure may have multiple versions. For instance, there +/// are multiple versions of packets. Each version of a given packet +/// may have different security properties. +#[derive(Debug, Clone)] +pub(super) struct VersionedCutoffList<A> where A: 'static { + // Indexed by `A as u8`. + // + // A value of `None` means that no vulnerabilities are known. + // + // Note: we use `u64` and not `SystemTime`, because there is no + // way to construct a `SystemTime` in a `const fn`. + pub(super) unversioned_cutoffs: VecOrSlice<'static, Option<Timestamp>>, + + // The content is: (algo, version, policy). + pub(super) versioned_cutoffs: + VecOrSlice<'static, (A, u8, Option<Timestamp>)>, + + pub(super) _a: std::marker::PhantomData<A>, +} + +impl<A> Default for VersionedCutoffList<A> { + fn default() -> Self { + Self::reject_all() + } +} + +impl<A> VersionedCutoffList<A> { + // Rejects all algorithms. + pub(super) const fn reject_all() -> Self { + Self { + unversioned_cutoffs: VecOrSlice::empty(), + versioned_cutoffs: VecOrSlice::empty(), + _a: std::marker::PhantomData, + } + } +} + +impl<A> VersionedCutoffList<A> + where u8: From<A>, + A: fmt::Display, + A: std::clone::Clone, + A: Eq, + A: Ord, +{ + // versioned_cutoffs must be sorted and deduplicated. Make sure + // it is so. + pub(super) fn assert_sorted(&self) { + if cfg!(debug_assertions) || cfg!(test) { + for window in self.versioned_cutoffs.windows(2) { + let a = &window[0]; + let b = &window[1]; + + // Sorted, no duplicates. + assert!((&a.0, a.1) < (&b.0, b.1)); + } + } + } + + // Sets a cutoff time for version `version` of algorithm `algo`. + pub(super) fn set_versioned(&mut self, + algo: A, version: u8, + cutoff: Option<Timestamp>) + { + self.assert_sorted(); + let cutofflist = self.versioned_cutoffs.as_mut(); + match cutofflist.binary_search_by(|(a, v, _)| { + algo.cmp(a).then(version.cmp(v)).reverse() + }) { + Ok(i) => { + // Replace. + cutofflist[i] = (algo, version, cutoff); + } + Err(i) => { + // Insert. + cutofflist.insert(i, (algo, version, cutoff)); + } + }; + self.assert_sorted(); + } + + // Sets a cutoff time for algorithm `algo`. + pub(super) fn set_unversioned(&mut self, algo: A, + cutoff: Option<Timestamp>) + { + let i: u8 = algo.into(); + let i: usize = i.into(); + + if i >= self.unversioned_cutoffs.len() { + // We reject by default. + self.unversioned_cutoffs.resize(i + 1, DEFAULT_POLICY); + } + self.unversioned_cutoffs[i] = cutoff; + } + + // Returns the cutoff time for version `version` of algorithm `algo`. + #[inline] + pub(super) fn cutoff(&self, algo: A, version: u8) -> Option<Timestamp> { + self.assert_sorted(); + match self.versioned_cutoffs.binary_search_by(|(a, v, _)| { + algo.cmp(a).then(version.cmp(v)).reverse() + }) { + Ok(i) => { + self.versioned_cutoffs[i].2 + } + Err(_loc) => { + // Fallback to the unversioned cutoff list. + *self.unversioned_cutoffs.get(u8::from(algo) as usize) + .unwrap_or(&DEFAULT_POLICY) + } + } + } + + // Checks whether version `version` of the algorithm `algo` is safe + // to use at time `time`. + // + // `tolerance` is added to the cutoff time. + #[inline] + pub(super) fn check(&self, a: A, version: u8, time: Timestamp, + tolerance: Option<Duration>) + -> Result<()> + { + if let Some(cutoff) = self.cutoff(a.clone(), version) { + let cutoff = cutoff + .checked_add(tolerance.unwrap_or_else(|| Duration::seconds(0))) + .unwrap_or(Timestamp::MAX); + if time >= cutoff { + Err(Error::PolicyViolation( + a.to_string(), Some(cutoff.into())).into()) + } else { + Ok(()) + } + } else { + // None => always secure. + Ok(()) + } + } +} + +macro_rules! a_versioned_cutoff_list { + ($name:ident, $algo:ty, + // A slice indexed by the algorithm. + $unversioned_values_count: expr, $unversioned_values: expr, + // A slice of the form: [ (algo, version, cutoff), ... ] + // + // Note: the values must be sorted and (algo, version) must be + // unique! + $versioned_values_count:expr, $versioned_values:expr) => { + // It would be nicer to just have a `CutoffList` and store the + // default as a `VecOrSlice::Slice`. Unfortunately, we can't + // create a slice in a `const fn`, so that doesn't work. + // + // To work around that issue, we store the array in the + // wrapper type, and remember if we are using it or a custom + // version. + #[derive(Debug, Clone)] + enum $name { + Default(), + Custom(VersionedCutoffList<$algo>), + } + + impl std::ops::Deref for $name { + type Target = VersionedCutoffList<$algo>; + + fn deref(&self) -> &Self::Target { + match self { + $name::Default() => &Self::DEFAULT, + $name::Custom(l) => l, + } + } + } + + #[allow(unused)] + impl $name { + const VERSIONED_DEFAULTS: + [ ($algo, u8, Option<Timestamp>); $versioned_values_count ] + = $versioned_values; + const UNVERSIONED_DEFAULTS: + [ Option<Timestamp>; $unversioned_values_count ] + = $unversioned_values; + + const DEFAULT: VersionedCutoffList<$algo> = VersionedCutoffList { + versioned_cutoffs: + crate::policy::cutofflist::VecOrSlice::Slice( + &Self::VERSIONED_DEFAULTS), + unversioned_cutoffs: + crate::policy::cutofflist::VecOrSlice::Slice( + &Self::UNVERSIONED_DEFAULTS), + _a: std::marker::PhantomData, + }; + + // Turn the `Foo::Default` into a `Foo::Custom`, if + // necessary, to allow modification. + fn force(&mut self) -> &mut VersionedCutoffList<$algo> { + use crate::policy::cutofflist::VecOrSlice; + + if let $name::Default() = self { + *self = Self::Custom($name::DEFAULT); + } + + match self { + $name::Custom(ref mut l) => l, + _ => unreachable!(), + } + } + + // Set the cutoff for the specified version of the + // specified algorithm. + fn set_versioned(&mut self, algo: $algo, version: u8, + cutoff: Option<Timestamp>) + { + self.force().set_versioned(algo, version, cutoff) + } + + // Sets the cutoff for the specified algorithm independent + // of its version. + fn set_unversioned(&mut self, algo: $algo, + cutoff: Option<Timestamp>) + { + // Clear any versioned cutoffs. + let l = self.force(); + l.versioned_cutoffs.as_mut().retain(|(a, _v, _c)| { + &algo != a + }); + + l.set_unversioned(algo, cutoff) + } + + // Resets the cutoff list to its defaults. + fn defaults(&mut self) { + *self = Self::Default(); + } + + // Causes the cutoff list to reject everything. + fn reject_all(&mut self) { + *self = Self::Custom(VersionedCutoffList::reject_all()); + } + + // Returns the cutoff for the specified version of the + // specified algorithm. + // + // This first considers the versioned cutoff list. If + // there is no entry in the versioned list, it fallsback + // to the unversioned cutoff list. If there is also no + // entry there, then it falls back to the default. + fn cutoff(&self, algo: $algo, version: u8) -> Option<Timestamp> { + let cutofflist = if let $name::Custom(ref l) = self { + l + } else { + &Self::DEFAULT + }; + + cutofflist.cutoff(algo, version) + } + + fn check(&self, algo: $algo, version: u8, + time: Timestamp, d: Option<types::Duration>) + -> Result<()> + { + let cutofflist = if let $name::Custom(ref l) = self { + l + } else { + &Self::DEFAULT + }; + + cutofflist.check(algo, version, time, d) + } + } + + // Make sure VERSIONED_DEFAULTS is sorted and the keys are + // unique. + #[test] + #[allow(non_snake_case)] + fn $name() { + $name::DEFAULT.assert_sorted(); + } + } +} |