deltachat/net/tls/
spki.rs

1//! SPKI hash storage.
2//!
3//! We store hashes of Subject Public Key Info from TLS certificates
4//! after successful connection to allow connecting when
5//! server certificate expires as long as the key is not changed.
6
7use std::collections::BTreeMap;
8
9use anyhow::Result;
10use base64::Engine as _;
11use parking_lot::RwLock;
12use sha2::{Digest, Sha256};
13use tokio_rustls::rustls::pki_types::SubjectPublicKeyInfoDer;
14
15use crate::sql::Sql;
16use crate::tools::time;
17
18/// Calculates Subject Public Key Info SHA-256 hash and returns it as base64.
19///
20/// This is the same format as used in <https://www.rfc-editor.org/rfc/rfc7469>.
21/// You can calculate the same hash for any remote host with
22/// ```sh
23/// openssl s_client -connect "$HOST:993" -servername "$HOST" </dev/null 2>/dev/null |
24/// openssl x509 -pubkey -noout |
25/// openssl pkey -pubin -outform der |
26/// openssl dgst -sha256 -binary |
27/// openssl enc -base64
28/// ```
29pub fn spki_hash(spki: &SubjectPublicKeyInfoDer) -> String {
30    let spki_hash = Sha256::digest(spki);
31    base64::engine::general_purpose::STANDARD.encode(spki_hash)
32}
33
34/// Write-through cache for SPKI hashes.
35#[derive(Debug)]
36pub struct SpkiHashStore {
37    /// Map from hostnames to base64 of SHA-256 hashes.
38    pub hash_store: RwLock<BTreeMap<String, String>>,
39}
40
41impl SpkiHashStore {
42    pub fn new() -> Self {
43        Self {
44            hash_store: RwLock::new(BTreeMap::new()),
45        }
46    }
47
48    /// Returns base64 of SPKI hash if we have previously successfully connected to given hostname.
49    pub async fn get_spki_hash(&self, hostname: &str, sql: &Sql) -> Result<Option<String>> {
50        if let Some(hash) = self.hash_store.read().get(hostname).cloned() {
51            return Ok(Some(hash));
52        }
53
54        match sql
55            .query_row_optional(
56                "SELECT spki_hash FROM tls_spki WHERE host=?",
57                (hostname,),
58                |row| {
59                    let spki_hash: String = row.get(0)?;
60                    Ok(spki_hash)
61                },
62            )
63            .await?
64        {
65            Some(hash) => {
66                self.hash_store
67                    .write()
68                    .insert(hostname.to_string(), hash.clone());
69                Ok(Some(hash))
70            }
71            None => Ok(None),
72        }
73    }
74
75    /// Saves SPKI hash after successful connection.
76    pub async fn save_spki(
77        &self,
78        hostname: &str,
79        spki: &SubjectPublicKeyInfoDer<'_>,
80        sql: &Sql,
81        timestamp: i64,
82    ) -> Result<()> {
83        let hash = spki_hash(spki);
84        self.hash_store
85            .write()
86            .insert(hostname.to_string(), hash.clone());
87        sql.execute(
88            "INSERT OR REPLACE INTO tls_spki (host, spki_hash, timestamp) VALUES (?, ?, ?)",
89            (hostname, hash, timestamp),
90        )
91        .await?;
92        Ok(())
93    }
94
95    /// Removes stale entries from SPKI storage.
96    pub async fn cleanup(&self, sql: &Sql) -> Result<()> {
97        let now = time();
98        let removed_hosts = sql
99            .transaction(|transaction| {
100                let mut stmt = transaction
101                    .prepare("DELETE FROM tls_spki WHERE ? > timestamp + ? RETURNING host")?;
102                let mut res = Vec::new();
103                for row in stmt.query_map((now, 30 * 24 * 60 * 60), |row| {
104                    let host: String = row.get(0)?;
105                    Ok(host)
106                })? {
107                    res.push(row?);
108                }
109
110                // Fix timestamps that happen to be in the future
111                // if we had clock set incorrectly when the timestamp was stored.
112                // Otherwise entry may take more than 30 days to expire.
113                transaction.execute(
114                    "UPDATE tls_spki SET timestamp = ?1 WHERE timestamp > ?1",
115                    (now,),
116                )?;
117
118                Ok(res)
119            })
120            .await?;
121
122        let mut lock = self.hash_store.write();
123        for host in removed_hosts {
124            // We may accidentally remove a host that was added
125            // to the cache after SQL query but before we got
126            // the write lock on `hash_store`.
127            // It is unlikely and will only result
128            // in additional SQL query next time.
129            lock.remove(&host);
130        }
131        Ok(())
132    }
133}