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