Skip to main content

wire_e2e_identity/acquisition/
identity.rs

1use rusty_jwt_tools::prelude::{ClientId, HashAlgorithm, QualifiedHandle};
2use x509_cert::der::Decode as _;
3
4use crate::{
5    acquisition::{error::CertificateError, thumbprint::try_compute_jwk_canonicalized_thumbprint},
6    pki_env::PkiEnvironment,
7    x509_check::IdentityStatus,
8};
9
10type Result<T> = std::result::Result<T, CertificateError>;
11
12#[derive(Debug, Clone)]
13pub struct WireIdentity {
14    pub client_id: String,
15    pub handle: QualifiedHandle,
16    pub display_name: String,
17    pub domain: String,
18    pub status: IdentityStatus,
19    pub thumbprint: String,
20    pub serial_number: String,
21    pub not_before: u64,
22    pub not_after: u64,
23}
24
25pub trait WireIdentityReader {
26    /// Verifies a proof of identity, may it be a x509 certificate (or a Verifiable Presentation (later)).
27    /// We do not verify anything else e.g. expiry, it is left to MLS implementation
28    #[allow(async_fn_in_trait)]
29    async fn extract_identity(&self, env: &PkiEnvironment, hash_alg: HashAlgorithm) -> Result<WireIdentity>;
30
31    /// returns the 'Not Before' claim which usually matches the creation timestamp
32    fn extract_created_at(&self) -> Result<u64>;
33
34    /// returns the 'Subject Public Key Info' claim
35    fn extract_public_key(&self) -> Result<Vec<u8>>;
36}
37
38impl WireIdentityReader for x509_cert::Certificate {
39    async fn extract_identity(&self, env: &PkiEnvironment, hash_alg: HashAlgorithm) -> Result<WireIdentity> {
40        let serial_number = hex::encode(self.tbs_certificate.serial_number.as_bytes());
41        let not_before = self.tbs_certificate.validity.not_before.to_unix_duration().as_secs();
42        let not_after = self.tbs_certificate.validity.not_after.to_unix_duration().as_secs();
43        let (client_id, handle) = try_extract_san(&self.tbs_certificate)?;
44        let (display_name, domain) = try_extract_subject(&self.tbs_certificate)?;
45        let status = IdentityStatus::from_cert(self, env).await;
46        let thumbprint = try_compute_jwk_canonicalized_thumbprint(&self.tbs_certificate, hash_alg)?;
47
48        Ok(WireIdentity {
49            client_id,
50            handle,
51            display_name,
52            domain,
53            status,
54            thumbprint,
55            serial_number,
56            not_before,
57            not_after,
58        })
59    }
60
61    fn extract_created_at(&self) -> Result<u64> {
62        Ok(self.tbs_certificate.validity.not_before.to_unix_duration().as_secs())
63    }
64
65    fn extract_public_key(&self) -> Result<Vec<u8>> {
66        Ok(self
67            .tbs_certificate
68            .subject_public_key_info
69            .subject_public_key
70            .raw_bytes()
71            .to_vec())
72    }
73}
74
75impl WireIdentityReader for &[u8] {
76    async fn extract_identity(&self, env: &PkiEnvironment, hash_alg: HashAlgorithm) -> Result<WireIdentity> {
77        x509_cert::Certificate::from_der(self)?
78            .extract_identity(env, hash_alg)
79            .await
80    }
81
82    fn extract_created_at(&self) -> Result<u64> {
83        x509_cert::Certificate::from_der(self)?.extract_created_at()
84    }
85
86    fn extract_public_key(&self) -> Result<Vec<u8>> {
87        x509_cert::Certificate::from_der(self)?.extract_public_key()
88    }
89}
90
91impl WireIdentityReader for Vec<u8> {
92    async fn extract_identity(&self, env: &PkiEnvironment, hash_alg: HashAlgorithm) -> Result<WireIdentity> {
93        self.as_slice().extract_identity(env, hash_alg).await
94    }
95
96    fn extract_created_at(&self) -> Result<u64> {
97        self.as_slice().extract_created_at()
98    }
99
100    fn extract_public_key(&self) -> Result<Vec<u8>> {
101        self.as_slice().extract_public_key()
102    }
103}
104
105fn try_extract_subject(cert: &x509_cert::TbsCertificate) -> Result<(String, String)> {
106    let mut display_name = None;
107    let mut domain = None;
108
109    let mut subjects = cert.subject.0.iter().flat_map(|n| n.0.iter());
110    subjects.try_for_each(|s| -> Result<()> {
111        match s.oid {
112            const_oid::db::rfc4519::ORGANIZATION_NAME => {
113                domain = Some(std::str::from_utf8(s.value.value())?);
114            }
115            const_oid::db::rfc4519::COMMON_NAME => {
116                display_name = Some(std::str::from_utf8(s.value.value())?);
117            }
118            _ => {}
119        }
120
121        Ok(())
122    })?;
123    let display_name = display_name.ok_or(CertificateError::MissingDisplayName)?.to_string();
124    let domain = domain.ok_or(CertificateError::MissingDomain)?.to_string();
125    Ok((display_name, domain))
126}
127
128/// extract Subject Alternative Name to pick client-id & display name
129fn try_extract_san(cert: &x509_cert::TbsCertificate) -> Result<(String, QualifiedHandle)> {
130    let extensions = cert.extensions.as_ref().ok_or(CertificateError::InvalidFormat)?;
131
132    let san = extensions
133        .iter()
134        .find_map(|e| {
135            (e.extn_id == const_oid::db::rfc5280::ID_CE_SUBJECT_ALT_NAME)
136                .then(|| x509_cert::ext::pkix::SubjectAltName::from_der(e.extn_value.as_bytes()))
137        })
138        .transpose()?
139        .ok_or(CertificateError::InvalidFormat)?;
140
141    let mut client_id = None;
142    let mut handle = None;
143    san.0
144        .iter()
145        .filter_map(|n| match n {
146            x509_cert::ext::pkix::name::GeneralName::UniformResourceIdentifier(ia5_str) => Some(ia5_str.as_str()),
147            _ => None,
148        })
149        .try_for_each(|name| -> Result<()> {
150            // since both ClientId & handle are in the SAN we first try to parse the element as
151            // a ClientId (since it's the most characterizable) and else fallback to a handle
152            if let Ok(cid) = ClientId::try_from_uri(name) {
153                client_id = Some(cid.to_qualified());
154            } else if let Ok(h) = name.parse::<QualifiedHandle>() {
155                handle = Some(h);
156            }
157            Ok(())
158        })?;
159
160    let client_id = client_id.ok_or(CertificateError::MissingClientId)?;
161    let handle = handle.ok_or(CertificateError::MissingHandle)?;
162    Ok((client_id, handle))
163}
164
165#[cfg(test)]
166mod tests {
167    use core_crypto_keystore::{ConnectionType, Database, DatabaseKey};
168    use rstest::rstest;
169    use wasm_bindgen_test::*;
170
171    use super::*;
172
173    wasm_bindgen_test_configure!(run_in_browser);
174
175    const CERT: &str = r#"-----BEGIN CERTIFICATE-----
176MIICGjCCAcCgAwIBAgIRAJaZdl+hZDl9qSSju5kmWNAwCgYIKoZIzj0EAwIwLjEN
177MAsGA1UEChMEd2lyZTEdMBsGA1UEAxMUd2lyZSBJbnRlcm1lZGlhdGUgQ0EwHhcN
178MjQwMTA1MTQ1MzAyWhcNMzQwMTAyMTQ1MzAyWjApMREwDwYDVQQKEwh3aXJlLmNv
179bTEUMBIGA1UEAxMLQWxpY2UgU21pdGgwKjAFBgMrZXADIQChy/GdWnVyNKWvsB+D
180BoxYb+qpVN9QIBXeYdmp1hobOqOB8jCB7zAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0l
181BAwwCgYIKwYBBQUHAwIwHQYDVR0OBBYEFOM5yRKA3dHSlYnjEzcuWoiMWm+TMB8G
182A1UdIwQYMBaAFBP7HtkE3WdbqzE6Ll4aIB2jFM2LMGkGA1UdEQRiMGCGIHdpcmVh
183cHA6Ly8lNDBhbGljZV93aXJlQHdpcmUuY29thjx3aXJlYXBwOi8vb2Jha2pQT0hR
184MkNrTmIwck9yTk0zQSUyMWJhNTRlOGFjZThiNGM5MGRAd2lyZS5jb20wHQYMKwYB
185BAGCpGTGKEABBA0wCwIBBgQEd2lyZQQAMAoGCCqGSM49BAMCA0gAMEUCIDRaadkt
186pPSLrZ+qy07VJOhE/ypOS6oDItpaq/HPxoTUAiEA7EKzmAFv+/zIEA7lAZjNJ+x4
187dHnOydGcC6TZ9zo0pIM=
188-----END CERTIFICATE-----"#;
189
190    const CERT_EXPIRED: &str = r#"-----BEGIN CERTIFICATE-----
191MIICGTCCAb+gAwIBAgIQb84UE+pSF517knYRMfo5ozAKBggqhkjOPQQDAjAuMQ0w
192CwYDVQQKEwR3aXJlMR0wGwYDVQQDExR3aXJlIEludGVybWVkaWF0ZSBDQTAeFw0y
193NDAxMDUxNDUxMjVaFw0yNDAxMDUxNDU0MjVaMCkxETAPBgNVBAoTCHdpcmUuY29t
194MRQwEgYDVQQDEwtBbGljZSBTbWl0aDAqMAUGAytlcAMhAAbao8C3jBq8DxniGYmO
195lq6W1tlkNeRMs8aQ3SvIKMR3o4HyMIHvMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUE
196DDAKBggrBgEFBQcDAjAdBgNVHQ4EFgQUwTyA2moMyOKoHgJ8Y+dJezNuO8gwHwYD
197VR0jBBgwFoAUC7y0skJjTvA8UA3bHr1JoAzOxqgwaQYDVR0RBGIwYIYgd2lyZWFw
198cDovLyU0MGFsaWNlX3dpcmVAd2lyZS5jb22GPHdpcmVhcHA6Ly9OQjNjVnJRZFNi
199Ni1Dd2tmQWljUnpnJTIxNWNkNGViYjFmNzU0ODA5ZUB3aXJlLmNvbTAdBgwrBgEE
200AYKkZMYoQAEEDTALAgEGBAR3aXJlBAAwCgYIKoZIzj0EAwIDSAAwRQIgfwfd5vXm
201EoOKgYLyKNa24aewZZObydD+k0hFs4iKddICIQDf70uv+h0tHw/WNf15mZ8NGkJm
202OfqfZA1YMtN5NLz/AA==
203-----END CERTIFICATE-----"#;
204
205    #[rstest::fixture]
206    async fn pki_env() -> PkiEnvironment {
207        let key = DatabaseKey::generate();
208        let db = Database::open(ConnectionType::InMemory, &key).await.unwrap();
209        PkiEnvironment::with_dummy_hooks(db).await.unwrap()
210    }
211
212    #[rstest]
213    #[tokio::test]
214    #[wasm_bindgen_test]
215    async fn should_find_claims_in_x509(#[future] pki_env: PkiEnvironment) {
216        let cert_der = pem::parse(CERT).unwrap();
217        let identity = cert_der
218            .contents()
219            .extract_identity(&pki_env.await, HashAlgorithm::SHA256)
220            .await
221            .unwrap();
222
223        assert_eq!(&identity.client_id, "obakjPOHQ2CkNb0rOrNM3A:ba54e8ace8b4c90d@wire.com");
224        assert_eq!(identity.handle.as_str(), "wireapp://%40alice_wire@wire.com");
225        assert_eq!(&identity.display_name, "Alice Smith");
226        assert_eq!(&identity.domain, "wire.com");
227        assert_eq!(&identity.serial_number, "009699765fa164397da924a3bb992658d0");
228        assert_eq!(identity.not_before, 1704466382);
229        assert_eq!(identity.not_after, 2019826382);
230    }
231
232    #[test]
233    #[wasm_bindgen_test]
234    fn should_find_created_at_claim() {
235        let cert_der = pem::parse(CERT).unwrap();
236        let created_at = cert_der.contents().extract_created_at().unwrap();
237        assert_eq!(created_at, 1704466382);
238    }
239
240    #[test]
241    #[wasm_bindgen_test]
242    fn should_find_public_key() {
243        let cert_der = pem::parse(CERT).unwrap();
244        let spki = cert_der.contents().extract_public_key().unwrap();
245        assert_eq!(
246            hex::encode(spki),
247            "a1cbf19d5a757234a5afb01f83068c586feaa954df502015de61d9a9d61a1b3a"
248        );
249    }
250
251    #[rstest]
252    #[tokio::test]
253    #[wasm_bindgen_test]
254    async fn should_have_revoked_status(#[future] pki_env: PkiEnvironment) {
255        let cert_der = pem::parse(CERT_EXPIRED).unwrap();
256        let identity = cert_der
257            .contents()
258            .extract_identity(&pki_env.await, HashAlgorithm::SHA256)
259            .await
260            .unwrap();
261        assert_eq!(&identity.status, &IdentityStatus::Expired);
262    }
263
264    #[rstest]
265    #[tokio::test]
266    #[wasm_bindgen_test]
267    async fn should_have_thumbprint(#[future] pki_env: PkiEnvironment) {
268        let cert_der = pem::parse(CERT).unwrap();
269        let identity = cert_der
270            .contents()
271            .extract_identity(&pki_env.await, HashAlgorithm::SHA256)
272            .await
273            .unwrap();
274        assert!(!identity.thumbprint.is_empty());
275    }
276}