core_crypto/mls/credential/
x509.rs

1#[cfg(test)]
2use std::collections::HashMap;
3use std::fmt;
4
5#[cfg(test)]
6use mls_crypto_provider::PkiKeypair;
7use openmls::prelude::Credential as MlsCredential;
8use openmls_traits::types::SignatureScheme;
9use openmls_x509_credential::CertificateKeyPair;
10use wire_e2e_identity::prelude::{HashAlgorithm, WireIdentityReader};
11#[cfg(test)]
12use x509_cert::der::Encode;
13use zeroize::Zeroize;
14
15use super::{Error, Result};
16#[cfg(test)]
17use crate::test_utils::x509::X509Certificate;
18use crate::{ClientId, Credential, CredentialType, MlsError, RecursiveError, e2e_identity::id::WireQualifiedClientId};
19
20#[derive(core_crypto_macros::Debug, Clone, Zeroize)]
21#[zeroize(drop)]
22pub struct CertificatePrivateKey {
23    #[sensitive]
24    pub(crate) value: Vec<u8>,
25    #[zeroize(skip)]
26    pub(crate) signature_scheme: SignatureScheme,
27}
28
29impl CertificatePrivateKey {
30    pub(crate) fn into_parts(mut self) -> (Vec<u8>, SignatureScheme) {
31        (std::mem::take(&mut self.value), self.signature_scheme)
32    }
33}
34
35/// Represents a x509 certificate chain supplied by the client
36/// It can fetch it after an end-to-end identity process where it can get back a certificate
37/// from the Authentication Service
38#[derive(Clone)]
39pub struct CertificateBundle {
40    /// x509 certificate chain
41    /// First entry is the leaf certificate and each subsequent is its issuer
42    pub certificate_chain: Vec<Vec<u8>>,
43    /// Leaf certificate private key
44    pub private_key: CertificatePrivateKey,
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    /// Reads the client_id from the leaf certificate
71    pub fn get_client_id(&self) -> Result<ClientId> {
72        let leaf = self.certificate_chain.first().ok_or(Error::InvalidIdentity)?;
73
74        let hash_alg = match self.private_key.signature_scheme {
75            SignatureScheme::ECDSA_SECP256R1_SHA256 | SignatureScheme::ED25519 => HashAlgorithm::SHA256,
76            SignatureScheme::ECDSA_SECP384R1_SHA384 => HashAlgorithm::SHA384,
77            SignatureScheme::ED448 | SignatureScheme::ECDSA_SECP521R1_SHA512 => HashAlgorithm::SHA512,
78        };
79
80        let identity = leaf
81            .extract_identity(None, hash_alg)
82            .map_err(|_| Error::InvalidIdentity)?;
83        let client_id = identity
84            .client_id
85            .parse::<WireQualifiedClientId>()
86            .map_err(RecursiveError::e2e_identity("parsing wire qualified client id"))?;
87        Ok(client_id.into())
88    }
89
90    /// Reads the 'Not Before' claim from the leaf certificate
91    pub fn get_created_at(&self) -> Result<u64> {
92        let leaf = self.certificate_chain.first().ok_or(Error::InvalidIdentity)?;
93        leaf.extract_created_at().map_err(|_| Error::InvalidIdentity)
94    }
95}
96
97impl Credential {
98    /// Create a new x509 credential from a certificate bundle.
99    pub fn x509(cert: CertificateBundle) -> Result<Self> {
100        let earliest_validity = cert.get_created_at().map_err(RecursiveError::mls_credential(
101            "getting credential 'not before' claim from leaf cert in Credential::x509",
102        ))?;
103        let (sk, ..) = cert.private_key.into_parts();
104        let chain = cert.certificate_chain;
105
106        let kp = CertificateKeyPair::new(sk, chain.clone()).map_err(MlsError::wrap("creating certificate key pair"))?;
107
108        let credential = MlsCredential::new_x509(chain).map_err(MlsError::wrap("creating x509 credential"))?;
109
110        let cb = Credential {
111            credential_type: CredentialType::X509,
112            mls_credential: credential,
113            signature_key_pair: kp.0,
114            earliest_validity,
115        };
116        Ok(cb)
117    }
118}
119
120#[cfg(test)]
121fn new_rand_client(domain: Option<String>) -> (String, String) {
122    let rand_str = |n: usize| {
123        use rand::distributions::{Alphanumeric, DistString as _};
124        Alphanumeric.sample_string(&mut rand::thread_rng(), n)
125    };
126    let user_id = uuid::Uuid::new_v4().to_string();
127    let domain = domain.unwrap_or_else(|| format!("{}.com", rand_str(6)));
128    let client_id = wire_e2e_identity::prelude::E2eiClientId::try_new(user_id, rand::random::<u64>(), &domain)
129        .unwrap()
130        .to_qualified();
131    (client_id, domain)
132}
133
134#[cfg(test)]
135impl CertificateBundle {
136    // test functions are not held to the same standard as real functions
137    #![allow(missing_docs)]
138
139    /// Generates a certificate that is later turned into a [Credential]
140    ///
141    /// `name` is not known to be a qualified e2ei client id so we invent a new one
142    pub fn rand(name: &ClientId, signer: &crate::test_utils::x509::X509Certificate) -> Self {
143        // here in our tests client_id is generally just "alice" or "bob"
144        // so we will use it to augment handle & display_name
145        // and not a real client_id, instead we'll generate a random one
146        let handle = format!("{name}_wire");
147        let display_name = format!("{name} Smith");
148        Self::new(&handle, &display_name, None, None, signer)
149    }
150
151    /// Generates a certificate that is later turned into a [Credential]
152    pub fn new(
153        handle: &str,
154        display_name: &str,
155        client_id: Option<&crate::e2e_identity::id::QualifiedE2eiClientId>,
156        cert_keypair: Option<PkiKeypair>,
157        signer: &crate::test_utils::x509::X509Certificate,
158    ) -> Self {
159        Self::new_with_expiration(handle, display_name, client_id, cert_keypair, signer, None)
160    }
161
162    pub fn new_with_expiration(
163        handle: &str,
164        display_name: &str,
165        client_id: Option<&crate::e2e_identity::id::QualifiedE2eiClientId>,
166        cert_keypair: Option<PkiKeypair>,
167        signer: &crate::test_utils::x509::X509Certificate,
168        expiration: Option<std::time::Duration>,
169    ) -> Self {
170        // here in our tests client_id is generally just "alice" or "bob"
171        // so we will use it to augment handle & display_name
172        // and not a real client_id, instead we'll generate a random one
173        let domain = "world.com";
174        let (client_id, domain) = client_id
175            .map(|cid| {
176                let cid = String::from_utf8(cid.to_vec()).unwrap();
177                (cid, domain.to_string())
178            })
179            .unwrap_or_else(|| new_rand_client(Some(domain.to_string())));
180
181        let mut cert_params = crate::test_utils::x509::CertificateParams {
182            domain: domain.into(),
183            common_name: Some(display_name.to_string()),
184            handle: Some(handle.to_string()),
185            client_id: Some(client_id.to_string()),
186            cert_keypair,
187            ..Default::default()
188        };
189
190        if let Some(expiration) = expiration {
191            cert_params.expiration = expiration;
192        }
193
194        let cert = signer.create_and_sign_end_identity(cert_params);
195        Self::from_certificate_and_issuer(&cert, signer)
196    }
197
198    pub fn new_with_default_values(
199        signer: &crate::test_utils::x509::X509Certificate,
200        expiration: Option<std::time::Duration>,
201    ) -> Self {
202        Self::new_with_expiration("alice_wire@world.com", "Alice Smith", None, None, signer, expiration)
203    }
204
205    pub fn from_self_signed_certificate(cert: &X509Certificate) -> Self {
206        Self::from_certificate_and_issuer(cert, cert)
207    }
208
209    pub fn from_certificate_and_issuer(cert: &X509Certificate, issuer: &X509Certificate) -> Self {
210        Self {
211            certificate_chain: vec![cert.certificate.to_der().unwrap(), issuer.certificate.to_der().unwrap()],
212            private_key: CertificatePrivateKey {
213                value: cert.pki_keypair.signing_key_bytes(),
214                signature_scheme: cert.signature_scheme,
215            },
216        }
217    }
218
219    pub fn rand_identifier_certs(
220        client_id: &ClientId,
221        signers: &[&crate::test_utils::x509::X509Certificate],
222    ) -> HashMap<SignatureScheme, CertificateBundle> {
223        signers
224            .iter()
225            .map(|signer| (signer.signature_scheme, Self::rand(client_id, signer)))
226            .collect()
227    }
228
229    pub fn rand_identifier(
230        client_id: &ClientId,
231        signers: &[&crate::test_utils::x509::X509Certificate],
232    ) -> crate::ClientIdentifier {
233        crate::ClientIdentifier::X509(Self::rand_identifier_certs(client_id, signers))
234    }
235}