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