wire_e2e_identity/acme/identity/
mod.rs1use 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 fn extract_identity(&self, env: Option<&PkiEnvironment>, hash_alg: HashAlgorithm) -> RustyAcmeResult<WireIdentity>;
28
29 fn extract_created_at(&self) -> RustyAcmeResult<u64>;
31
32 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
124fn 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 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}