wire_e2e_identity/acquisition/
identity.rs1use rusty_jwt_tools::prelude::{ClientId, 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 #[expect(async_fn_in_trait)]
29 async fn extract_identity(&self, env: &PkiEnvironment) -> Result<WireIdentity>;
30
31 fn extract_created_at(&self) -> Result<u64>;
33
34 fn extract_public_key(&self) -> Result<Vec<u8>>;
36}
37
38impl WireIdentityReader for x509_cert::Certificate {
39 async fn extract_identity(&self, env: &PkiEnvironment) -> 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())?;
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) -> Result<WireIdentity> {
83 x509_cert::Certificate::from_der(self)?.extract_identity(env).await
84 }
85
86 fn extract_created_at(&self) -> Result<u64> {
87 x509_cert::Certificate::from_der(self)?.extract_created_at()
88 }
89
90 fn extract_public_key(&self) -> Result<Vec<u8>> {
91 x509_cert::Certificate::from_der(self)?.extract_public_key()
92 }
93}
94
95impl WireIdentityReader for Vec<u8> {
96 async fn extract_identity(&self, env: &PkiEnvironment) -> Result<WireIdentity> {
97 self.as_slice().extract_identity(env).await
98 }
99
100 fn extract_created_at(&self) -> Result<u64> {
101 self.as_slice().extract_created_at()
102 }
103
104 fn extract_public_key(&self) -> Result<Vec<u8>> {
105 self.as_slice().extract_public_key()
106 }
107}
108
109fn try_extract_subject(cert: &x509_cert::TbsCertificate) -> Result<(String, String)> {
110 let display_name = cert
111 .subject()
112 .common_name()?
113 .ok_or(CertificateError::MissingDisplayName)?
114 .into();
115 let domain = cert
116 .subject()
117 .organization()?
118 .ok_or(CertificateError::MissingDomain)?
119 .into();
120 Ok((display_name, domain))
121}
122
123fn try_extract_san(cert: &x509_cert::TbsCertificate) -> Result<(String, QualifiedHandle)> {
125 let extensions = cert.extensions().ok_or(CertificateError::InvalidFormat)?;
126
127 let san = extensions
128 .iter()
129 .find_map(|e| {
130 (e.extn_id == const_oid::db::rfc5280::ID_CE_SUBJECT_ALT_NAME)
131 .then(|| x509_cert::ext::pkix::SubjectAltName::from_der(e.extn_value.as_bytes()))
132 })
133 .transpose()?
134 .ok_or(CertificateError::InvalidFormat)?;
135
136 let mut client_id = None;
137 let mut handle = None;
138 san.0
139 .iter()
140 .filter_map(|n| match n {
141 x509_cert::ext::pkix::name::GeneralName::UniformResourceIdentifier(ia5_str) => Some(ia5_str.as_str()),
142 _ => None,
143 })
144 .try_for_each(|name| -> Result<()> {
145 if let Ok(cid) = ClientId::try_from_uri(name) {
148 client_id = Some(cid.to_qualified());
149 } else if let Ok(h) = name.parse::<QualifiedHandle>() {
150 handle = Some(h);
151 }
152 Ok(())
153 })?;
154
155 let client_id = client_id.ok_or(CertificateError::MissingClientId)?;
156 let handle = handle.ok_or(CertificateError::MissingHandle)?;
157 Ok((client_id, handle))
158}
159
160#[cfg(test)]
161mod tests {
162 use core_crypto_keystore::Database;
163 use rstest::rstest;
164
165 use super::*;
166
167 const CERT: &str = r#"-----BEGIN CERTIFICATE-----
168MIICGjCCAcCgAwIBAgIRAJaZdl+hZDl9qSSju5kmWNAwCgYIKoZIzj0EAwIwLjEN
169MAsGA1UEChMEd2lyZTEdMBsGA1UEAxMUd2lyZSBJbnRlcm1lZGlhdGUgQ0EwHhcN
170MjQwMTA1MTQ1MzAyWhcNMzQwMTAyMTQ1MzAyWjApMREwDwYDVQQKEwh3aXJlLmNv
171bTEUMBIGA1UEAxMLQWxpY2UgU21pdGgwKjAFBgMrZXADIQChy/GdWnVyNKWvsB+D
172BoxYb+qpVN9QIBXeYdmp1hobOqOB8jCB7zAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0l
173BAwwCgYIKwYBBQUHAwIwHQYDVR0OBBYEFOM5yRKA3dHSlYnjEzcuWoiMWm+TMB8G
174A1UdIwQYMBaAFBP7HtkE3WdbqzE6Ll4aIB2jFM2LMGkGA1UdEQRiMGCGIHdpcmVh
175cHA6Ly8lNDBhbGljZV93aXJlQHdpcmUuY29thjx3aXJlYXBwOi8vb2Jha2pQT0hR
176MkNrTmIwck9yTk0zQSUyMWJhNTRlOGFjZThiNGM5MGRAd2lyZS5jb20wHQYMKwYB
177BAGCpGTGKEABBA0wCwIBBgQEd2lyZQQAMAoGCCqGSM49BAMCA0gAMEUCIDRaadkt
178pPSLrZ+qy07VJOhE/ypOS6oDItpaq/HPxoTUAiEA7EKzmAFv+/zIEA7lAZjNJ+x4
179dHnOydGcC6TZ9zo0pIM=
180-----END CERTIFICATE-----"#;
181
182 const CERT_EXPIRED: &str = r#"-----BEGIN CERTIFICATE-----
183MIICGTCCAb+gAwIBAgIQb84UE+pSF517knYRMfo5ozAKBggqhkjOPQQDAjAuMQ0w
184CwYDVQQKEwR3aXJlMR0wGwYDVQQDExR3aXJlIEludGVybWVkaWF0ZSBDQTAeFw0y
185NDAxMDUxNDUxMjVaFw0yNDAxMDUxNDU0MjVaMCkxETAPBgNVBAoTCHdpcmUuY29t
186MRQwEgYDVQQDEwtBbGljZSBTbWl0aDAqMAUGAytlcAMhAAbao8C3jBq8DxniGYmO
187lq6W1tlkNeRMs8aQ3SvIKMR3o4HyMIHvMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUE
188DDAKBggrBgEFBQcDAjAdBgNVHQ4EFgQUwTyA2moMyOKoHgJ8Y+dJezNuO8gwHwYD
189VR0jBBgwFoAUC7y0skJjTvA8UA3bHr1JoAzOxqgwaQYDVR0RBGIwYIYgd2lyZWFw
190cDovLyU0MGFsaWNlX3dpcmVAd2lyZS5jb22GPHdpcmVhcHA6Ly9OQjNjVnJRZFNi
191Ni1Dd2tmQWljUnpnJTIxNWNkNGViYjFmNzU0ODA5ZUB3aXJlLmNvbTAdBgwrBgEE
192AYKkZMYoQAEEDTALAgEGBAR3aXJlBAAwCgYIKoZIzj0EAwIDSAAwRQIgfwfd5vXm
193EoOKgYLyKNa24aewZZObydD+k0hFs4iKddICIQDf70uv+h0tHw/WNf15mZ8NGkJm
194OfqfZA1YMtN5NLz/AA==
195-----END CERTIFICATE-----"#;
196
197 #[rstest::fixture]
198 async fn pki_env() -> PkiEnvironment {
199 let db = Database::open_in_memory().unwrap();
200 PkiEnvironment::with_dummy_hooks(db).await.unwrap()
201 }
202
203 #[rstest]
204 #[tokio::test]
205 async fn should_find_claims_in_x509(#[future] pki_env: PkiEnvironment) {
206 let cert_der = pem::parse(CERT).unwrap();
207 let identity = cert_der.contents().extract_identity(&pki_env.await).await.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 fn should_find_created_at_claim() {
220 let cert_der = pem::parse(CERT).unwrap();
221 let created_at = cert_der.contents().extract_created_at().unwrap();
222 assert_eq!(created_at, 1704466382);
223 }
224
225 #[test]
226 fn should_find_public_key() {
227 let cert_der = pem::parse(CERT).unwrap();
228 let spki = cert_der.contents().extract_public_key().unwrap();
229 assert_eq!(
230 hex::encode(spki),
231 "a1cbf19d5a757234a5afb01f83068c586feaa954df502015de61d9a9d61a1b3a"
232 );
233 }
234
235 #[rstest]
236 #[tokio::test]
237 async fn should_have_expired_status(#[future] pki_env: PkiEnvironment) {
238 let cert_der = pem::parse(CERT_EXPIRED).unwrap();
239 let identity = cert_der.contents().extract_identity(&pki_env.await).await.unwrap();
240 assert_eq!(&identity.status, &IdentityStatus::Expired);
241 }
242
243 #[rstest]
244 #[tokio::test]
245 async fn should_have_thumbprint(#[future] pki_env: PkiEnvironment) {
246 let cert_der = pem::parse(CERT).unwrap();
247 let identity = cert_der.contents().extract_identity(&pki_env.await).await.unwrap();
248 assert!(!identity.thumbprint.is_empty());
249 }
250}