wire_e2e_identity/acme/identity/
mod.rs

1use rusty_jwt_tools::prelude::{ClientId, HashAlgorithm, QualifiedHandle};
2use x509_cert::der::Decode as _;
3
4use crate::{
5    acme::{RustyAcmeResult, error::CertificateError},
6    x509_check::{IdentityStatus, revocation::PkiEnvironment},
7};
8
9pub(crate) mod thumbprint;
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: Option<&PkiEnvironment>, hash_alg: HashAlgorithm) -> RustyAcmeResult<WireIdentity>;
28
29    /// returns the 'Not Before' claim which usually matches the creation timestamp
30    fn extract_created_at(&self) -> RustyAcmeResult<u64>;
31
32    /// returns the 'Subject Public Key Info' claim
33    fn extract_public_key(&self) -> RustyAcmeResult<Vec<u8>>;
34}
35
36impl WireIdentityReader for x509_cert::Certificate {
37    fn extract_identity(&self, env: Option<&PkiEnvironment>, hash_alg: HashAlgorithm) -> RustyAcmeResult<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 = 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) -> RustyAcmeResult<u64> {
60        Ok(self.tbs_certificate.validity.not_before.to_unix_duration().as_secs())
61    }
62
63    fn extract_public_key(&self) -> RustyAcmeResult<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: Option<&PkiEnvironment>, hash_alg: HashAlgorithm) -> RustyAcmeResult<WireIdentity> {
75        x509_cert::Certificate::from_der(self)?.extract_identity(env, hash_alg)
76    }
77
78    fn extract_created_at(&self) -> RustyAcmeResult<u64> {
79        x509_cert::Certificate::from_der(self)?.extract_created_at()
80    }
81
82    fn extract_public_key(&self) -> RustyAcmeResult<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: Option<&PkiEnvironment>, hash_alg: HashAlgorithm) -> RustyAcmeResult<WireIdentity> {
89        self.as_slice().extract_identity(env, hash_alg)
90    }
91
92    fn extract_created_at(&self) -> RustyAcmeResult<u64> {
93        self.as_slice().extract_created_at()
94    }
95
96    fn extract_public_key(&self) -> RustyAcmeResult<Vec<u8>> {
97        self.as_slice().extract_public_key()
98    }
99}
100
101fn try_extract_subject(cert: &x509_cert::TbsCertificate) -> RustyAcmeResult<(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| -> RustyAcmeResult<()> {
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) -> RustyAcmeResult<(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| -> RustyAcmeResult<()> {
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 wasm_bindgen_test::*;
164
165    use super::*;
166    use crate::x509_check::revocation::PkiEnvironmentParams;
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    #[test]
201    #[wasm_bindgen_test]
202    fn should_find_claims_in_x509() {
203        let cert_der = pem::parse(CERT).unwrap();
204        let identity = cert_der
205            .contents()
206            .extract_identity(None, HashAlgorithm::SHA256)
207            .unwrap();
208
209        assert_eq!(&identity.client_id, "obakjPOHQ2CkNb0rOrNM3A:ba54e8ace8b4c90d@wire.com");
210        assert_eq!(identity.handle.as_str(), "wireapp://%40alice_wire@wire.com");
211        assert_eq!(&identity.display_name, "Alice Smith");
212        assert_eq!(&identity.domain, "wire.com");
213        assert_eq!(&identity.serial_number, "009699765fa164397da924a3bb992658d0");
214        assert_eq!(identity.not_before, 1704466382);
215        assert_eq!(identity.not_after, 2019826382);
216    }
217
218    #[test]
219    #[wasm_bindgen_test]
220    fn should_find_created_at_claim() {
221        let cert_der = pem::parse(CERT).unwrap();
222        let created_at = cert_der.contents().extract_created_at().unwrap();
223        assert_eq!(created_at, 1704466382);
224    }
225
226    #[test]
227    #[wasm_bindgen_test]
228    fn should_find_public_key() {
229        let cert_der = pem::parse(CERT).unwrap();
230        let spki = cert_der.contents().extract_public_key().unwrap();
231        assert_eq!(
232            hex::encode(spki),
233            "a1cbf19d5a757234a5afb01f83068c586feaa954df502015de61d9a9d61a1b3a"
234        );
235    }
236
237    #[test]
238    #[wasm_bindgen_test]
239    fn should_have_valid_status() {
240        let cert_der = pem::parse(CERT).unwrap();
241        let identity = cert_der
242            .contents()
243            .extract_identity(None, HashAlgorithm::SHA256)
244            .unwrap();
245        assert_eq!(&identity.status, &IdentityStatus::Valid);
246
247        let cert_der = pem::parse(CERT_EXPIRED).unwrap();
248        let mut env = PkiEnvironment::init(PkiEnvironmentParams::default()).unwrap();
249        env.refresh_time_of_interest().unwrap();
250        let identity = cert_der
251            .contents()
252            .extract_identity(Some(&env), HashAlgorithm::SHA256)
253            .unwrap();
254        assert_eq!(&identity.status, &IdentityStatus::Expired);
255    }
256
257    #[test]
258    #[wasm_bindgen_test]
259    fn should_have_thumbprint() {
260        let cert_der = pem::parse(CERT).unwrap();
261        let identity = cert_der
262            .contents()
263            .extract_identity(None, HashAlgorithm::SHA256)
264            .unwrap();
265        assert!(!identity.thumbprint.is_empty());
266    }
267}