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    validation::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 validity = self.tbs_certificate().validity();
41        let serial_number = hex::encode(self.tbs_certificate().serial_number().as_bytes());
42        let not_before = validity.not_before.to_unix_duration().as_secs();
43        let not_after = validity.not_after.to_unix_duration().as_secs();
44        let (client_id, handle) = try_extract_san(self.tbs_certificate())?;
45        let (display_name, domain) = try_extract_subject(self.tbs_certificate())?;
46        let status = IdentityStatus::from_cert(self, env).await;
47        let thumbprint = try_compute_jwk_canonicalized_thumbprint(self.tbs_certificate(), hash_alg)?;
48
49        Ok(WireIdentity {
50            client_id,
51            handle,
52            display_name,
53            domain,
54            status,
55            thumbprint,
56            serial_number,
57            not_before,
58            not_after,
59        })
60    }
61
62    fn extract_created_at(&self) -> Result<u64> {
63        Ok(self
64            .tbs_certificate()
65            .validity()
66            .not_before
67            .to_unix_duration()
68            .as_secs())
69    }
70
71    fn extract_public_key(&self) -> Result<Vec<u8>> {
72        Ok(self
73            .tbs_certificate()
74            .subject_public_key_info()
75            .subject_public_key
76            .raw_bytes()
77            .to_vec())
78    }
79}
80
81impl WireIdentityReader for &[u8] {
82    async fn extract_identity(&self, env: &PkiEnvironment, hash_alg: HashAlgorithm) -> Result<WireIdentity> {
83        x509_cert::Certificate::from_der(self)?
84            .extract_identity(env, hash_alg)
85            .await
86    }
87
88    fn extract_created_at(&self) -> Result<u64> {
89        x509_cert::Certificate::from_der(self)?.extract_created_at()
90    }
91
92    fn extract_public_key(&self) -> Result<Vec<u8>> {
93        x509_cert::Certificate::from_der(self)?.extract_public_key()
94    }
95}
96
97impl WireIdentityReader for Vec<u8> {
98    async fn extract_identity(&self, env: &PkiEnvironment, hash_alg: HashAlgorithm) -> Result<WireIdentity> {
99        self.as_slice().extract_identity(env, hash_alg).await
100    }
101
102    fn extract_created_at(&self) -> Result<u64> {
103        self.as_slice().extract_created_at()
104    }
105
106    fn extract_public_key(&self) -> Result<Vec<u8>> {
107        self.as_slice().extract_public_key()
108    }
109}
110
111fn try_extract_subject(cert: &x509_cert::TbsCertificate) -> Result<(String, String)> {
112    let display_name = cert
113        .subject()
114        .common_name()?
115        .ok_or(CertificateError::MissingDisplayName)?
116        .into();
117    let domain = cert
118        .subject()
119        .organization()?
120        .ok_or(CertificateError::MissingDomain)?
121        .into();
122    Ok((display_name, domain))
123}
124
125/// extract Subject Alternative Name to pick client-id & display name
126fn try_extract_san(cert: &x509_cert::TbsCertificate) -> Result<(String, QualifiedHandle)> {
127    let extensions = cert.extensions().ok_or(CertificateError::InvalidFormat)?;
128
129    let san = extensions
130        .iter()
131        .find_map(|e| {
132            (e.extn_id == const_oid::db::rfc5280::ID_CE_SUBJECT_ALT_NAME)
133                .then(|| x509_cert::ext::pkix::SubjectAltName::from_der(e.extn_value.as_bytes()))
134        })
135        .transpose()?
136        .ok_or(CertificateError::InvalidFormat)?;
137
138    let mut client_id = None;
139    let mut handle = None;
140    san.0
141        .iter()
142        .filter_map(|n| match n {
143            x509_cert::ext::pkix::name::GeneralName::UniformResourceIdentifier(ia5_str) => Some(ia5_str.as_str()),
144            _ => None,
145        })
146        .try_for_each(|name| -> Result<()> {
147            // since both ClientId & handle are in the SAN we first try to parse the element as
148            // a ClientId (since it's the most characterizable) and else fallback to a handle
149            if let Ok(cid) = ClientId::try_from_uri(name) {
150                client_id = Some(cid.to_qualified());
151            } else if let Ok(h) = name.parse::<QualifiedHandle>() {
152                handle = Some(h);
153            }
154            Ok(())
155        })?;
156
157    let client_id = client_id.ok_or(CertificateError::MissingClientId)?;
158    let handle = handle.ok_or(CertificateError::MissingHandle)?;
159    Ok((client_id, handle))
160}
161
162#[cfg(test)]
163mod tests {
164    use core_crypto_keystore::Database;
165    use rstest::rstest;
166
167    use super::*;
168
169    const CERT: &str = r#"-----BEGIN CERTIFICATE-----
170MIICGjCCAcCgAwIBAgIRAJaZdl+hZDl9qSSju5kmWNAwCgYIKoZIzj0EAwIwLjEN
171MAsGA1UEChMEd2lyZTEdMBsGA1UEAxMUd2lyZSBJbnRlcm1lZGlhdGUgQ0EwHhcN
172MjQwMTA1MTQ1MzAyWhcNMzQwMTAyMTQ1MzAyWjApMREwDwYDVQQKEwh3aXJlLmNv
173bTEUMBIGA1UEAxMLQWxpY2UgU21pdGgwKjAFBgMrZXADIQChy/GdWnVyNKWvsB+D
174BoxYb+qpVN9QIBXeYdmp1hobOqOB8jCB7zAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0l
175BAwwCgYIKwYBBQUHAwIwHQYDVR0OBBYEFOM5yRKA3dHSlYnjEzcuWoiMWm+TMB8G
176A1UdIwQYMBaAFBP7HtkE3WdbqzE6Ll4aIB2jFM2LMGkGA1UdEQRiMGCGIHdpcmVh
177cHA6Ly8lNDBhbGljZV93aXJlQHdpcmUuY29thjx3aXJlYXBwOi8vb2Jha2pQT0hR
178MkNrTmIwck9yTk0zQSUyMWJhNTRlOGFjZThiNGM5MGRAd2lyZS5jb20wHQYMKwYB
179BAGCpGTGKEABBA0wCwIBBgQEd2lyZQQAMAoGCCqGSM49BAMCA0gAMEUCIDRaadkt
180pPSLrZ+qy07VJOhE/ypOS6oDItpaq/HPxoTUAiEA7EKzmAFv+/zIEA7lAZjNJ+x4
181dHnOydGcC6TZ9zo0pIM=
182-----END CERTIFICATE-----"#;
183
184    const CERT_EXPIRED: &str = r#"-----BEGIN CERTIFICATE-----
185MIICGTCCAb+gAwIBAgIQb84UE+pSF517knYRMfo5ozAKBggqhkjOPQQDAjAuMQ0w
186CwYDVQQKEwR3aXJlMR0wGwYDVQQDExR3aXJlIEludGVybWVkaWF0ZSBDQTAeFw0y
187NDAxMDUxNDUxMjVaFw0yNDAxMDUxNDU0MjVaMCkxETAPBgNVBAoTCHdpcmUuY29t
188MRQwEgYDVQQDEwtBbGljZSBTbWl0aDAqMAUGAytlcAMhAAbao8C3jBq8DxniGYmO
189lq6W1tlkNeRMs8aQ3SvIKMR3o4HyMIHvMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUE
190DDAKBggrBgEFBQcDAjAdBgNVHQ4EFgQUwTyA2moMyOKoHgJ8Y+dJezNuO8gwHwYD
191VR0jBBgwFoAUC7y0skJjTvA8UA3bHr1JoAzOxqgwaQYDVR0RBGIwYIYgd2lyZWFw
192cDovLyU0MGFsaWNlX3dpcmVAd2lyZS5jb22GPHdpcmVhcHA6Ly9OQjNjVnJRZFNi
193Ni1Dd2tmQWljUnpnJTIxNWNkNGViYjFmNzU0ODA5ZUB3aXJlLmNvbTAdBgwrBgEE
194AYKkZMYoQAEEDTALAgEGBAR3aXJlBAAwCgYIKoZIzj0EAwIDSAAwRQIgfwfd5vXm
195EoOKgYLyKNa24aewZZObydD+k0hFs4iKddICIQDf70uv+h0tHw/WNf15mZ8NGkJm
196OfqfZA1YMtN5NLz/AA==
197-----END CERTIFICATE-----"#;
198
199    #[rstest::fixture]
200    async fn pki_env() -> PkiEnvironment {
201        let db = Database::open_in_memory().unwrap();
202        PkiEnvironment::with_dummy_hooks(db).await.unwrap()
203    }
204
205    #[rstest]
206    #[tokio::test]
207    async fn should_find_claims_in_x509(#[future] pki_env: PkiEnvironment) {
208        let cert_der = pem::parse(CERT).unwrap();
209        let identity = cert_der
210            .contents()
211            .extract_identity(&pki_env.await, HashAlgorithm::SHA256)
212            .await
213            .unwrap();
214
215        assert_eq!(&identity.client_id, "obakjPOHQ2CkNb0rOrNM3A:ba54e8ace8b4c90d@wire.com");
216        assert_eq!(identity.handle.as_str(), "wireapp://%40alice_wire@wire.com");
217        assert_eq!(&identity.display_name, "Alice Smith");
218        assert_eq!(&identity.domain, "wire.com");
219        assert_eq!(&identity.serial_number, "009699765fa164397da924a3bb992658d0");
220        assert_eq!(identity.not_before, 1704466382);
221        assert_eq!(identity.not_after, 2019826382);
222    }
223
224    #[test]
225    fn should_find_created_at_claim() {
226        let cert_der = pem::parse(CERT).unwrap();
227        let created_at = cert_der.contents().extract_created_at().unwrap();
228        assert_eq!(created_at, 1704466382);
229    }
230
231    #[test]
232    fn should_find_public_key() {
233        let cert_der = pem::parse(CERT).unwrap();
234        let spki = cert_der.contents().extract_public_key().unwrap();
235        assert_eq!(
236            hex::encode(spki),
237            "a1cbf19d5a757234a5afb01f83068c586feaa954df502015de61d9a9d61a1b3a"
238        );
239    }
240
241    #[rstest]
242    #[tokio::test]
243    async fn should_have_expired_status(#[future] pki_env: PkiEnvironment) {
244        let cert_der = pem::parse(CERT_EXPIRED).unwrap();
245        let identity = cert_der
246            .contents()
247            .extract_identity(&pki_env.await, HashAlgorithm::SHA256)
248            .await
249            .unwrap();
250        assert_eq!(&identity.status, &IdentityStatus::Expired);
251    }
252
253    #[rstest]
254    #[tokio::test]
255    async fn should_have_thumbprint(#[future] pki_env: PkiEnvironment) {
256        let cert_der = pem::parse(CERT).unwrap();
257        let identity = cert_der
258            .contents()
259            .extract_identity(&pki_env.await, HashAlgorithm::SHA256)
260            .await
261            .unwrap();
262        assert!(!identity.thumbprint.is_empty());
263    }
264}