core_crypto/mls/credential/
x509.rs

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