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