wire_e2e_identity/acme/identity/
mod.rs

1use rusty_jwt_tools::prelude::*;
2use x509_cert::der::Decode as _;
3
4use crate::acme::{
5    error::CertificateError,
6    prelude::*,
7    x509_check::{IdentityStatus, revocation::PkiEnvironment},
8};
9
10pub(crate) mod thumbprint;
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    fn extract_identity(&self, env: Option<&PkiEnvironment>, hash_alg: HashAlgorithm) -> RustyAcmeResult<WireIdentity>;
29
30    /// returns the 'Not Before' claim which usually matches the creation timestamp
31    fn extract_created_at(&self) -> RustyAcmeResult<u64>;
32
33    /// returns the 'Subject Public Key Info' claim
34    fn extract_public_key(&self) -> RustyAcmeResult<Vec<u8>>;
35}
36
37impl WireIdentityReader for x509_cert::Certificate {
38    fn extract_identity(&self, env: Option<&PkiEnvironment>, hash_alg: HashAlgorithm) -> RustyAcmeResult<WireIdentity> {
39        let serial_number = hex::encode(self.tbs_certificate.serial_number.as_bytes());
40        let not_before = self.tbs_certificate.validity.not_before.to_unix_duration().as_secs();
41        let not_after = self.tbs_certificate.validity.not_after.to_unix_duration().as_secs();
42        let (client_id, handle) = try_extract_san(&self.tbs_certificate)?;
43        let (display_name, domain) = try_extract_subject(&self.tbs_certificate)?;
44        let status = IdentityStatus::from_cert(self, env);
45        let thumbprint = thumbprint::try_compute_jwk_canonicalized_thumbprint(&self.tbs_certificate, hash_alg)?;
46
47        Ok(WireIdentity {
48            client_id,
49            handle,
50            display_name,
51            domain,
52            status,
53            thumbprint,
54            serial_number,
55            not_before,
56            not_after,
57        })
58    }
59
60    fn extract_created_at(&self) -> RustyAcmeResult<u64> {
61        Ok(self.tbs_certificate.validity.not_before.to_unix_duration().as_secs())
62    }
63
64    fn extract_public_key(&self) -> RustyAcmeResult<Vec<u8>> {
65        Ok(self
66            .tbs_certificate
67            .subject_public_key_info
68            .subject_public_key
69            .raw_bytes()
70            .to_vec())
71    }
72}
73
74impl WireIdentityReader for &[u8] {
75    fn extract_identity(&self, env: Option<&PkiEnvironment>, hash_alg: HashAlgorithm) -> RustyAcmeResult<WireIdentity> {
76        x509_cert::Certificate::from_der(self)?.extract_identity(env, hash_alg)
77    }
78
79    fn extract_created_at(&self) -> RustyAcmeResult<u64> {
80        x509_cert::Certificate::from_der(self)?.extract_created_at()
81    }
82
83    fn extract_public_key(&self) -> RustyAcmeResult<Vec<u8>> {
84        x509_cert::Certificate::from_der(self)?.extract_public_key()
85    }
86}
87
88impl WireIdentityReader for Vec<u8> {
89    fn extract_identity(&self, env: Option<&PkiEnvironment>, hash_alg: HashAlgorithm) -> RustyAcmeResult<WireIdentity> {
90        self.as_slice().extract_identity(env, hash_alg)
91    }
92
93    fn extract_created_at(&self) -> RustyAcmeResult<u64> {
94        self.as_slice().extract_created_at()
95    }
96
97    fn extract_public_key(&self) -> RustyAcmeResult<Vec<u8>> {
98        self.as_slice().extract_public_key()
99    }
100}
101
102fn try_extract_subject(cert: &x509_cert::TbsCertificate) -> RustyAcmeResult<(String, String)> {
103    let mut display_name = None;
104    let mut domain = None;
105
106    let mut subjects = cert.subject.0.iter().flat_map(|n| n.0.iter());
107    subjects.try_for_each(|s| -> RustyAcmeResult<()> {
108        match s.oid {
109            const_oid::db::rfc4519::ORGANIZATION_NAME => {
110                domain = Some(std::str::from_utf8(s.value.value())?);
111            }
112            const_oid::db::rfc4519::COMMON_NAME => {
113                display_name = Some(std::str::from_utf8(s.value.value())?);
114            }
115            _ => {}
116        }
117
118        Ok(())
119    })?;
120    let display_name = display_name.ok_or(CertificateError::MissingDisplayName)?.to_string();
121    let domain = domain.ok_or(CertificateError::MissingDomain)?.to_string();
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) -> RustyAcmeResult<(String, QualifiedHandle)> {
127    let extensions = cert.extensions.as_ref().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| -> RustyAcmeResult<()> {
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)]
163pub mod tests {
164    use wasm_bindgen_test::*;
165
166    use super::*;
167    use crate::acme::x509_check::revocation::PkiEnvironmentParams;
168
169    wasm_bindgen_test_configure!(run_in_browser);
170
171    const CERT: &str = r#"-----BEGIN CERTIFICATE-----
172MIICGjCCAcCgAwIBAgIRAJaZdl+hZDl9qSSju5kmWNAwCgYIKoZIzj0EAwIwLjEN
173MAsGA1UEChMEd2lyZTEdMBsGA1UEAxMUd2lyZSBJbnRlcm1lZGlhdGUgQ0EwHhcN
174MjQwMTA1MTQ1MzAyWhcNMzQwMTAyMTQ1MzAyWjApMREwDwYDVQQKEwh3aXJlLmNv
175bTEUMBIGA1UEAxMLQWxpY2UgU21pdGgwKjAFBgMrZXADIQChy/GdWnVyNKWvsB+D
176BoxYb+qpVN9QIBXeYdmp1hobOqOB8jCB7zAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0l
177BAwwCgYIKwYBBQUHAwIwHQYDVR0OBBYEFOM5yRKA3dHSlYnjEzcuWoiMWm+TMB8G
178A1UdIwQYMBaAFBP7HtkE3WdbqzE6Ll4aIB2jFM2LMGkGA1UdEQRiMGCGIHdpcmVh
179cHA6Ly8lNDBhbGljZV93aXJlQHdpcmUuY29thjx3aXJlYXBwOi8vb2Jha2pQT0hR
180MkNrTmIwck9yTk0zQSUyMWJhNTRlOGFjZThiNGM5MGRAd2lyZS5jb20wHQYMKwYB
181BAGCpGTGKEABBA0wCwIBBgQEd2lyZQQAMAoGCCqGSM49BAMCA0gAMEUCIDRaadkt
182pPSLrZ+qy07VJOhE/ypOS6oDItpaq/HPxoTUAiEA7EKzmAFv+/zIEA7lAZjNJ+x4
183dHnOydGcC6TZ9zo0pIM=
184-----END CERTIFICATE-----"#;
185
186    const CERT_EXPIRED: &str = r#"-----BEGIN CERTIFICATE-----
187MIICGTCCAb+gAwIBAgIQb84UE+pSF517knYRMfo5ozAKBggqhkjOPQQDAjAuMQ0w
188CwYDVQQKEwR3aXJlMR0wGwYDVQQDExR3aXJlIEludGVybWVkaWF0ZSBDQTAeFw0y
189NDAxMDUxNDUxMjVaFw0yNDAxMDUxNDU0MjVaMCkxETAPBgNVBAoTCHdpcmUuY29t
190MRQwEgYDVQQDEwtBbGljZSBTbWl0aDAqMAUGAytlcAMhAAbao8C3jBq8DxniGYmO
191lq6W1tlkNeRMs8aQ3SvIKMR3o4HyMIHvMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUE
192DDAKBggrBgEFBQcDAjAdBgNVHQ4EFgQUwTyA2moMyOKoHgJ8Y+dJezNuO8gwHwYD
193VR0jBBgwFoAUC7y0skJjTvA8UA3bHr1JoAzOxqgwaQYDVR0RBGIwYIYgd2lyZWFw
194cDovLyU0MGFsaWNlX3dpcmVAd2lyZS5jb22GPHdpcmVhcHA6Ly9OQjNjVnJRZFNi
195Ni1Dd2tmQWljUnpnJTIxNWNkNGViYjFmNzU0ODA5ZUB3aXJlLmNvbTAdBgwrBgEE
196AYKkZMYoQAEEDTALAgEGBAR3aXJlBAAwCgYIKoZIzj0EAwIDSAAwRQIgfwfd5vXm
197EoOKgYLyKNa24aewZZObydD+k0hFs4iKddICIQDf70uv+h0tHw/WNf15mZ8NGkJm
198OfqfZA1YMtN5NLz/AA==
199-----END CERTIFICATE-----"#;
200
201    #[test]
202    #[wasm_bindgen_test]
203    fn should_find_claims_in_x509() {
204        let cert_der = pem::parse(CERT).unwrap();
205        let identity = cert_der
206            .contents()
207            .extract_identity(None, HashAlgorithm::SHA256)
208            .unwrap();
209
210        assert_eq!(&identity.client_id, "obakjPOHQ2CkNb0rOrNM3A:ba54e8ace8b4c90d@wire.com");
211        assert_eq!(identity.handle.as_str(), "wireapp://%40alice_wire@wire.com");
212        assert_eq!(&identity.display_name, "Alice Smith");
213        assert_eq!(&identity.domain, "wire.com");
214        assert_eq!(&identity.serial_number, "009699765fa164397da924a3bb992658d0");
215        assert_eq!(identity.not_before, 1704466382);
216        assert_eq!(identity.not_after, 2019826382);
217    }
218
219    #[test]
220    #[wasm_bindgen_test]
221    fn should_find_created_at_claim() {
222        let cert_der = pem::parse(CERT).unwrap();
223        let created_at = cert_der.contents().extract_created_at().unwrap();
224        assert_eq!(created_at, 1704466382);
225    }
226
227    #[test]
228    #[wasm_bindgen_test]
229    fn should_find_public_key() {
230        let cert_der = pem::parse(CERT).unwrap();
231        let spki = cert_der.contents().extract_public_key().unwrap();
232        assert_eq!(
233            hex::encode(spki),
234            "a1cbf19d5a757234a5afb01f83068c586feaa954df502015de61d9a9d61a1b3a"
235        );
236    }
237
238    #[test]
239    #[wasm_bindgen_test]
240    fn should_have_valid_status() {
241        let cert_der = pem::parse(CERT).unwrap();
242        let identity = cert_der
243            .contents()
244            .extract_identity(None, HashAlgorithm::SHA256)
245            .unwrap();
246        assert_eq!(&identity.status, &IdentityStatus::Valid);
247
248        let cert_der = pem::parse(CERT_EXPIRED).unwrap();
249        let mut env = PkiEnvironment::init(PkiEnvironmentParams::default()).unwrap();
250        env.refresh_time_of_interest().unwrap();
251        let identity = cert_der
252            .contents()
253            .extract_identity(Some(&env), HashAlgorithm::SHA256)
254            .unwrap();
255        assert_eq!(&identity.status, &IdentityStatus::Expired);
256    }
257
258    #[test]
259    #[wasm_bindgen_test]
260    fn should_have_thumbprint() {
261        let cert_der = pem::parse(CERT).unwrap();
262        let identity = cert_der
263            .contents()
264            .extract_identity(None, HashAlgorithm::SHA256)
265            .unwrap();
266        assert!(!identity.thumbprint.is_empty());
267    }
268}