Skip to main content

openpgp_cert_d/
lib.rs

1//! Shared OpenPGP Certificate Directory
2//!
3//! This crate implements a generic [OpenPGP certificate store] that can
4//! be shared between implementations.  It also defines a way to root
5//! trust, and a way to associate pet names with certificates.
6//! Sharing certificates and trust decisions increases security by
7//! enabling more applications to take advantage of OpenPGP.  It also
8//! improves privacy by reducing the required certificate discoveries
9//! that go out to the network.
10//!
11//! Note that this crate is only concerned with the low-level
12//! mechanics of the certificate directory and does not depend on an
13//! OpenPGP implementation.  This is the reason that it works with
14//! bytes and not high-level OpenPGP data structures.  Generally, it
15//! has to be combined with an OpenPGP implementation to be useful.
16//!
17//! [OpenPGP certificate store]: https://datatracker.ietf.org/doc/draft-nwjw-openpgp-cert-d/
18
19use std::time::{Duration, UNIX_EPOCH};
20
21#[macro_use]
22mod macros;
23mod certd;
24pub use certd::CertD;
25pub use certd::MergeResult;
26
27mod error;
28pub use error::*;
29
30mod pgp;
31
32#[cfg(unix)]
33mod unixdir;
34
35/// Special name of the trust root.
36///
37/// This is the [special name] under which the [trust root] is stored.
38///
39/// [special name]: https://www.ietf.org/archive/id/draft-nwjw-openpgp-cert-d-00.html#name-special-names
40/// [trust root]: https://www.ietf.org/archive/id/draft-nwjw-openpgp-cert-d-00.html#name-trust-root
41pub const TRUST_ROOT: &str = "trust-root";
42
43// SPECIAL_NAMES must be sorted. This allows using
44// SPECIAL_NAMES.binary_search(needle).is_ok() (O(log(n))) instead of
45// SPECIAL_NAMES.contains(needle) (O(n))).
46const SPECIAL_NAMES: &[&str] = &[TRUST_ROOT];
47
48/// Facilitates caching of derived data.
49///
50/// Every time you look up a cert in the directory, the operation also
51/// returns a tag.  This tag, which can be converted to a `u64`, is an
52/// opaque identifier that changes whenever the cert in the directory
53/// changes.
54///
55/// To use it, store the tag with your cached data, and every time you
56/// re-do the lookup, compare the returned tag with the stored tag to
57/// see if your cached data is still up-to-date.
58///
59/// # Examples
60///
61/// This demonstrates how to use the tag to prevent useless
62/// recomputations.
63///
64/// ```
65/// # use openpgp_cert_d::*;
66/// # fn dosth(certd: &CertD) -> Result<()> {
67/// let fp = "eb85bb5fa33a75e15e944e63f231550c4f47e38e";
68/// let (tag, cert) = certd.get(fp)?.expect("cert to exist");
69/// // ...
70/// if let Some((new_cert, new_tag)) = certd.get_if_changed(tag, fp)? {
71///     // cert changed...
72/// } else {
73///     // cert didn't change...
74/// }
75/// # Ok(()) }
76/// ```
77#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
78pub struct Tag(pub u64);
79
80impl Tag {
81    fn new(mtime_secs: u64, mtime_nsec: u32, size: u64, is_dir: bool)
82        -> Self
83    {
84        // We estimate the entropy of the parameters as follows:
85        //
86        // - mtime_secs: log2(age of least recently updated certificate)
87        //
88        //   Assuming the least recently updated certificate was
89        //   updated a year ago and a uniform distribution of
90        //   insertions and updates, there are 31536000 possible
91        //   values, which corresponds to 25 bits of entropy.
92        //
93        // - mtime_nsecs: log2(1 billion) = just under 30 bits of entropy.
94        //
95        //   Since all values are equally likely, this represents
96        //
97        // - size: log2(large certificate size)
98        //
99        //   Assuming certificates can grow to be about 1 MB large and
100        //   their size is uniformly distributed, we'd have log2(1 M)
101        //   = 20 bits of entropy.  In practice, we expect clustering
102        //   around different typical sizes.
103        //
104        // We can empirically estimate entropy using a maximum
105        // likelihood estimator for each bit, and summing them.  This
106        // is done in the `entropy` example.  On my certificate store
107        // with 3285 certificates, I see:
108        //
109        // ```
110        // $ cargo run --release --example entropy
111        //  0: size:  50% ( 1639); secs:  51% ( 1664); nanos:  50% ( 1641)
112        //  1: size:  50% ( 1654); secs:  48% ( 1563); nanos:  50% ( 1654)
113        //  2: size:  51% ( 1663); secs:  49% ( 1625); nanos:  50% ( 1641)
114        //  3: size:  50% ( 1640); secs:  55% ( 1810); nanos:  50% ( 1656)
115        //  4: size:  50% ( 1632); secs:  45% ( 1466); nanos:  50% ( 1647)
116        //  5: size:  48% ( 1577); secs:  55% ( 1812); nanos:  50% ( 1631)
117        //  6: size:  53% ( 1730); secs:  62% ( 2039); nanos:  50% ( 1635)
118        //  7: size:  51% ( 1671); secs:  63% ( 2056); nanos:  52% ( 1694)
119        //  8: size:  49% ( 1618); secs:  38% ( 1246); nanos:  49% ( 1619)
120        //  9: size:  49% ( 1594); secs:  63% ( 2060); nanos:  50% ( 1636)
121        // 10: size:  47% ( 1536); secs:  63% ( 2055); nanos:  50% ( 1656)
122        // 11: size:  53% ( 1741); secs:  63% ( 2068); nanos:  51% ( 1660)
123        // 12: size:  39% ( 1274); secs:  37% ( 1218); nanos:  50% ( 1641)
124        // 13: size:  28% (  935); secs:  38% ( 1254); nanos:  50% ( 1649)
125        // 14: size:  21% (  701); secs:  37% ( 1225); nanos:  51% ( 1664)
126        // 15: size:  12% (  381); secs:  65% ( 2124); nanos:  50% ( 1630)
127        // 16: size:   6% (  210); secs:  60% ( 1972); nanos:  50% ( 1640)
128        // 17: size:   3% (   97); secs:  38% ( 1240); nanos:  50% ( 1649)
129        // 18: size:   1% (   33); secs:  35% ( 1144); nanos:  49% ( 1610)
130        // 19: size:   0% (    7); secs:  67% ( 2216); nanos:  50% ( 1638)
131        // 20: size:   0% (    3); secs:  47% ( 1534); nanos:  49% ( 1608)
132        // 21: size:   0% (    1); secs:  50% ( 1641); nanos:  50% ( 1656)
133        // 22: size:   0% (    0); secs:  73% ( 2405); nanos:  49% ( 1598)
134        // 23: size:   0% (    0); secs:  98% ( 3218); nanos:  50% ( 1634)
135        // 24: size:   0% (    0); secs:   1% (   38); nanos:  50% ( 1628)
136        // 25: size:   0% (    0); secs:   0% (    0); nanos:  51% ( 1659)
137        // 26: size:   0% (    0); secs: 100% ( 3284); nanos:  47% ( 1538)
138        // 27: size:   0% (    0); secs:   0% (    0); nanos:  46% ( 1500)
139        // 28: size:   0% (    0); secs:   0% (    0); nanos:  44% ( 1460)
140        // 29: size:   0% (    0); secs: 100% ( 3284); nanos:  47% ( 1540)
141        // 30: size:   0% (    0); secs: 100% ( 3284); nanos:   0% (    0)
142        // 31: size:   0% (    0); secs:   0% (    0); nanos:   0% (    0)
143        // ...
144        // Maximum-likelihood estimate of entropy (max: 64 bits):
145        //   size: 15.73 bits
146        //   secs: 22.34 bits
147        //   nanos: 29.98 bits
148        //   max empirical entropy: 68.05 bits
149        // ```
150        //
151        // So for me, size has a bit less than 16 bits of entropy,
152        // most of which appears to be concentrated in bit 0 through
153        // bit 11.
154        //
155        // For mtime_secs, we expect less significant bits to have
156        // more entropy than higher bits, and that matches what we
157        // observe.  The amount of entropy through bit 20 is pretty
158        // good.  That is followed by a steep drop in the amount of
159        // entropy.  After bit 25, there is no entropy.
160        //
161        // For nano seconds, we expect the bits needs to represent the
162        // range of values to have nearly full entropy, and that is
163        // also what we see, i.e., just under 30 bits of entropy.
164        //
165        // Empirically we observed 68 bits of entropy, which is close
166        // to our intuition.
167        //
168        // Based on the above analysis, we mix the values by xoring
169        // the parameters as follows:
170        //
171        // - mtime_secs as is,
172        // - mtime_nsec left shifted by 34, and
173        // - size left rotated by 22.
174        //
175        // Using this algorithm on my current certificate directory, I
176        // observe the tags have nearly maximum entropy: 63.23 bits
177        // out of a maximum of 64 bits of entropy:
178        //
179        // ```
180        // $ cargo run --release --example tag-entropy /tmp/test-certd
181        // ...
182        // Maximum-likelihood estimate of entropy (max: 64 bits):
183        //   tag: 63.23 bits
184        // ```
185        //
186        // In conclusion, it's unclear that using a cryptographic hash
187        // function will add much.
188        //
189        // Note: as directory sizes are not constant across file
190        // systems, and we want to support synchronization, we don't
191        // consider the size parameter for directories.
192        Tag(mtime_secs
193            ^ ((mtime_nsec as u64) << 34)
194            ^ if is_dir { 0 } else { size.rotate_left(22) })
195    }
196}
197
198impl std::convert::TryFrom<&std::fs::File> for Tag {
199    type Error = std::io::Error;
200
201    /// Compute a `Tag` from file metadata.
202    fn try_from(fp: &std::fs::File) -> std::io::Result<Self> {
203        Tag::try_from(fp.metadata()?)
204    }
205}
206
207impl std::convert::TryFrom<&std::fs::Metadata> for Tag {
208    type Error = std::io::Error;
209
210    /// Compute a `Tag` from file metadata.
211    fn try_from(m: &std::fs::Metadata) -> std::io::Result<Self> {
212        let d = m
213            .modified()?
214            .duration_since(UNIX_EPOCH)
215            .unwrap_or_else(|_| Duration::new(0, 0));
216
217        Ok(Tag::new(d.as_secs(), d.subsec_nanos(), m.len(), m.is_dir()))
218    }
219}
220
221impl std::convert::TryFrom<std::fs::Metadata> for Tag {
222    type Error = std::io::Error;
223
224    /// Compute a `Tag` from file metadata.
225    fn try_from(m: std::fs::Metadata) -> std::io::Result<Self> {
226        Tag::try_from(&m)
227    }
228}
229
230impl From<u64> for Tag {
231    fn from(t: u64) -> Self {
232        Tag(t)
233    }
234}
235
236impl From<Tag> for u64 {
237    fn from(t: Tag) -> Self {
238        t.0
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    #[test]
247    fn special_names_is_sorted() {
248        let mut sn = SPECIAL_NAMES.to_vec();
249        sn.sort_unstable();
250        assert_eq!(sn, SPECIAL_NAMES);
251    }
252}