Skip to main content

core_crypto/mls/credential/
x509.rs

1use std::fmt;
2
3use derive_more::derive;
4use openmls::prelude::Credential as MlsCredential;
5use openmls_traits::types::SignatureScheme;
6use openmls_x509_credential::CertificateKeyPair;
7use wire_e2e_identity::{
8    HashAlgorithm, WireIdentityReader, legacy::id::WireQualifiedClientId, pki_env::PkiEnvironment,
9};
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::{
20    CipherSuite, ClientId, Credential, CredentialType, OpenMlsError, RecursiveError,
21    mls::credential::ext::CredentialExt as _,
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    /// Create a certificate bundle from a DER-encoded certificate chain and raw private key bytes.
75    pub fn from_raw(certificate_chain: Vec<Vec<u8>>, private_key: Vec<u8>, signature_scheme: SignatureScheme) -> Self {
76        Self {
77            certificate_chain,
78            private_key: CertificatePrivateKey::new(private_key),
79            signature_scheme,
80        }
81    }
82
83    /// Reads the client_id from the leaf certificate
84    pub async fn get_client_id(&self, pki_env: &PkiEnvironment) -> Result<ClientId> {
85        let leaf = self.certificate_chain.first().ok_or(Error::InvalidIdentity)?;
86
87        let hash_alg = match self.signature_scheme {
88            SignatureScheme::ECDSA_SECP256R1_SHA256 | SignatureScheme::ED25519 => HashAlgorithm::SHA256,
89            SignatureScheme::ECDSA_SECP384R1_SHA384 => HashAlgorithm::SHA384,
90            SignatureScheme::ED448 | SignatureScheme::ECDSA_SECP521R1_SHA512 => HashAlgorithm::SHA512,
91        };
92
93        let identity = leaf
94            .extract_identity(pki_env, hash_alg)
95            .await
96            .map_err(|_| Error::InvalidIdentity)?;
97
98        use wire_e2e_identity::legacy::id as legacy_id;
99
100        let client_id: legacy_id::ClientId = identity
101            .client_id
102            .parse::<WireQualifiedClientId>()
103            .map_err(RecursiveError::e2e_identity("parsing wire qualified client id"))?
104            .into();
105        let client_id: Vec<u8> = client_id.into();
106        let client_id =
107            ClientId::new_from_bytes(client_id).map_err(RecursiveError::mls_client("client id from bytes"))?;
108        Ok(client_id)
109    }
110
111    /// Reads the 'Not Before' claim from the leaf certificate
112    pub fn get_created_at(&self) -> Result<u64> {
113        let leaf = self.certificate_chain.first().ok_or(Error::InvalidIdentity)?;
114        leaf.extract_created_at().map_err(|_| Error::InvalidIdentity)
115    }
116}
117
118impl Credential {
119    /// Create a new x509 credential from a certificate bundle.
120    pub fn x509(cipher_suite: CipherSuite, cert: CertificateBundle) -> Result<Self> {
121        let earliest_validity = cert.get_created_at().map_err(RecursiveError::mls_credential(
122            "getting credential 'not before' claim from leaf cert in Credential::x509",
123        ))?;
124        let sk = cert.private_key.into_inner();
125        let chain = cert.certificate_chain;
126
127        let kp =
128            CertificateKeyPair::new(sk, chain.clone()).map_err(OpenMlsError::wrap("creating certificate key pair"))?;
129
130        let credential = MlsCredential::new_x509(chain).map_err(OpenMlsError::wrap("creating x509 credential"))?;
131
132        let cb = Credential {
133            cipher_suite,
134            credential_type: CredentialType::X509,
135            mls_credential: credential,
136            signature_key_pair: kp.0,
137            earliest_validity,
138        };
139        Ok(cb)
140    }
141
142    /// Check an X509 Credential for expiration or revocation
143    ///
144    /// For now we only care about X509 credentials which require the pki_env as an external resource.
145    /// As soon as we have validation logic for other types the pki_env parameter becomes very smelly.
146    /// Other credential types will just return.
147    pub(crate) async fn check(&self, pki_env: &PkiEnvironment) -> Result<()> {
148        if self.credential_type == CredentialType::X509 {
149            let cert = self
150                .mls_credential()
151                .parse_leaf_cert()
152                .map_err(RecursiveError::mls_credential("parsing leaf certificate"))?
153                // This can actually never happen and points to a type issue with credentials
154                .expect("parse_leaf_cert to return a Certificate");
155
156            pki_env
157                .validate_cert(&cert)
158                .await
159                .map_err(RecursiveError::e2e_identity("validating credential certificate"))?;
160        }
161        Ok(())
162    }
163}
164
165#[cfg(test)]
166fn new_rand_client(domain: Option<String>) -> (ClientId, String) {
167    let rand_str = |n: usize| {
168        use rand::distributions::{Alphanumeric, DistString as _};
169        Alphanumeric.sample_string(&mut rand::thread_rng(), n)
170    };
171    let user_id = uuid::Uuid::new_v4();
172    let domain = domain.unwrap_or_else(|| format!("{}.com", rand_str(6)));
173    let device_id = rand::random::<u64>();
174    let client_id = ClientId::new(user_id, device_id, &domain);
175    (client_id, domain)
176}
177
178#[cfg(test)]
179impl CertificateBundle {
180    // test functions are not held to the same standard as real functions
181    #![allow(missing_docs)]
182
183    /// Generates a certificate that is later turned into a [Credential]
184    ///
185    /// `name` is not known to be a qualified e2ei client id so we invent a new one
186    pub fn rand(name: &ClientId, signer: &crate::test_utils::x509::X509Certificate) -> Self {
187        // here in our tests client_id is generally just "alice" or "bob"
188        // so we will use it to augment handle & display_name
189        // and not a real client_id, instead we'll generate a random one
190        let handle = format!("{name}_wire");
191        let display_name = format!("{name} Smith");
192        Self::new(&handle, &display_name, None, None, signer)
193    }
194
195    pub fn new_with_exact_client_id(client_id: &ClientId, signer: &crate::test_utils::x509::X509Certificate) -> Self {
196        // Unlike Self::rand() above, this uses the provided client ID and does not generate a new
197        // one.
198        // TODO: this should all be reworked by the time WPB-19540 is done.
199        let rand_str = |n: usize| {
200            use rand::distributions::{Alphanumeric, DistString as _};
201            Alphanumeric.sample_string(&mut rand::thread_rng(), n)
202        };
203        let name = rand_str(10);
204        let handle = format!("{name}_wire");
205        let display_name = format!("{name} Smith");
206        Self::new(&handle, &display_name, Some(client_id), None, signer)
207    }
208
209    /// Generates a certificate that is later turned into a [Credential]
210    pub fn new(
211        handle: &str,
212        display_name: &str,
213        client_id: Option<&ClientId>,
214        cert_keypair: Option<PkiKeypair>,
215        signer: &crate::test_utils::x509::X509Certificate,
216    ) -> Self {
217        Self::new_with_expiration(handle, display_name, client_id, cert_keypair, signer, None)
218    }
219
220    pub fn new_with_expiration(
221        handle: &str,
222        display_name: &str,
223        client_id: Option<&ClientId>,
224        cert_keypair: Option<PkiKeypair>,
225        signer: &crate::test_utils::x509::X509Certificate,
226        expiration: Option<std::time::Duration>,
227    ) -> Self {
228        // here in our tests client_id is generally just "alice" or "bob"
229        // so we will use it to augment handle & display_name
230        // and not a real client_id, instead we'll generate a random one
231        let domain = "world.com";
232        let (client_id, domain) = client_id
233            .cloned()
234            .map(|cid| (cid, domain.to_string()))
235            .unwrap_or_else(|| new_rand_client(Some(domain.to_string())));
236
237        let mut cert_params = crate::test_utils::x509::CertificateParams {
238            domain: domain.into(),
239            common_name: Some(display_name.to_string()),
240            handle: Some(handle.to_string()),
241            client_id: Some(client_id),
242            cert_keypair,
243            ..Default::default()
244        };
245
246        if let Some(expiration) = expiration {
247            cert_params.expiration = expiration;
248        }
249
250        let cert = signer.create_and_sign_end_identity(cert_params);
251        Self::from_certificate_and_issuer(&cert, signer)
252    }
253
254    pub fn new_with_default_values(
255        signer: &crate::test_utils::x509::X509Certificate,
256        expiration: Option<std::time::Duration>,
257    ) -> Self {
258        Self::new_with_expiration("alice_wire@world.com", "Alice Smith", None, None, signer, expiration)
259    }
260
261    pub fn from_self_signed_certificate(cert: &X509Certificate) -> Self {
262        Self::from_certificate_and_issuer(cert, cert)
263    }
264
265    pub fn from_certificate_and_issuer(cert: &X509Certificate, issuer: &X509Certificate) -> Self {
266        Self {
267            certificate_chain: vec![cert.certificate.to_der().unwrap(), issuer.certificate.to_der().unwrap()],
268            private_key: CertificatePrivateKey::new(cert.pki_keypair.signing_key_bytes()),
269            signature_scheme: cert.signature_scheme,
270        }
271    }
272}