summaryrefslogtreecommitdiffstats
path: root/openpgp/src/policy
diff options
context:
space:
mode:
authorNeal H. Walfield <neal@pep.foundation>2022-11-17 13:45:34 +0100
committerNeal H. Walfield <neal@pep.foundation>2022-11-17 22:51:08 +0100
commitd638a95af5e3505436c262f6ca09ebfa11bfe77d (patch)
tree05b531fb777befda0bff9b069873d245e392ea7f /openpgp/src/policy
parent573d02d2425d329a6c982bcf1374f577d599f83c (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.rs313
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();
+ }
+ }
+}