greenpass/
lib.rs

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/// Error type that represents every possible error condition encountered while loading a certificate
46#[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
90// does not work for Tag, which is not needed
91macro_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/// Structure that represents a Green Pass entry.
138#[derive(Debug, PartialEq)]
139pub struct GreenPass {
140    /// Date of birth
141    pub date_of_birth: String, // dob can have weird formats
142
143    /// Family name
144    pub surname: String, // nam/fn
145
146    /// First name
147    pub givenname: String, // nam/gn
148
149    /// Family name in standardized form (see docs)
150    pub std_surname: String, // nam/fnt
151
152    /// First name in standardized form
153    pub std_givenname: String, // nam/gnt
154
155    /// Document version
156    pub ver: String, // ver
157
158    /// Attestation of immunity from an illness due to vaccination, recovery or a negative test
159    pub entries: Vec<CertInfo>, // [v | t | r]
160}
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/// Represents the signature and signature metadata for a [HealthCert].
221#[derive(Debug, PartialEq)]
222pub struct Signature {
223    /// Key id
224    pub kid: Vec<u8>,
225
226    /// Algorithm used for signing
227    pub algorithm: i128,
228
229    /// Raw signature
230    pub signature: Vec<u8>,
231}
232
233/// Represents the whole certificate blob
234#[derive(Debug, PartialEq)]
235pub struct HealthCert {
236    // Member country that issued the bundle (might be missing)
237    pub some_issuer: Option<String>,
238
239    /// Bundle creation timestamp
240    pub created: DateTime<Utc>,
241
242    /// Bundle expiration timestamp
243    pub expires: DateTime<Utc>,
244
245    /// List of passes contained in this bundle
246    pub passes: Vec<GreenPass>,
247
248    /// Raw signature
249    pub signature: Signature,
250}
251
252/// Attests the full recovery from a given disease
253#[derive(Debug, PartialEq)]
254pub struct Recovery {
255    /// Certificate ID
256    pub cert_id: String, // ci
257
258    /// Member State where the test was performed
259    pub country: String, // co
260
261    /// Date of diagnosis
262    pub diagnosed: NaiveDate, // fr
263
264    /// String that identifies the contracted disease
265    pub disease: String, // tg
266
267    /// Issuing entity
268    pub issuer: String, // is
269
270    /// Recovery attestation validity start date
271    pub valid_from: NaiveDate, // df
272
273    /// Recovery attestation validity expire date
274    pub valid_until: NaiveDate, // du
275}
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/// Attests that a test for a given disease has been conducted.
306#[derive(Debug, PartialEq)]
307pub struct Test {
308    /// Certificate ID
309    pub cert_id: String, // ci
310
311    /// Date and time when samples where collected
312    pub collect_ts: DateTime<FixedOffset>, // sc
313
314    /// Member State where the test was performed
315    pub country: String, // co
316
317    /// Target disease
318    pub disease: String, // tg
319
320    /// Issuing entity
321    pub issuer: String, // is
322
323    /// Name and identifier of the used testing technology
324    pub name: TestName, // nm | ma
325
326    /// Test result, as defined in  SNOMED CT GPS
327    pub result: String, // tr
328
329    /// Coded string value identifying the testing method
330    pub test_type: String, // tt
331
332    /// Name of the centre that conducted the test
333    pub testing_centre: String, // tc
334}
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/// Attests that an individual has been vaccinated for a given disease.
377#[derive(Debug, PartialEq)]
378pub struct Vaccine {
379    /// Certificate ID
380    pub cert_id: String, // ci
381
382    /// Vaccination country
383    pub country: String, // co
384
385    /// Vaccination date
386    pub date: NaiveDate, // dt
387
388    /// Targeted disease
389    pub disease: String, // tg
390
391    /// Number of administered doses
392    pub dose_number: usize, // dn
393
394    /// Total number of doses required by the administered vaccine
395    pub dose_total: usize, // sd
396
397    /// Issuing entity
398    pub issuer: String, // is
399
400    /// EUDCC Gateway market authorization identifier
401    pub market_auth: String, // ma
402
403    /// Product identifier as defined in EUDCC Gateway
404    pub product: String, // mp
405
406    /// Type of vaccine or prophylaxis used as defined in EUDCC Gateway
407    pub prophylaxis_kind: String, // vp
408}
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        // The KID can be stored in the unprotected or the protected properties
577        // See https://ec.europa.eu/health/system/files/2021-04/digital-green-certificates_v3_en_0.pdf on page 7
578        // Try to get the KID from the unprotected properties
579        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        // If the unprotected properties don't contain a KID, try with the protected properties
592        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
630/// Parses a Base45 CBOR Web Token containing a EU Health Certificate. No signature validation is currently performed by
631/// this crate.
632///
633/// ```no_run
634/// use std::{error::Error, fs::read_to_string};
635///
636/// fn main() -> Result<(), Box<dyn Error>> {
637///     // Read a Base45 payload extracted from a QR code
638///     let buf_str = read_to_string("base45_file.txt")?;
639///
640///     let health_cert = greenpass::parse(&buf_str)?;
641///
642///     println!("{:#?}", health_cert);
643///     
644///     Ok(())
645/// }
646/// ```
647pub fn parse(data: &str) -> Result<HealthCert> {
648    HealthCert::try_from(data)
649}