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