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}