Skip to main content

wire_e2e_identity/pki_env/
mod.rs

1//! PKI Environment API
2
3mod crl;
4pub mod hooks;
5
6#[cfg(test)]
7mod dummy;
8
9use std::{collections::HashSet, sync::Arc};
10
11use async_lock::Mutex;
12use certval::{
13    CertSource, CertVector as _, CertificationPathSettings, Error as CertvalError, PathValidationStatus, TaSource,
14};
15use core_crypto_keystore::{
16    CryptoKeystoreError, Database,
17    entities::{E2eiAcmeCA, E2eiCrl, E2eiIntermediateCert},
18    traits::{FetchFromDatabase, UniqueEntity},
19};
20use openmls_traits::authentication_service::{CredentialAuthenticationStatus, CredentialRef};
21use x509_cert::{
22    Certificate,
23    anchor::TrustAnchorChoice,
24    der::{Decode as _, Encode as _},
25};
26
27use crate::{
28    pki_env::hooks::PkiEnvironmentHooks,
29    x509_check::{
30        RustyX509CheckError, RustyX509CheckResult, extract_crl_uris,
31        revocation::{PkiEnvironment as RjtPkiEnvironment, PkiEnvironmentParams, now},
32    },
33};
34
35pub type Result<T> = core::result::Result<T, Error>;
36
37#[derive(Debug, thiserror::Error)]
38pub enum Error {
39    #[error("The trust anchor certificate couldn't be loaded from the database.")]
40    NoTrustAnchor,
41    #[error("Failed to fetch CRL from '{uri}': HTTP {status}")]
42    CrlFetchUnsuccessful { uri: String, status: u16 },
43    #[error(transparent)]
44    HooksError(#[from] hooks::PkiEnvironmentHooksError),
45    #[error(transparent)]
46    X509Error(#[from] RustyX509CheckError),
47    #[error(transparent)]
48    UrlError(#[from] url::ParseError),
49    #[error(transparent)]
50    JsonError(#[from] serde_json::Error),
51    #[error(transparent)]
52    X509CertDerError(#[from] x509_cert::der::Error),
53    #[error(transparent)]
54    KeystoreError(#[from] core_crypto_keystore::CryptoKeystoreError),
55    #[error("certval error: {0}")]
56    Certval(certval::Error),
57}
58
59/// New Certificate Revocation List distribution points.
60#[derive(Debug, Clone, derive_more::From, derive_more::Into, derive_more::Deref, derive_more::DerefMut)]
61pub struct NewCrlDistributionPoints(Option<HashSet<String>>);
62
63impl From<NewCrlDistributionPoints> for Option<Vec<String>> {
64    fn from(mut dp: NewCrlDistributionPoints) -> Self {
65        dp.take().map(|d| d.into_iter().collect())
66    }
67}
68
69impl IntoIterator for NewCrlDistributionPoints {
70    type Item = String;
71
72    type IntoIter = std::collections::hash_set::IntoIter<String>;
73
74    fn into_iter(self) -> Self::IntoIter {
75        let items = self.0.unwrap_or_default();
76        items.into_iter()
77    }
78}
79
80async fn restore_pki_env(data_provider: &impl FetchFromDatabase) -> Result<RjtPkiEnvironment> {
81    let mut trust_roots = vec![];
82    if let Ok(Some(ta_raw)) = data_provider.get_unique::<E2eiAcmeCA>().await {
83        trust_roots.push(
84            x509_cert::Certificate::from_der(&ta_raw.content).map(x509_cert::anchor::TrustAnchorChoice::Certificate)?,
85        );
86    }
87
88    let intermediates = data_provider
89        .load_all::<E2eiIntermediateCert>()
90        .await?
91        .into_iter()
92        .map(|inter| x509_cert::Certificate::from_der(&inter.content))
93        .collect::<core::result::Result<Vec<_>, _>>()?;
94
95    let crls = data_provider
96        .load_all::<E2eiCrl>()
97        .await?
98        .into_iter()
99        .map(|crl| x509_cert::crl::CertificateList::from_der(&crl.content))
100        .collect::<core::result::Result<Vec<_>, _>>()?;
101
102    let params = PkiEnvironmentParams {
103        trust_roots: &trust_roots,
104        intermediates: &intermediates,
105        crls: &crls,
106    };
107
108    Ok(RjtPkiEnvironment::init(params)?)
109}
110
111/// The PKI environment which can be initialized independently from a CoreCrypto session.
112#[derive(Debug)]
113pub struct PkiEnvironment {
114    /// Implemented by the clients and used by us to make external calls during e2e flow
115    hooks: Arc<dyn PkiEnvironmentHooks>,
116    /// The database in which X509 Credentials are stored.
117    database: Arc<Database>,
118    rjt_pki_env: Mutex<RjtPkiEnvironment>,
119}
120
121impl PkiEnvironment {
122    /// Create a new PKI Environment
123    pub async fn new(hooks: Arc<dyn PkiEnvironmentHooks>, database: Arc<Database>) -> Result<PkiEnvironment> {
124        let rjt_pki_env = restore_pki_env(&*database).await?;
125        Ok(Self {
126            hooks,
127            database,
128            rjt_pki_env: Mutex::new(rjt_pki_env),
129        })
130    }
131
132    /// Return certificates that are used as trust anchors.
133    pub async fn get_trust_anchors(&self) -> Vec<Certificate> {
134        self.rjt_pki_env
135            .lock()
136            .await
137            .get_trust_anchors()
138            .iter()
139            .filter_map(|choice| match choice.decoded_ta {
140                TrustAnchorChoice::Certificate(ref cert) => Some(cert.clone()),
141                _ => None,
142            })
143            .collect()
144    }
145
146    /// Get the hooks.
147    pub fn hooks(&self) -> Arc<dyn PkiEnvironmentHooks> {
148        self.hooks.clone()
149    }
150
151    /// Get the database.
152    pub fn database(&self) -> &Database {
153        &self.database
154    }
155
156    /// Get an Arc to the database.
157    ///
158    /// In general [`Self::database`] is lighter-weight and should be preferred.
159    pub fn database_arc(&self) -> Arc<Database> {
160        self.database.clone()
161    }
162
163    /// Wrap an operation which requires a transaction.
164    ///
165    /// If a transaction does not already exist, creates one.
166    ///
167    /// After the operation finishes, if we created a transaction, then either
168    /// commit or rollback the operation according to the operation's success.
169    async fn transactionally<T, E>(&self, operation: impl AsyncFnOnce() -> std::result::Result<T, E>) -> Result<T>
170    where
171        E: Into<Error>,
172    {
173        let created_transaction = match self.database.try_new_immediate_transaction().await {
174            Ok(()) => true,
175            Err(CryptoKeystoreError::TransactionInProgress) => false,
176            Err(err) => return Err(err.into()),
177        };
178        let operation_outcome = operation().await;
179        if created_transaction {
180            if operation_outcome.is_ok() {
181                self.database.commit_transaction().await?;
182            } else {
183                self.database.rollback_transaction().await?;
184            }
185        }
186        operation_outcome.map_err(Into::into)
187    }
188
189    /// Adds the certificate as a trust anchor to the PKI environment.
190    ///
191    /// The certificate is saved to the database, and included in the PKI environment for
192    /// future validation.
193    ///
194    /// # Caution
195    ///
196    /// Adding a trust anchor will replace any existing trust anchor. This limitation
197    /// will be relaxed in the future.
198    pub async fn add_trust_anchor(&self, cert: Certificate) -> Result<()> {
199        // Validate it (expiration & signature only)
200        self.rjt_pki_env.lock().await.validate_trust_anchor_cert(&cert)?;
201
202        // Save cert's DER representation to the database
203        // TODO: make this work for multiple trust anchors, see WPB-25632
204        let cert_data = E2eiAcmeCA {
205            content: cert.to_der()?,
206        };
207
208        self.transactionally(async || self.database.save(cert_data).await)
209            .await?;
210
211        let mut trust_anchors = TaSource::new();
212        trust_anchors.push(certval::CertFile {
213            filename: "".to_string(),
214            bytes: cert.to_der()?,
215        });
216        trust_anchors.initialize().map_err(Error::Certval)?;
217        self.rjt_pki_env
218            .lock()
219            .await
220            .add_trust_anchor_source(Box::new(trust_anchors));
221        Ok(())
222    }
223
224    /// Remove the trust anchor with serial number `serial_number` from the PKI environment.
225    ///
226    /// Note that any certificates relying on the removed trust anchor may no longer
227    /// validate.
228    pub async fn remove_trust_anchor(&self, serial_number: &[u8]) -> Result<()> {
229        let mut guard = self.rjt_pki_env.lock().await;
230
231        let certs: Vec<_> = guard
232            .get_trust_anchors()
233            .iter()
234            .filter_map(|choice| match choice.decoded_ta {
235                TrustAnchorChoice::Certificate(ref cert)
236                    if cert.tbs_certificate.serial_number.as_bytes() != serial_number =>
237                {
238                    Some(cert.clone())
239                }
240                _ => None,
241            })
242            .collect();
243
244        guard.clear_trust_anchor_sources();
245
246        let mut trust_anchors = TaSource::new();
247        for cert in certs {
248            trust_anchors.push(certval::CertFile {
249                filename: "".to_string(),
250                bytes: cert.to_der()?,
251            });
252        }
253        trust_anchors.initialize().map_err(Error::Certval)?;
254        guard.add_trust_anchor_source(Box::new(trust_anchors));
255
256        // TODO: make this work for multiple trust anchors, see WPB-25632
257        self.transactionally(async || {
258            self.database
259                .remove::<E2eiAcmeCA>(&<E2eiAcmeCA as UniqueEntity>::KEY)
260                .await
261        })
262        .await?;
263
264        Ok(())
265    }
266
267    /// Adds the certificate to the PKI environment.
268    ///
269    /// The certificate is saved to the database, and included in the PKI environment for
270    /// future validation.
271    ///
272    /// CRL (Certificate Revocation List) distribution points are extracted from the certificate and
273    /// an attempt is made to fetch a CRL from each one.
274    pub async fn add_intermediate_cert(&self, cert: Certificate) -> Result<()> {
275        // Save cert's DER representation to the database
276        let (ski, aki) = RjtPkiEnvironment::extract_ski_aki_from_cert(&cert)?;
277        let ski_aki_pair = format!("{ski}:{}", aki.unwrap_or_default());
278        let cert_der = RjtPkiEnvironment::encode_cert_to_der(&cert)?;
279        let intermediate_cert = E2eiIntermediateCert {
280            content: cert_der,
281            ski_aki_pair,
282        };
283
284        self.transactionally(async || {
285            self.database.save(intermediate_cert).await?;
286
287            // Get CRL distribution points and CRLs
288            let dps: Vec<String> = extract_crl_uris(&cert)?.iter().flatten().cloned().collect();
289            let crls = self.fetch_crls(dps.iter().map(AsRef::as_ref)).await?;
290
291            // Save all CRLs to the database
292            for (distribution_point, crl) in &crls {
293                self.save_crl(distribution_point, crl).await?;
294            }
295
296            Result::Ok(())
297        })
298        .await?;
299
300        let mut cps = CertificationPathSettings::new();
301        certval::set_time_of_interest(&mut cps, now()?);
302        let mut cert_source = CertSource::new();
303        cert_source.push(certval::CertFile {
304            filename: "".to_string(),
305            bytes: cert.to_der()?,
306        });
307
308        let mut guard = self.rjt_pki_env.lock().await;
309        cert_source.initialize(&cps).map_err(Error::Certval)?;
310        cert_source.find_all_partial_paths(&guard, &cps);
311        guard.add_certificate_source(Box::new(cert_source));
312
313        Ok(())
314    }
315
316    /// Validate an end-entity X509 certificate.
317    ///
318    /// Performs validation of the provided certificate in the context
319    /// defined by the set of trust anchors and intermediate certificates
320    /// contained in this PKI environment. Revocation check is performed
321    /// and time of interest is set to the time of the call.
322    pub async fn validate_cert(&self, cert: &x509_cert::Certificate) -> RustyX509CheckResult<()> {
323        self.rjt_pki_env.lock().await.validate_cert_and_revocation(cert)
324    }
325
326    /// Validate an X509 credential.
327    ///
328    /// # Panics
329    ///
330    /// Panics if the provided credential is not of type X509.
331    pub async fn validate_credential<'a>(&'a self, credential: CredentialRef<'a>) -> CredentialAuthenticationStatus {
332        let CredentialRef::X509 { certificates } = credential else {
333            panic!("this function can only be called with an X509 credential");
334        };
335
336        let Some(cert) = certificates
337            .first()
338            .and_then(|cert_raw| x509_cert::Certificate::from_der(cert_raw).ok())
339        else {
340            return CredentialAuthenticationStatus::Invalid;
341        };
342
343        match self.rjt_pki_env.lock().await.validate_cert_and_revocation(&cert) {
344            Err(RustyX509CheckError::CertValError(CertvalError::PathValidation(
345                PathValidationStatus::CertificateRevoked
346                | PathValidationStatus::CertificateRevokedEndEntity
347                | PathValidationStatus::CertificateRevokedIntermediateCa,
348            ))) => {
349                // ? Revoked credentials are A-OK. They still degrade conversations though.
350                // TODO: update this after WPB-25524
351                CredentialAuthenticationStatus::Valid
352            }
353            Err(RustyX509CheckError::CertValError(CertvalError::PathValidation(
354                PathValidationStatus::InvalidNotAfterDate,
355            ))) => {
356                // ? Expired credentials are A-OK. They still degrade conversations though.
357                // TODO: update this after WPB-25524
358                CredentialAuthenticationStatus::Valid
359            }
360            Err(RustyX509CheckError::CertValError(CertvalError::PathValidation(_))) => {
361                CredentialAuthenticationStatus::Invalid
362            }
363            Err(_) => CredentialAuthenticationStatus::Unknown,
364            Ok(_) => CredentialAuthenticationStatus::Valid,
365        }
366    }
367}
368
369#[cfg(test)]
370mod tests {
371    use spki::der::DecodePem as _;
372
373    use super::*;
374
375    const EXAMPLE_CERT_PEM: &str = "
376-----BEGIN CERTIFICATE-----
377MIIBkzCCAUWgAwIBAgIUHFYIFRkm33GKIOb4xLeNtkjl3TIwBQYDK2VwMDcxFTAT
378BgNVBAMMDFRlc3QgUm9vdCBDQTERMA8GA1UECgwIVGVzdCBPcmcxCzAJBgNVBAYT
379AlVTMB4XDTI2MDUyODE1MzA0NFoXDTM2MDUyNTE1MzA0NFowNzEVMBMGA1UEAwwM
380VGVzdCBSb290IENBMREwDwYDVQQKDAhUZXN0IE9yZzELMAkGA1UEBhMCVVMwKjAF
381BgMrZXADIQDa0nMgIgBZeNM2ysNUVp80zwjZNqPJt7HYK3GX7GPp9aNjMGEwHQYD
382VR0OBBYEFHA0MmaaNGOTuBvdo3zzQoKFJ3p5MB8GA1UdIwQYMBaAFHA0MmaaNGOT
383uBvdo3zzQoKFJ3p5MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAUG
384AytlcANBAJffPzL50OWnmEBo9mGBQfPVzKRIfFc8EaXox1D5VF9cC1r8nRa0hUq+
385LOVS/gxNk618+PKA2bYq67MZQXCYGgk=
386-----END CERTIFICATE-----
387";
388
389    #[tokio::test]
390    async fn can_add_trust_anchor() {
391        let db = Database::open_in_memory().unwrap();
392        let pki_env = PkiEnvironment::with_dummy_hooks(db).await.unwrap();
393        let cert = x509_cert::Certificate::from_pem(EXAMPLE_CERT_PEM).unwrap();
394        assert!(pki_env.add_trust_anchor(cert).await.is_ok());
395    }
396
397    #[tokio::test]
398    async fn can_remove_trust_anchor() {
399        let db = Database::open_in_memory().unwrap();
400        let pki_env = PkiEnvironment::with_dummy_hooks(db).await.unwrap();
401        let cert = x509_cert::Certificate::from_pem(EXAMPLE_CERT_PEM).unwrap();
402        pki_env.add_trust_anchor(cert.clone()).await.unwrap();
403
404        let certs = pki_env.get_trust_anchors().await;
405        assert_eq!(certs.len(), 1);
406
407        pki_env
408            .remove_trust_anchor(certs[0].tbs_certificate.serial_number.as_bytes())
409            .await
410            .unwrap();
411        assert_eq!(pki_env.get_trust_anchors().await.len(), 0);
412    }
413
414    #[tokio::test]
415    async fn can_add_intermediate_cert() {
416        let db = Database::open_in_memory().unwrap();
417        let pki_env = PkiEnvironment::with_dummy_hooks(db).await.unwrap();
418        let cert = x509_cert::Certificate::from_pem(EXAMPLE_CERT_PEM).unwrap();
419        assert!(pki_env.add_intermediate_cert(cert).await.is_ok());
420    }
421}