Skip to main content

core_crypto/mls/credential/
x509.rs

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