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    Database, Transaction,
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    /// Adds the certificate as a trust anchor to the PKI environment.
164    ///
165    /// The certificate is saved to the database, and included in the PKI environment for
166    /// future validation.
167    ///
168    /// # Caution
169    ///
170    /// Adding a trust anchor will replace any existing trust anchor. This limitation
171    /// will be relaxed in the future.
172    pub async fn add_trust_anchor(&self, tx: &Transaction, cert: Certificate) -> Result<()> {
173        // Validate it (expiration & signature only)
174        self.rjt_pki_env.lock().await.validate_trust_anchor_cert(&cert)?;
175
176        // Save cert's DER representation to the database
177        // TODO: make this work for multiple trust anchors, see WPB-25632
178        let cert_data = E2eiAcmeCA {
179            content: cert.to_der()?,
180        };
181
182        tx.save(cert_data).await?;
183
184        let mut trust_anchors = TaSource::new();
185        trust_anchors.push(certval::CertFile {
186            filename: "".to_string(),
187            bytes: cert.to_der()?,
188        });
189        trust_anchors.initialize().map_err(Error::Certval)?;
190        self.rjt_pki_env
191            .lock()
192            .await
193            .add_trust_anchor_source(Box::new(trust_anchors));
194        Ok(())
195    }
196
197    /// Remove the trust anchor with serial number `serial_number` from the PKI environment.
198    ///
199    /// Note that any certificates relying on the removed trust anchor may no longer
200    /// validate.
201    pub async fn remove_trust_anchor(&self, tx: &Transaction, serial_number: &[u8]) -> Result<()> {
202        let mut guard = self.rjt_pki_env.lock().await;
203
204        let certs: Vec<_> = guard
205            .get_trust_anchors()
206            .iter()
207            .filter_map(|choice| match choice.decoded_ta {
208                TrustAnchorChoice::Certificate(ref cert)
209                    if cert.tbs_certificate.serial_number.as_bytes() != serial_number =>
210                {
211                    Some(cert.clone())
212                }
213                _ => None,
214            })
215            .collect();
216
217        guard.clear_trust_anchor_sources();
218
219        let mut trust_anchors = TaSource::new();
220        for cert in certs {
221            trust_anchors.push(certval::CertFile {
222                filename: "".to_string(),
223                bytes: cert.to_der()?,
224            });
225        }
226        trust_anchors.initialize().map_err(Error::Certval)?;
227        guard.add_trust_anchor_source(Box::new(trust_anchors));
228
229        // TODO: make this work for multiple trust anchors, see WPB-25632
230        tx.remove::<E2eiAcmeCA>(&<E2eiAcmeCA as UniqueEntity>::KEY).await?;
231
232        Ok(())
233    }
234
235    /// Adds the certificate to the PKI environment.
236    ///
237    /// The certificate is saved to the database, and included in the PKI environment for
238    /// future validation.
239    ///
240    /// CRL (Certificate Revocation List) distribution points are extracted from the certificate and
241    /// an attempt is made to fetch a CRL from each one.
242    pub async fn add_intermediate_cert(&self, tx: &Transaction, cert: Certificate) -> Result<()> {
243        // Save cert's DER representation to the database
244        let (ski, aki) = RjtPkiEnvironment::extract_ski_aki_from_cert(&cert)?;
245        let ski_aki_pair = format!("{ski}:{}", aki.unwrap_or_default());
246        let cert_der = RjtPkiEnvironment::encode_cert_to_der(&cert)?;
247        let intermediate_cert = E2eiIntermediateCert {
248            content: cert_der,
249            ski_aki_pair,
250        };
251
252        tx.save(intermediate_cert).await?;
253
254        // Get CRL distribution points and CRLs
255        let dps: Vec<String> = extract_crl_uris(&cert)?.iter().flatten().cloned().collect();
256        let crls = self.fetch_crls(dps.iter().map(AsRef::as_ref)).await?;
257
258        // Save all CRLs to the database
259        for (distribution_point, crl) in &crls {
260            self.save_crl(tx, distribution_point, crl).await?;
261        }
262
263        let mut cps = CertificationPathSettings::new();
264        certval::set_time_of_interest(&mut cps, now()?);
265        let mut cert_source = CertSource::new();
266        cert_source.push(certval::CertFile {
267            filename: "".to_string(),
268            bytes: cert.to_der()?,
269        });
270
271        let mut guard = self.rjt_pki_env.lock().await;
272        cert_source.initialize(&cps).map_err(Error::Certval)?;
273        cert_source.find_all_partial_paths(&guard, &cps);
274        guard.add_certificate_source(Box::new(cert_source));
275
276        Ok(())
277    }
278
279    /// Validate an end-entity X509 certificate.
280    ///
281    /// Performs validation of the provided certificate in the context
282    /// defined by the set of trust anchors and intermediate certificates
283    /// contained in this PKI environment. Revocation check is performed
284    /// and time of interest is set to the time of the call.
285    pub async fn validate_cert(&self, cert: &x509_cert::Certificate) -> RustyX509CheckResult<()> {
286        self.rjt_pki_env.lock().await.validate_cert_and_revocation(cert)
287    }
288
289    /// Validate an X509 credential.
290    ///
291    /// # Panics
292    ///
293    /// Panics if the provided credential is not of type X509.
294    pub async fn validate_credential<'a>(&'a self, credential: CredentialRef<'a>) -> CredentialAuthenticationStatus {
295        let CredentialRef::X509 { certificates } = credential else {
296            panic!("this function can only be called with an X509 credential");
297        };
298
299        let Some(cert) = certificates
300            .first()
301            .and_then(|cert_raw| x509_cert::Certificate::from_der(cert_raw).ok())
302        else {
303            return CredentialAuthenticationStatus::Invalid;
304        };
305
306        match self.rjt_pki_env.lock().await.validate_cert_and_revocation(&cert) {
307            Err(RustyX509CheckError::CertValError(CertvalError::PathValidation(
308                PathValidationStatus::CertificateRevoked
309                | PathValidationStatus::CertificateRevokedEndEntity
310                | PathValidationStatus::CertificateRevokedIntermediateCa,
311            ))) => {
312                // ? Revoked credentials are A-OK. They still degrade conversations though.
313                // TODO: update this after WPB-25524
314                CredentialAuthenticationStatus::Valid
315            }
316            Err(RustyX509CheckError::CertValError(CertvalError::PathValidation(
317                PathValidationStatus::InvalidNotAfterDate,
318            ))) => {
319                // ? Expired credentials are A-OK. They still degrade conversations though.
320                // TODO: update this after WPB-25524
321                CredentialAuthenticationStatus::Valid
322            }
323            Err(RustyX509CheckError::CertValError(CertvalError::PathValidation(_))) => {
324                CredentialAuthenticationStatus::Invalid
325            }
326            Err(_) => CredentialAuthenticationStatus::Unknown,
327            Ok(_) => CredentialAuthenticationStatus::Valid,
328        }
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use spki::der::DecodePem as _;
335
336    use super::*;
337
338    const EXAMPLE_CERT_PEM: &str = "
339-----BEGIN CERTIFICATE-----
340MIIBkzCCAUWgAwIBAgIUHFYIFRkm33GKIOb4xLeNtkjl3TIwBQYDK2VwMDcxFTAT
341BgNVBAMMDFRlc3QgUm9vdCBDQTERMA8GA1UECgwIVGVzdCBPcmcxCzAJBgNVBAYT
342AlVTMB4XDTI2MDUyODE1MzA0NFoXDTM2MDUyNTE1MzA0NFowNzEVMBMGA1UEAwwM
343VGVzdCBSb290IENBMREwDwYDVQQKDAhUZXN0IE9yZzELMAkGA1UEBhMCVVMwKjAF
344BgMrZXADIQDa0nMgIgBZeNM2ysNUVp80zwjZNqPJt7HYK3GX7GPp9aNjMGEwHQYD
345VR0OBBYEFHA0MmaaNGOTuBvdo3zzQoKFJ3p5MB8GA1UdIwQYMBaAFHA0MmaaNGOT
346uBvdo3zzQoKFJ3p5MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAUG
347AytlcANBAJffPzL50OWnmEBo9mGBQfPVzKRIfFc8EaXox1D5VF9cC1r8nRa0hUq+
348LOVS/gxNk618+PKA2bYq67MZQXCYGgk=
349-----END CERTIFICATE-----
350";
351
352    #[tokio::test]
353    async fn can_add_trust_anchor() {
354        let db = Database::open_in_memory().unwrap();
355        let tx = db.new_transaction().await.unwrap();
356        let pki_env = PkiEnvironment::with_dummy_hooks(db).await.unwrap();
357        let cert = x509_cert::Certificate::from_pem(EXAMPLE_CERT_PEM).unwrap();
358        assert!(pki_env.add_trust_anchor(&tx, cert).await.is_ok());
359    }
360
361    #[tokio::test]
362    async fn can_remove_trust_anchor() {
363        let db = Database::open_in_memory().unwrap();
364        let tx = db.new_transaction().await.unwrap();
365        let pki_env = PkiEnvironment::with_dummy_hooks(db).await.unwrap();
366        let cert = x509_cert::Certificate::from_pem(EXAMPLE_CERT_PEM).unwrap();
367        pki_env.add_trust_anchor(&tx, cert.clone()).await.unwrap();
368
369        let certs = pki_env.get_trust_anchors().await;
370        assert_eq!(certs.len(), 1);
371
372        pki_env
373            .remove_trust_anchor(&tx, certs[0].tbs_certificate.serial_number.as_bytes())
374            .await
375            .unwrap();
376        assert_eq!(pki_env.get_trust_anchors().await.len(), 0);
377    }
378
379    #[tokio::test]
380    async fn can_add_intermediate_cert() {
381        let db = Database::open_in_memory().unwrap();
382        let tx = db.new_transaction().await.unwrap();
383        let pki_env = PkiEnvironment::with_dummy_hooks(db).await.unwrap();
384        let cert = x509_cert::Certificate::from_pem(EXAMPLE_CERT_PEM).unwrap();
385        assert!(pki_env.add_intermediate_cert(&tx, cert).await.is_ok());
386    }
387}