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