dgc 0.0.7

A parser and validator for the EU Digital Green Certificate (dgc) a.k.a. greenpass
Documentation
use ring::digest;
use std::{collections::HashMap, convert::TryFrom, ops::Deref};
use thiserror::Error;

/// Error struct that represents all the possible errors that can occur
/// while trying to parse a public key.
#[derive(Error, Debug)]
pub enum KeyParseError {
    /// Failed to decode the string using base64
    #[error("Cannot decode base64 data: {0}")]
    Base64DecodeError(#[from] base64::DecodeError),
    /// The encoded string did not contain a valid X509 key
    #[error("Failed to parse X509 data: {0}")]
    X509ParseError(#[from] x509_parser::nom::Err<x509_parser::error::X509Error>),
    /// The given certificate did not contain a public key
    #[error("Cannot extract a valid public key from certificate: {0}")]
    PublicKeyParseError(String),
}

/// Error struct that represents all the possible errors that can occur
/// while trying to create a trustlist from a given JSON payload.
#[derive(Error, Debug)]
pub enum TrustListFromJsonError {
    /// The given JSON is not an object
    #[error("The given JSON is not an object")]
    InvalidRootType,
    /// The given key is not associated to an object
    #[error("Key '{0}' is not an object")]
    KeyIsNotObject(String),
    /// A given key does not specify a public key algorithm
    #[error("Key '{0}' does not contain 'publicKeyAlgorithm'")]
    MissingPublicKeyAlgorithm(String),
    /// A given key does not specifies the public key algorithm as a string
    #[error("'publicKeyAlgorithm' for key '{0}' is not a string")]
    InvalidPublicKeyAlgorithm(String),
    /// A given key does not specify the name of the public key algorithm
    #[error("Key '{0}' does not contain 'publicKeyAlgorithm.name'")]
    MissingPublicKeyAlgorithmName(String),
    /// A given key does not specifies the public key algorithm name as a string
    #[error("'publicKeyAlgorithm.name' for key '{0}' is not a string")]
    InvalidPublicKeyAlgorithmName(String),
    /// A given key specifies a public key algorithm name that is not supported
    // TODO: add RSA here once #2 is done
    #[error("Key '{0}' 'publicKeyAlgorithm.name' is '{1}' where only 'ECDSA' is supported")]
    UnsupportedPublicKeyAlgorithmName(String, String),
    /// A given key does not contain 'publicKeyAlgorithm.namedCurve'
    // TODO: We might have to revisit these errors if we want to support RSA (see #2)
    #[error("Key '{0}' does not contain 'publicKeyAlgorithm.namedCurve'")]
    MissingPublicKeyAlgorithmCurve(String),
    /// A given key does not express 'publicKeyAlgorithm.namedCurve' as a string
    #[error("'publicKeyAlgorithm.namedCurve' for key '{0}' is not a string")]
    InvalidPublicKeyAlgorithmCurve(String),
    /// A given key is using a 'publicKeyAlgorithm.namedCurve' that is not supported
    #[error("Key '{0}' 'publicKeyAlgorithm.namedCurve' is '{0}' where only 'P-256' is supported")]
    UnsupportedPublicKeyAlgorithmCurve(String, String),
    /// A given key does not contain 'publicKeyPem'
    #[error("Key '{0}' does not contain 'publicKeyPem'")]
    MissingPublicKeyPem(String),
    /// A given key does not express 'publicKeyPem' as a string
    #[error("'publicKeyPem' for key '{0}' is not a string")]
    InvalidPublicKeyPem(String),
    /// A given key have a 'publicKeyPem' which cannot be correctly decoded using base64
    #[error("'publicKeyPem' for key '{0}' could not be decoded: {1}")]
    PublicKeyPemDecodeError(String, #[source] base64::DecodeError),
    /// The kid of a given key could not be decoded using base64
    #[error("Cannot base64 decode key '{0}': {1}")]
    KidBase64DecodeError(String, #[source] base64::DecodeError),
    /// A given key provided a public key that could not be parsed
    #[error("Cannot parse public key for key '{0}': {1}")]
    KeyParseError(String, #[source] KeyParseError),
}

/// Struct used to index all the available public keys which
/// can be used to validate the signature on a given certificate.
///
/// Keys are indexed by their `kid` (Key ID) which is an arbitrary sequence of bytes.
#[derive(Debug)]
pub struct TrustList {
    keys: HashMap<Vec<u8>, Vec<u8>>,
}

impl TrustList {
    /// Returns the public key with the specified key identifier or
    /// [`None`] if there is no key with that key ID.
    pub fn get_key(&self, kid: &[u8]) -> Option<&[u8]> {
        self.keys.get(kid).map(Vec::deref)
    }

    /// Creates a new empty trustlist
    pub fn new() -> Self {
        TrustList {
            keys: HashMap::new(),
        }
    }

    /// Adds a raw public key to the [`TrustList`]
    pub fn add(&mut self, kid: &[u8], key: Vec<u8>) {
        self.keys.insert(kid.to_vec(), key);
    }

    /// Adds a public key from a X509 certificate encoded in Base64 (certificate data only, without delimiters).
    /// When using a certificate the KID are the first 8 bytes of the SHA256
    /// hash of the certificate data ([source](https://github.com/eu-digital-green-certificates/dgc-testdata/issues/76#issuecomment-841037329)).
    pub fn add_key_from_certificate(
        &mut self,
        base64_x509_cert: &str,
    ) -> Result<(), KeyParseError> {
        let decoded = base64::decode(base64_x509_cert)?;
        let certificate_digest = digest::digest(&digest::SHA256, &decoded);
        let kid = &certificate_digest.as_ref()[0..8];

        let certificate = x509_parser::parse_x509_certificate(&decoded)?.1;
        // TODO: find a way to do this, or make it a warning
        // certificate
        //     .verify_signature(None)
        //     .map_err(|e| KeyParseError::PublicKeyParseError(e.to_string()))?;
        let raw_key = certificate
            .tbs_certificate
            .subject_pki
            .subject_public_key
            .data;

        self.keys.insert(kid.to_vec(), raw_key.to_owned());

        Ok(())
    }

    /// Adds a base64 encoded raw key with the specified kid to the trust list
    pub fn add_key_from_base64(
        &mut self,
        kid: &[u8],
        base64_key: &str,
    ) -> Result<(), KeyParseError> {
        let raw_data = base64::decode(base64_key)?;
        self.keys.insert(kid.to_vec(), raw_data);
        Ok(())
    }
}

impl Default for TrustList {
    fn default() -> Self {
        TrustList::new()
    }
}

impl TryFrom<serde_json::Value> for TrustList {
    type Error = TrustListFromJsonError;

    fn try_from(data: serde_json::Value) -> Result<Self, Self::Error> {
        use TrustListFromJsonError::*;

        let mut trustlist = TrustList::default();

        let keys = data.as_object().ok_or(InvalidRootType)?;

        for (kid, keydef) in keys {
            let keydef = keydef
                .as_object()
                .ok_or_else(|| KeyIsNotObject(kid.clone()))?;

            // "publicKeyAlgorithm" must be an object that contains
            // "name" == "ECDSA" and "namedCurve" == "P-256"
            let pub_key_alg = keydef
                .get("publicKeyAlgorithm")
                .ok_or_else(|| MissingPublicKeyAlgorithm(kid.clone()))?
                .as_object()
                .ok_or_else(|| InvalidPublicKeyAlgorithm(kid.clone()))?;

            let pub_key_alg_name = pub_key_alg
                .get("name")
                .ok_or_else(|| MissingPublicKeyAlgorithmName(kid.clone()))?
                .as_str()
                .ok_or_else(|| InvalidPublicKeyAlgorithmName(kid.clone()))?;

            if pub_key_alg_name != "ECDSA" {
                return Err(UnsupportedPublicKeyAlgorithmName(
                    kid.clone(),
                    String::from(pub_key_alg_name),
                ));
            }

            let named_curve = pub_key_alg
                .get("namedCurve")
                .ok_or_else(|| MissingPublicKeyAlgorithmCurve(kid.clone()))?
                .as_str()
                .ok_or_else(|| InvalidPublicKeyAlgorithmCurve(kid.clone()))?;

            if named_curve != "P-256" {
                return Err(UnsupportedPublicKeyAlgorithmCurve(
                    kid.clone(),
                    String::from(named_curve),
                ));
            }

            // "publicKeyPem" must exist and be a base64 encoded bynary string
            let base64_der_public_key = keydef
                .get("publicKeyPem")
                .ok_or_else(|| MissingPublicKeyPem(kid.clone()))?
                .as_str()
                .ok_or_else(|| InvalidPublicKeyPem(kid.clone()))?;

            let decoded_kid =
                base64::decode(kid).map_err(|e| KidBase64DecodeError(kid.clone(), e))?;
            trustlist
                .add_key_from_base64(decoded_kid.as_slice(), base64_der_public_key)
                .map_err(|e| KeyParseError(kid.clone(), e))?;
        }

        Ok(trustlist)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;
    use std::convert::TryInto;

    #[test]
    fn it_adds_a_public_key_from_a_certificate() {
        let base64_x509_cert = "MIIEHjCCAgagAwIBAgIUM5lJeGCHoRF1raR6cbZqDV4vPA8wDQYJKoZIhvcNAQELBQAwTjELMAkGA1UEBhMCSVQxHzAdBgNVBAoMFk1pbmlzdGVybyBkZWxsYSBTYWx1dGUxHjAcBgNVBAMMFUl0YWx5IERHQyBDU0NBIFRFU1QgMTAeFw0yMTA1MDcxNzAyMTZaFw0yMzA1MDgxNzAyMTZaME0xCzAJBgNVBAYTAklUMR8wHQYDVQQKDBZNaW5pc3Rlcm8gZGVsbGEgU2FsdXRlMR0wGwYDVQQDDBRJdGFseSBER0MgRFNDIFRFU1QgMTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABDSp7t86JxAmjZFobmmu0wkii53snRuwqVWe3/g/wVz9i306XA5iXpHkRPZVUkSZmYhutMDrheg6sfwMRdql3aajgb8wgbwwHwYDVR0jBBgwFoAUS2iy4oMAoxUY87nZRidUqYg9yyMwagYDVR0fBGMwYTBfoF2gW4ZZbGRhcDovL2NhZHMuZGdjLmdvdi5pdC9DTj1JdGFseSUyMERHQyUyMENTQ0ElMjBURVNUJTIwMSxPPU1pbmlzdGVybyUyMGRlbGxhJTIwU2FsdXRlLEM9SVQwHQYDVR0OBBYEFNSEwjzu61pAMqliNhS9vzGJFqFFMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsFAAOCAgEAIF74yHgzCGdor5MaqYSvkS5aog5+7u52TGggiPl78QAmIpjPO5qcYpJZVf6AoL4MpveEI/iuCUVQxBzYqlLACjSbZEbtTBPSzuhfvsf9T3MUq5cu10lkHKbFgApUDjrMUnG9SMqmQU2Cv5S4t94ec2iLmokXmhYP/JojRXt1ZMZlsw/8/lRJ8vqPUorJ/fMvOLWDE/fDxNhh3uK5UHBhRXCT8MBep4cgt9cuT9O4w1JcejSr5nsEfeo8u9Pb/h6MnmxpBSq3JbnjONVK5ak7iwCkLr5PMk09ncqG+/8Kq+qTjNC76IetS9ST6bWzTZILX4BD1BL8bHsFGgIeeCO0GqalFZAsbapnaB+36HVUZVDYOoA+VraIWECNxXViikZdjQONaeWDVhCxZ/vBl1/KLAdX3OPxRwl/jHLnaSXeqr/zYf9a8UqFrpadT0tQff/q3yH5hJRJM0P6Yp5CPIEArJRW6ovDBbp3DVF2GyAI1lFA2Trs798NN6qf7SkuySz5HSzm53g6JsLY/HLzdwJPYLObD7U+x37n+DDi4Wa6vM5xdC7FZ5IyWXuT1oAa9yM4h6nW3UvC+wNUusW6adqqtdd4F1gHPjCf5lpW5Ye1bdLUmO7TGlePmbOkzEB08Mlc6atl/vkx/crfl4dq1LZivLgPBwDzE8arIk0f2vCx1+4=";
        let mut trustlist = TrustList::new();
        trustlist
            .add_key_from_certificate(base64_x509_cert)
            .unwrap();
    }

    #[test]
    fn it_adds_a_public_key() {
        let base64_der_public_key = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEt5hwD0cJUB5TeQIAaE7nLjeef0vV5mamR30kjErGOcReGe37dDrmFAeOqILajQTiBXzcnPaMxWUd9SK9ZRexzQ==";
        let mut trustlist = TrustList::new();
        trustlist
            .add_key_from_base64(&[1, 2, 3], base64_der_public_key)
            .unwrap();
        assert_eq!(trustlist.keys.len(), 1);
        assert!(trustlist.get_key(&[1, 2, 3]).is_some())
    }

    #[test]
    fn it_creates_a_trustlist_from_json() {
        let data = json!({
          "25QCxBrBJvA=": {
            "serialNumber": "3d1f6391763b08f1",
            "subject": "C=HR, O=AKD d.o.o., CN=Croatia DGC DS 001",
            "issuer": "C=HR, O=AKD d.o.o., CN=Croatia DGC CSCA",
            "notBefore": "2021-05-20T13:17:46.000Z",
            "notAfter": "2023-05-20T13:17:45.000Z",
            "signatureAlgorithm": "ECDSA",
            "fingerprint": "678a9b63d73aa4e82ce35b455fbe8363feee98c4",
            "publicKeyAlgorithm": {
              "hash": {
                "name": "SHA-256"
              },
              "name": "ECDSA",
              "namedCurve": "P-256"
            },
            "publicKeyPem": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEt5hwD0cJUB5TeQIAaE7nLjeef0vV5mamR30kjErGOcReGe37dDrmFAeOqILajQTiBXzcnPaMxWUd9SK9ZRexzQ=="
          },
          "NAyCKly+hCg=": {
            "serialNumber": "01",
            "subject": "C=DK, O=The Danish Health Data Authority, OU=The Danish Health Data Authority, CN=PROD_DSC_DGC_DK_01, E=kontakt@sundhedsdata.dk",
            "issuer": "C=DK, O=The Danish Health Data Authority, OU=The Danish Health Data Authority, CN=PROD_CSCA_DGC_DK_01, E=kontakt@sundhedsdata.dk",
            "notBefore": "2021-05-19T09:47:25.000Z",
            "notAfter": "2023-05-20T09:47:25.000Z",
            "signatureAlgorithm": "ECDSA",
            "fingerprint": "a6bbf6b1a1aca900a7c0b99e6e831272dff23e9e",
            "publicKeyAlgorithm": {
              "hash": {
                "name": "SHA-256"
              },
              "name": "ECDSA",
              "namedCurve": "P-256"
            },
            "publicKeyPem": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBmdgY/VORsecXxY/0xNNOzoJNRaVnMMmHs5jiXrGvaDOy1jzDUOyvR++Jxgf0+YuGyp5/UAY0QIh75b+JQnlHA=="
          }
        });

        let trustlist: TrustList = data.try_into().unwrap();
        assert_eq!(trustlist.keys.len(), 2);
        let first_key = trustlist.get_key(&base64::decode("25QCxBrBJvA=").unwrap());
        assert!(first_key.is_some());
    }
}