1use std::{
2 collections::BTreeMap,
3 convert::TryFrom,
4 fmt,
5 io::{self, Read},
6};
7
8use chrono::prelude::*;
9use ciborium::value::Value;
10use flate2::read::ZlibDecoder;
11use serde_derive::Deserialize;
12use thiserror::Error;
13
14mod values;
15pub use values::*;
16
17type Result<T> = std::result::Result<T, Error>;
18
19#[derive(Deserialize)]
20struct Cwt(Vec<Value>);
21
22impl fmt::Display for Cwt {
23 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24 write!(f, "Cwt {{ values: [")?;
25
26 let mut iter = self.0.iter();
27 if let Some(v1) = iter.next() {
28 write!(f, "{:?}", v1)?;
29
30 for v in iter {
31 write!(f, ", {:?}", v)?;
32 }
33 }
34
35 write!(f, "] }}")
36 }
37}
38
39#[derive(Deserialize)]
40struct RawCert(BTreeMap<isize, Value>);
41
42#[derive(Deserialize)]
43struct RawHeader(BTreeMap<isize, Value>);
44
45#[derive(Debug, Error)]
47pub enum Error {
48 #[error("invalid base45 in input")]
49 InvalidBase45(#[from] base45::DecodeError),
50
51 #[error(transparent)]
52 IOError(#[from] io::Error),
53
54 #[error("invalid key in document: {0}")]
55 InvalidKey(String),
56
57 #[error("invalid format for `{key}`")]
58 InvalidFormatFor { key: String },
59
60 #[error("failed to parse a payload as CBOR")]
61 MalformedCBOR(#[from] ciborium::de::Error<std::io::Error>),
62
63 #[error("the root structure for the certificate is malformed")]
64 MalformedCWT,
65
66 #[error("malformed date: {0}")]
67 MalformedDate(String),
68
69 #[error("found unexpected non-string keys in map")]
70 MalformedStringMap,
71
72 #[error("missing initial HC string from input")]
73 MissingHCID,
74
75 #[error("invalid key in document: {0}")]
76 MissingKey(String),
77
78 #[error("spurious leftover data detected: {0:?}")]
79 SpuriousData(BTreeMap<String, Value>),
80}
81
82macro_rules! map_empty {
83 ($m:expr) => {
84 if !$m.is_empty() {
85 return Err(Error::SpuriousData($m));
86 }
87 };
88}
89
90macro_rules! gen_extract {
92 ($name:ident, $variant:path, $for_type:ty) => {
93 fn $name(m: &mut BTreeMap<String, Value>, k: &str) -> Result<$for_type> {
94 extract_key(m, k).and_then(|v| match v {
95 $variant(r) => Ok(r.into()),
96 _ => Err(Error::InvalidFormatFor { key: k.into() }),
97 })
98 }
99 };
100}
101
102gen_extract!(extract_array, Value::Array, Vec<Value>);
103
104fn extract_date(m: &mut BTreeMap<String, Value>, k: &str) -> Result<NaiveDate> {
105 extract_string(m, k)
106 .and_then(|ds| NaiveDate::parse_from_str(&ds, "%F").map_err(|_| Error::MalformedDate(ds)))
107}
108
109fn extract_isodatetime(m: &mut BTreeMap<String, Value>, k: &str) -> Result<DateTime<FixedOffset>> {
110 extract_string(m, k).and_then(|ds| {
111 DateTime::parse_from_str(&ds, "%+")
112 .or_else(|_| DateTime::parse_from_str(&ds, "%Y-%m-%dT%H:%M:%S%.f%#z"))
113 .or_else(|_| DateTime::parse_from_str(&ds, "%Y-%m-%dT%H:%M:%S%.f%z"))
114 .map_err(|_| Error::MalformedDate(ds))
115 })
116}
117
118gen_extract!(extract_int, Value::Integer, i128);
119
120fn extract_key(m: &mut BTreeMap<String, Value>, k: &str) -> Result<Value> {
121 m.remove(k).ok_or_else(|| Error::MissingKey(k.into()))
122}
123
124gen_extract!(extract_string, Value::Text, String);
125
126fn extract_string_map(m: &mut BTreeMap<String, Value>, k: &str) -> Result<BTreeMap<String, Value>> {
127 to_strmap(k, extract_key(m, k)?)
128}
129
130#[derive(Debug, PartialEq)]
131pub enum CertInfo {
132 Recovery(Recovery),
133 Test(Test),
134 Vaccine(Vaccine),
135}
136
137#[derive(Debug, PartialEq)]
139pub struct GreenPass {
140 pub date_of_birth: String, pub surname: String, pub givenname: String, pub std_surname: String, pub std_givenname: String, pub ver: String, pub entries: Vec<CertInfo>, }
161
162impl TryFrom<BTreeMap<String, Value>> for GreenPass {
163 type Error = Error;
164
165 fn try_from(mut values: BTreeMap<String, Value>) -> std::result::Result<Self, Self::Error> {
166 let date_of_birth = extract_string(&mut values, "dob")?;
167 let ver = extract_string(&mut values, "ver")?;
168
169 let entries = if let Ok(rs) = extract_array(&mut values, "r") {
170 rs.into_iter()
171 .map(|v| {
172 to_strmap("recovery entry", v)
173 .and_then(Recovery::try_from)
174 .map(CertInfo::Recovery)
175 })
176 .collect::<Result<_>>()?
177 } else if let Ok(ts) = extract_array(&mut values, "t") {
178 ts.into_iter()
179 .map(|v| {
180 to_strmap("test entry", v)
181 .and_then(Test::try_from)
182 .map(CertInfo::Test)
183 })
184 .collect::<Result<_>>()?
185 } else if let Ok(vs) = extract_array(&mut values, "v") {
186 vs.into_iter()
187 .map(|v| {
188 to_strmap("vaccine entry", v)
189 .and_then(Vaccine::try_from)
190 .map(CertInfo::Vaccine)
191 })
192 .collect::<Result<_>>()?
193 } else {
194 return Err(Error::MissingKey("r, t or v (the actual data)".into()));
195 };
196
197 let mut nam = extract_string_map(&mut values, "nam")?;
198
199 let surname = extract_string(&mut nam, "fn")?;
200 let givenname = extract_string(&mut nam, "gn")?;
201 let std_surname = extract_string(&mut nam, "fnt")?;
202 let std_givenname = extract_string(&mut nam, "gnt")?;
203
204 let gp = GreenPass {
205 date_of_birth,
206 surname,
207 givenname,
208 std_surname,
209 std_givenname,
210 ver,
211 entries,
212 };
213
214 map_empty!(values);
215
216 Ok(gp)
217 }
218}
219
220#[derive(Debug, PartialEq)]
222pub struct Signature {
223 pub kid: Vec<u8>,
225
226 pub algorithm: i128,
228
229 pub signature: Vec<u8>,
231}
232
233#[derive(Debug, PartialEq)]
235pub struct HealthCert {
236 pub some_issuer: Option<String>,
238
239 pub created: DateTime<Utc>,
241
242 pub expires: DateTime<Utc>,
244
245 pub passes: Vec<GreenPass>,
247
248 pub signature: Signature,
250}
251
252#[derive(Debug, PartialEq)]
254pub struct Recovery {
255 pub cert_id: String, pub country: String, pub diagnosed: NaiveDate, pub disease: String, pub issuer: String, pub valid_from: NaiveDate, pub valid_until: NaiveDate, }
276
277impl TryFrom<BTreeMap<String, Value>> for Recovery {
278 type Error = Error;
279
280 fn try_from(mut values: BTreeMap<String, Value>) -> std::result::Result<Self, Self::Error> {
281 let cert_id = extract_string(&mut values, "ci")?;
282 let country = extract_string(&mut values, "co")?;
283 let diagnosed = extract_date(&mut values, "fr")?;
284 let disease = extract_string(&mut values, "tg")?;
285 let issuer = extract_string(&mut values, "is")?;
286 let valid_from = extract_date(&mut values, "df")?;
287 let valid_until = extract_date(&mut values, "du")?;
288
289 let gp = Recovery {
290 cert_id,
291 country,
292 diagnosed,
293 disease,
294 issuer,
295 valid_from,
296 valid_until,
297 };
298
299 map_empty!(values);
300
301 Ok(gp)
302 }
303}
304
305#[derive(Debug, PartialEq)]
307pub struct Test {
308 pub cert_id: String, pub collect_ts: DateTime<FixedOffset>, pub country: String, pub disease: String, pub issuer: String, pub name: TestName, pub result: String, pub test_type: String, pub testing_centre: String, }
335
336impl TryFrom<BTreeMap<String, Value>> for Test {
337 type Error = Error;
338
339 fn try_from(mut values: BTreeMap<String, Value>) -> std::result::Result<Self, Self::Error> {
340 let cert_id = extract_string(&mut values, "ci")?;
341 let collect_ts = extract_isodatetime(&mut values, "sc")?;
342 let country = extract_string(&mut values, "co")?;
343 let disease = extract_string(&mut values, "tg")?;
344 let issuer = extract_string(&mut values, "is")?;
345
346 let name = if let Ok(nm) = extract_string(&mut values, "nm") {
347 TestName::NAAT { name: nm }
348 } else if let Ok(ma) = extract_string(&mut values, "ma") {
349 TestName::RAT { device_id: ma }
350 } else {
351 return Err(Error::MissingKey("ma or nm in test".into()));
352 };
353
354 let result = extract_string(&mut values, "tr")?;
355 let test_type = extract_string(&mut values, "tt")?;
356 let testing_centre = extract_string(&mut values, "tc")?;
357
358 let ts = Test {
359 cert_id,
360 collect_ts,
361 country,
362 disease,
363 issuer,
364 name,
365 result,
366 test_type,
367 testing_centre,
368 };
369
370 map_empty!(values);
371
372 Ok(ts)
373 }
374}
375
376#[derive(Debug, PartialEq)]
378pub struct Vaccine {
379 pub cert_id: String, pub country: String, pub date: NaiveDate, pub disease: String, pub dose_number: usize, pub dose_total: usize, pub issuer: String, pub market_auth: String, pub product: String, pub prophylaxis_kind: String, }
409
410impl TryFrom<BTreeMap<String, Value>> for Vaccine {
411 type Error = Error;
412
413 fn try_from(mut values: BTreeMap<String, Value>) -> std::result::Result<Self, Self::Error> {
414 let cert_id = extract_string(&mut values, "ci")?;
415 let country = extract_string(&mut values, "co")?;
416 let date = extract_date(&mut values, "dt")?;
417 let disease = extract_string(&mut values, "tg")?;
418 let dose_number = extract_int(&mut values, "dn")? as usize;
419 let dose_total = extract_int(&mut values, "sd")? as usize;
420 let issuer = extract_string(&mut values, "is")?;
421 let market_auth = extract_string(&mut values, "ma")?;
422 let product = extract_string(&mut values, "mp")?;
423 let prophylaxis_kind = extract_string(&mut values, "vp")?;
424
425 let gp = Vaccine {
426 cert_id,
427 country,
428 date,
429 disease,
430 dose_number,
431 dose_total,
432 issuer,
433 market_auth,
434 product,
435 prophylaxis_kind,
436 };
437
438 map_empty!(values);
439
440 Ok(gp)
441 }
442}
443
444fn to_strmap(desc: &str, v: Value) -> Result<BTreeMap<String, Value>> {
445 match v {
446 Value::Map(m) => m
447 .into_iter()
448 .map(|(k, v)| match k {
449 Value::Text(s) => Ok((s, v)),
450 _ => Err(Error::MalformedStringMap),
451 })
452 .collect(),
453 _ => Err(Error::InvalidFormatFor { key: desc.into() }),
454 }
455}
456
457impl TryFrom<&str> for HealthCert {
458 type Error = Error;
459
460 fn try_from(data: &str) -> std::result::Result<Self, Self::Error> {
461 const HCID: &str = "HC1:";
462
463 if !data.starts_with(HCID) {
464 return Err(Error::MissingHCID);
465 }
466
467 let defl = base45::decode(data[HCID.len()..].trim())?;
468
469 let mut dec = ZlibDecoder::new(&defl as &[u8]);
470
471 let mut data = Vec::new();
472 dec.read_to_end(&mut data)?;
473
474 let cwt = ciborium::de::from_reader(&data[..])?;
475
476 let Cwt(cwt_arr) = cwt;
477
478 if cwt_arr.len() != 4 {
479 return Err(Error::MalformedCWT);
480 }
481
482 let protected_properties: RawHeader = match &cwt_arr[0] {
483 Value::Bytes(_bys) => ciborium::de::from_reader(&_bys[..])?,
484 _ => {
485 return Err(Error::InvalidFormatFor {
486 key: "protected properties".into(),
487 })
488 }
489 };
490
491 let unprotected_properties = match &cwt_arr[1] {
492 Value::Map(map) => map,
493 _ => {
494 return Err(Error::InvalidFormatFor {
495 key: "unprotected properties".into(),
496 })
497 }
498 };
499
500 let RawCert(mut cert_map) = match &cwt_arr[2] {
501 Value::Bytes(bys) => ciborium::de::from_reader(&bys[..])?,
502 _ => {
503 return Err(Error::InvalidFormatFor {
504 key: "root cert".into(),
505 })
506 }
507 };
508
509 let some_issuer = if let Some(iss_v) = cert_map.remove(&1) {
510 match iss_v {
511 Value::Text(iss) => Some(iss),
512 _ => {
513 return Err(Error::InvalidFormatFor {
514 key: "issuing country".into(),
515 })
516 }
517 }
518 } else {
519 None
520 };
521
522 let expires = match cert_map
523 .remove(&4isize)
524 .ok_or_else(|| Error::MissingKey("expiration timestamp".into()))?
525 {
526 Value::Integer(ts) => Utc.timestamp(i128::from(ts) as i64, 0),
527 _ => {
528 return Err(Error::InvalidFormatFor {
529 key: "expiration timestamp".into(),
530 })
531 }
532 };
533
534 let created = match cert_map
535 .remove(&6isize)
536 .ok_or_else(|| Error::MissingKey("issue timestamp".into()))?
537 {
538 Value::Integer(ts) => Utc.timestamp(i128::from(ts) as i64, 0),
539 _ => {
540 return Err(Error::InvalidFormatFor {
541 key: "issue timestamp".into(),
542 })
543 }
544 };
545
546 let hcerts = match cert_map
547 .remove(&-260isize)
548 .ok_or_else(|| Error::MissingKey("hcert".into()))?
549 {
550 Value::Map(hcmap) => hcmap
551 .into_iter()
552 .map(|(_, v)| to_strmap("hcert", v))
553 .collect::<Result<Vec<_>>>()?,
554 _ => {
555 return Err(Error::InvalidFormatFor {
556 key: "hcert".into(),
557 })
558 }
559 };
560
561 let passes = hcerts
562 .into_iter()
563 .map(GreenPass::try_from)
564 .collect::<Result<Vec<_>>>()?;
565
566 let signature = match &cwt_arr[3] {
567 Value::Bytes(bys) => bys.clone(),
568 _ => {
569 return Err(Error::InvalidFormatFor {
570 key: "signature".into(),
571 })
572 }
573 };
574
575 let mut protected_properties = protected_properties.0;
576 let kid = unprotected_properties
580 .iter()
581 .find(|&(key, _)| key == &Value::Integer(ciborium::value::Integer::from(4isize)))
582 .ok_or_else(|| Error::MissingKey("KID".into()))
583 .and_then(|kid| {
584 if let (_, Value::Bytes(bys)) = kid {
585 Ok(bys.clone())
586 } else {
587 Err(Error::InvalidFormatFor { key: "KID".into() })
588 }
589 });
590
591 let kid = kid.or_else(|_| {
593 match protected_properties
594 .remove(&4isize)
595 .ok_or(Error::MissingKey("KID".into()))
596 {
597 Ok(Value::Bytes(bys)) => Ok(bys),
598 _ => Err(Error::InvalidFormatFor { key: "KID".into() }),
599 }
600 })?;
601
602 let algorithm: i128 = match protected_properties
603 .remove(&1isize)
604 .ok_or_else(|| Error::MissingKey("algorithm".into()))?
605 {
606 Value::Integer(i) => i.into(),
607 _ => {
608 return Err(Error::InvalidFormatFor {
609 key: "algorithm".into(),
610 })
611 }
612 };
613
614 let signature = Signature {
615 kid,
616 algorithm,
617 signature,
618 };
619
620 Ok(HealthCert {
621 some_issuer,
622 created,
623 expires,
624 passes,
625 signature,
626 })
627 }
628}
629
630pub fn parse(data: &str) -> Result<HealthCert> {
648 HealthCert::try_from(data)
649}