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