Skip to main content

core_crypto/mls/credential/
x509.rs

1use std::fmt;
2
3use derive_more::derive;
4use openmls::prelude::Credential as MlsCredential;
5use openmls_traits::types::SignatureScheme;
6use openmls_x509_credential::CertificateKeyPair;
7use wire_e2e_identity::{WireIdentityReader, pki_env::PkiEnvironment};
8#[cfg(test)]
9use x509_cert::der::Encode;
10use zeroize::Zeroize;
11
12use super::{Error, Result};
13#[cfg(test)]
14use crate::test_utils::x509::{PkiKeypair, X509Certificate};
15use crate::{
16    CipherSuite, ClientId, Credential, CredentialType, OpenMlsError, RecursiveError,
17    mls::credential::ext::CredentialExt as _,
18};
19
20#[derive(core_crypto_macros::Debug, Clone, Zeroize, derive::Constructor)]
21#[zeroize(drop)]
22pub struct CertificatePrivateKey {
23    #[sensitive]
24    value: Vec<u8>,
25}
26
27impl CertificatePrivateKey {
28    pub(crate) fn into_inner(mut self) -> Vec<u8> {
29        std::mem::take(&mut self.value)
30    }
31}
32
33/// Represents a x509 certificate chain supplied by the client
34/// It can fetch it after an end-to-end identity process where it can get back a certificate
35/// from the Authentication Service
36#[derive(Clone)]
37pub struct CertificateBundle {
38    /// x509 certificate chain
39    /// First entry is the leaf certificate and each subsequent is its issuer
40    pub certificate_chain: Vec<Vec<u8>>,
41    /// Leaf certificate private key
42    pub private_key: CertificatePrivateKey,
43    /// Signature scheme of private key
44    pub signature_scheme: SignatureScheme,
45}
46
47impl fmt::Debug for CertificateBundle {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        use base64::prelude::*;
50
51        #[derive(derive_more::Debug)]
52        #[debug("{}", BASE64_STANDARD.encode(_0))]
53        // this only exists for the debug impl, which is ignored by the dead code check
54        #[expect(dead_code)]
55        struct CertificateDebugHelper<'a>(&'a Vec<u8>);
56
57        let certificates = self
58            .certificate_chain
59            .iter()
60            .map(CertificateDebugHelper)
61            .collect::<Vec<_>>();
62        f.debug_struct("CertificateBundle")
63            .field("certificate_chain", &certificates)
64            .field("private_key", &self.private_key)
65            .finish()
66    }
67}
68
69impl CertificateBundle {
70    /// Create a certificate bundle from a DER-encoded certificate chain and raw private key bytes.
71    pub fn from_raw(certificate_chain: Vec<Vec<u8>>, private_key: Vec<u8>, signature_scheme: SignatureScheme) -> Self {
72        Self {
73            certificate_chain,
74            private_key: CertificatePrivateKey::new(private_key),
75            signature_scheme,
76        }
77    }
78
79    /// Reads the client_id from the leaf certificate
80    pub async fn get_client_id(&self, pki_env: &PkiEnvironment) -> Result<ClientId> {
81        let leaf = self.certificate_chain.first().ok_or(Error::InvalidIdentity)?;
82
83        let identity = leaf
84            .extract_identity(pki_env)
85            .await
86            .map_err(|_| Error::InvalidIdentity)?;
87
88        ClientId::try_from_str_with_base64_user_id(&identity.client_id)
89            .map_err(RecursiveError::context(
90                "converting client id types from leaf certificate",
91            ))
92            .map_err(Into::into)
93    }
94
95    /// Reads the 'Not Before' claim from the leaf certificate
96    pub fn get_created_at(&self) -> Result<u64> {
97        let leaf = self.certificate_chain.first().ok_or(Error::InvalidIdentity)?;
98        leaf.extract_created_at().map_err(|_| Error::InvalidIdentity)
99    }
100}
101
102impl Credential {
103    /// Create a new x509 credential from a certificate bundle.
104    pub fn x509(cipher_suite: CipherSuite, cert: CertificateBundle) -> Result<Self> {
105        let earliest_validity = cert.get_created_at().map_err(RecursiveError::context(
106            "getting credential 'not before' claim from leaf cert in Credential::x509",
107        ))?;
108        let sk = cert.private_key.into_inner();
109        let chain = cert.certificate_chain;
110
111        let kp =
112            CertificateKeyPair::new(sk, chain.clone()).map_err(OpenMlsError::wrap("creating certificate key pair"))?;
113
114        let credential = MlsCredential::new_x509(chain).map_err(OpenMlsError::wrap("creating x509 credential"))?;
115
116        let cb = Credential {
117            cipher_suite,
118            credential_type: CredentialType::X509,
119            mls_credential: credential,
120            signature_key_pair: kp.0,
121            earliest_validity,
122        };
123        Ok(cb)
124    }
125
126    /// Check an X509 Credential for expiration or revocation
127    ///
128    /// For now we only care about X509 credentials which require the pki_env as an external resource.
129    /// As soon as we have validation logic for other types the pki_env parameter becomes very smelly.
130    /// Other credential types will just return.
131    pub(crate) async fn check(&self, pki_env: &PkiEnvironment) -> Result<()> {
132        if self.credential_type == CredentialType::X509 {
133            let cert = self
134                .mls_credential()
135                .parse_leaf_cert()
136                .map_err(RecursiveError::context("parsing leaf certificate"))?
137                // This can actually never happen and points to a type issue with credentials
138                .expect("parse_leaf_cert to return a Certificate");
139
140            pki_env
141                .validate_cert(&cert)
142                .await
143                .map_err(RecursiveError::e2e_identity("validating credential certificate"))?;
144        }
145        Ok(())
146    }
147}
148
149#[cfg(test)]
150fn new_rand_client(domain: Option<String>) -> (ClientId, String) {
151    let rand_str = |n: usize| {
152        use rand::distr::{Alphanumeric, SampleString as _};
153        Alphanumeric.sample_string(&mut rand::rng(), n)
154    };
155    let user_id = uuid::Uuid::new_v4();
156    let domain = domain.unwrap_or_else(|| format!("{}.com", rand_str(6)));
157    let device_id = rand::random::<u64>();
158    let client_id = ClientId::new(user_id, device_id, &domain);
159    (client_id, domain)
160}
161
162#[cfg(test)]
163impl CertificateBundle {
164    // test functions are not held to the same standard as real functions
165    #![expect(missing_docs)]
166
167    /// Generates a certificate that is later turned into a [Credential]
168    ///
169    /// `name` is not known to be a qualified e2ei client id so we invent a new one
170    pub fn rand(name: &ClientId, signer: &crate::test_utils::x509::X509Certificate) -> Self {
171        // here in our tests client_id is generally just "alice" or "bob"
172        // so we will use it to augment handle & display_name
173        // and not a real client_id, instead we'll generate a random one
174        let handle = format!("{name}_wire");
175        let display_name = format!("{name} Smith");
176        Self::new(&handle, &display_name, None, None, signer)
177    }
178
179    pub fn new_with_exact_client_id(client_id: &ClientId, signer: &crate::test_utils::x509::X509Certificate) -> Self {
180        // Unlike Self::rand() above, this uses the provided client ID and does not generate a new
181        // one.
182        // TODO: this should all be reworked by the time WPB-19540 is done.
183        let rand_str = |n: usize| {
184            use rand::distr::{Alphanumeric, SampleString as _};
185            Alphanumeric.sample_string(&mut rand::rng(), n)
186        };
187        let name = rand_str(10);
188        let handle = format!("{name}_wire");
189        let display_name = format!("{name} Smith");
190        Self::new(&handle, &display_name, Some(client_id), None, signer)
191    }
192
193    /// Generates a certificate that is later turned into a [Credential]
194    pub fn new(
195        handle: &str,
196        display_name: &str,
197        client_id: Option<&ClientId>,
198        cert_keypair: Option<PkiKeypair>,
199        signer: &crate::test_utils::x509::X509Certificate,
200    ) -> Self {
201        Self::new_with_expiration(handle, display_name, client_id, cert_keypair, signer, None)
202    }
203
204    pub fn new_with_expiration(
205        handle: &str,
206        display_name: &str,
207        client_id: Option<&ClientId>,
208        cert_keypair: Option<PkiKeypair>,
209        signer: &crate::test_utils::x509::X509Certificate,
210        expiration: Option<std::time::Duration>,
211    ) -> Self {
212        // here in our tests client_id is generally just "alice" or "bob"
213        // so we will use it to augment handle & display_name
214        // and not a real client_id, instead we'll generate a random one
215        let domain = "wire.com";
216        let (client_id, domain) = client_id
217            .cloned()
218            .map(|cid| (cid, domain.to_string()))
219            .unwrap_or_else(|| new_rand_client(Some(domain.to_string())));
220
221        let mut cert_params = crate::test_utils::x509::CertificateParams {
222            domain: domain.into(),
223            common_name: Some(display_name.to_string()),
224            handle: Some(handle.to_string()),
225            client_id: Some(client_id),
226            cert_keypair,
227            ..Default::default()
228        };
229
230        if let Some(expiration) = expiration {
231            cert_params.expiration = expiration;
232        }
233
234        let cert = signer.create_and_sign_end_identity(cert_params);
235        Self::from_certificate_and_issuer(&cert, signer)
236    }
237
238    pub fn new_with_default_values(
239        signer: &crate::test_utils::x509::X509Certificate,
240        expiration: Option<std::time::Duration>,
241    ) -> Self {
242        Self::new_with_expiration("alice_wire@wire.com", "Alice Smith", None, None, signer, expiration)
243    }
244
245    pub fn from_self_signed_certificate(cert: &X509Certificate) -> Self {
246        Self::from_certificate_and_issuer(cert, cert)
247    }
248
249    pub fn from_certificate_and_issuer(cert: &X509Certificate, issuer: &X509Certificate) -> Self {
250        Self {
251            certificate_chain: vec![cert.certificate.to_der().unwrap(), issuer.certificate.to_der().unwrap()],
252            private_key: CertificatePrivateKey::new(cert.pki_keypair.signing_key_bytes()),
253            signature_scheme: cert.signature_scheme,
254        }
255    }
256}