dgc 0.0.7

A parser and validator for the EU Digital Green Certificate (dgc) a.k.a. greenpass
Documentation
use crate::DgcContainer;
use ciborium::{
    ser::into_writer,
    value::{Integer, Value},
};
use std::iter::FromIterator;
use std::{
    convert::{TryFrom, TryInto},
    ops::Not,
};
use thiserror::Error;

const COSE_SIGN1_CBOR_TAG: u64 = 18;
const CBOR_WEB_TOKEN_TAG: u64 = 61;
const COSE_HEADER_KEY_KID: i128 = 4;
const COSE_HEADER_KEY_ALG: i128 = 1;
/// COSE key for ECDSA w/ SHA-256
const COSE_ES256: i128 = -7;
/// COSE key for RSASSA-PSS w/ SHA-256
const COSE_PS256: i128 = -37;

/// An enum representing all the possible errors that can occur while trying
/// to parse data representing a CWT ([CBOR Web Token](https://datatracker.ietf.org/doc/html/rfc8392)).
#[derive(Error, Debug)]
pub enum CwtParseError {
    /// Cannot parse the data as CBOR
    #[error("Cannot parse the data as CBOR: {0}")]
    CborError(#[from] ciborium::de::Error<std::io::Error>),
    /// The root value is not a tag
    #[error("The root value is not a tag")]
    InvalidRootValue,
    /// The root tag is invalid
    #[error(
        "Expected COSE_SIGN1_CBOR_TAG ({}) or CBOR_WEB_TOKEN_TAG ({}). Found: {0}",
        COSE_SIGN1_CBOR_TAG,
        CBOR_WEB_TOKEN_TAG
    )]
    InvalidTag(u64),
    /// The main CBOR object is not an array
    #[error("The main CBOR object is not an array")]
    InvalidParts,
    /// The main CBOR array does not contain 4 parts
    #[error("The main CBOR array does not contain 4 parts. {0} parts found")]
    InvalidPartsCount(usize),
    /// The unprotected header section is not a CBOR map or an emtpy sequence of bytes
    #[error("The unprotected header section is not a CBOR map or an emtpy sequence of bytes")]
    MalformedUnProtectedHeader,
    /// The protected header section is not a binary string
    #[error("The protected header section is not a binary string")]
    ProtectedHeaderNotBinary,
    /// The protected header section is not valid CBOR-encoded data
    #[error("The protected header section is not valid CBOR-encoded data")]
    ProtectedHeaderNotValidCbor,
    /// The protected header section does not contain key-value pairs
    #[error("The protected header section does not contain key-value pairs")]
    ProtectedHeaderNotMap,
    /// The payload section is not a binary string
    #[error("The payload section is not a binary string")]
    PayloadNotBinary,
    /// Cannot deserialize the payload
    #[error("Cannot deserialize payload: {0}")]
    InvalidPayload(#[source] ciborium::de::Error<std::io::Error>),
    /// The signature section is not a binary string
    #[error("The signature section is not a binary string")]
    SignatureNotBinary,
}

/// An enum representing the supported signing verification algorithms.
#[derive(Debug, PartialEq, Eq)]
pub enum EcAlg {
    /// ECDSA w/ SHA-256
    ///
    /// [Elliptic Curve Digital Signature Algorithm][ecdsa] using the
    /// [Secure Hash Algorithm 2][sha2] hash function
    /// with digest size of 256 bits.
    ///
    /// [ecdsa]: https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm
    /// [sha2]: https://en.wikipedia.org/wiki/SHA-2
    Es256,
    /// RSASSA-PSS w/ SHA-256
    ///
    /// [Rivest-Shamir-Adleman][rsa] signing algorithm using the
    /// [Secure Hash Algorithm 2][sha2] hash function
    /// with digest size of 256 bits.
    ///
    /// [rsa]: https://en.wikipedia.org/wiki/RSA_(cryptosystem)
    /// [sha2]: https://en.wikipedia.org/wiki/SHA-2
    Ps256,
    /// Unknown algorithm
    ///
    /// The value is the COSE algorithm identifier defined by the IANA,
    /// a complete list can be found [here](https://www.iana.org/assignments/cose/cose.xhtml)
    Unknown(i128),
}

impl From<Integer> for EcAlg {
    fn from(i: Integer) -> Self {
        let u: i128 = i.into();
        match u {
            COSE_ES256 => EcAlg::Es256,
            COSE_PS256 => EcAlg::Ps256,
            _ => EcAlg::Unknown(u),
        }
    }
}

/// The CWT header object.
///
/// This is a simplification of the actual CWT structure. In fact,
/// in the CWT spec there are 2 headers (protected header and unprotected header).
///
/// For the sake of DGC, we only need to extract `kid` and `alg` from either of them,
/// so we use this struct to keep those values.
#[derive(Debug)]
pub struct CwtHeader {
    /// The Key ID used for signing the certificate
    pub kid: Option<Vec<u8>>,
    /// The signature algorithm used to sign the certificate
    pub alg: Option<EcAlg>,
}

impl CwtHeader {
    fn new() -> Self {
        Self {
            kid: None,
            alg: None,
        }
    }

    fn kid(&mut self, kid: Vec<u8>) {
        self.kid = Some(kid);
    }

    fn alg(&mut self, alg: EcAlg) {
        self.alg = Some(alg);
    }
}

impl FromIterator<(Value, Value)> for CwtHeader {
    fn from_iter<T: IntoIterator<Item = (Value, Value)>>(iter: T) -> Self {
        // permissive parsing. We don't want to fail if we can't decode the header
        let mut header = CwtHeader::new();
        // tries to find kid and alg and apply them to the header before returning it
        for (key, val) in iter {
            if let Value::Integer(k) = key {
                let k: i128 = k.into();
                if k == COSE_HEADER_KEY_KID {
                    // found kid
                    if let Value::Bytes(kid) = val {
                        header.kid(kid);
                    }
                } else if k == COSE_HEADER_KEY_ALG {
                    // found alg
                    if let Value::Integer(raw_alg) = val {
                        let alg: EcAlg = raw_alg.into();
                        header.alg(alg);
                    }
                }
            }
        }
        header
    }
}

/// A representation of a CWT ([CBOR Web Token](https://datatracker.ietf.org/doc/html/rfc8392)).
///
/// In the context of DGC only a portion of the original CWT specification is actually used
/// ([COSE_Sign1](https://datatracker.ietf.org/doc/html/rfc8152#section-4.2)) so this module
/// is limited to implementing exclusively that portion.
#[derive(Debug)]
pub struct Cwt {
    header_protected_raw: Vec<u8>,
    payload_raw: Vec<u8>,
    /// A simplified representation of the original CWT headers (protected + unprotected)
    ///
    /// Stores only the `kid` and `alg`
    pub header: CwtHeader,
    /// The CWT payload parse as a DgcContainer
    pub payload: DgcContainer,
    /// The raw bytes of the signature
    pub signature: Vec<u8>,
}

impl Cwt {
    /// Creates the [sig structure](https://datatracker.ietf.org/doc/html/rfc8152#section-4.4) needed to be able
    /// to verify the signature against a public key.
    pub fn make_sig_structure(&self) -> Vec<u8> {
        let sig_structure_cbor = Value::Array(vec![
            Value::Text(String::from("Signature1")), // context of the signature
            Value::Bytes(self.header_protected_raw.clone()), // protected attributes from the body structure
            Value::Bytes(vec![]), // protected attributes from the application (these are not used in hcert so we keep them empty as per spec)
            Value::Bytes(self.payload_raw.clone()),
        ]);
        let mut sig_structure: Vec<u8> = vec![];
        into_writer(&sig_structure_cbor, &mut sig_structure).unwrap();
        sig_structure
    }
}

/// Extends `ciborium::value::Value` with some useful methods.
/// TODO: send a PR to `ciborium` to have these utilities out of the box.
trait ValueExt: Sized {
    fn into_tag(self) -> Result<(u64, Box<Value>), Self>;
    fn into_array(self) -> Result<Vec<Value>, Self>;
    fn into_bytes(self) -> Result<Vec<u8>, Self>;
}

impl ValueExt for Value {
    fn into_tag(self) -> Result<(u64, Box<Value>), Self> {
        match self {
            Self::Tag(tag, content) => Ok((tag, content)),
            _ => Err(self),
        }
    }

    fn into_array(self) -> Result<Vec<Value>, Self> {
        match self {
            Self::Array(array) => Ok(array),
            _ => Err(self),
        }
    }

    fn into_bytes(self) -> Result<Vec<u8>, Self> {
        match self {
            Self::Bytes(bytes) => Ok(bytes),
            _ => Err(self),
        }
    }
}

impl TryFrom<&[u8]> for Cwt {
    type Error = CwtParseError;

    fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
        use CwtParseError::*;

        let cwt_content = match ciborium::de::from_reader(data)? {
            Value::Tag(tag_id, content) if tag_id == CBOR_WEB_TOKEN_TAG => *content,
            cwt => cwt,
        };
        let cwt_content = match cwt_content.into_tag() {
            Ok((COSE_SIGN1_CBOR_TAG, content)) => *content,
            Ok((tag_id, _)) => return Err(InvalidTag(tag_id)),
            Err(cwt) => cwt,
        };

        let parts = cwt_content.into_array().map_err(|_| InvalidParts)?;

        let parts_len = parts.len();
        let [header_protected_raw, unprotected_header, payload_raw, signature]: [Value; 4] =
            parts.try_into().map_err(|_| InvalidPartsCount(parts_len))?;

        let header_protected_raw = header_protected_raw
            .into_bytes()
            .map_err(|_| ProtectedHeaderNotBinary)?;
        let payload_raw = payload_raw.into_bytes().map_err(|_| PayloadNotBinary)?;
        let signature = signature.into_bytes().map_err(|_| SignatureNotBinary)?;

        // unprotected header must be a cbor map or an empty sequence of bytes
        let unprotected_header = match unprotected_header {
            Value::Map(values) => Some(values),
            Value::Bytes(values) if values.is_empty() => Some(Vec::new()),
            _ => None,
        }
        .ok_or(MalformedUnProtectedHeader)?;

        // protected header is a bytes sequence.
        // If the length of the sequence is 0 we assume it represents an empty map.
        // Otherwise we decode the binary string as a CBOR value and we make sure it represents a map.
        let protected_header_values = header_protected_raw
            .is_empty()
            .not()
            .then(|| {
                let value = ciborium::de::from_reader(header_protected_raw.as_slice())
                    .map_err(|_| ProtectedHeaderNotValidCbor)?;

                match value {
                    Value::Map(map) => Ok(map),
                    _ => Err(ProtectedHeaderNotMap),
                }
            })
            .transpose()?
            .unwrap_or_default();

        // Take data from unprotected header first, then from the protected one
        let header: CwtHeader = unprotected_header
            .into_iter()
            .chain(protected_header_values)
            .collect();

        let payload: DgcContainer =
            ciborium::de::from_reader(payload_raw.as_slice()).map_err(InvalidPayload)?;

        Ok(Cwt {
            header_protected_raw,
            payload_raw,
            header,
            payload,
            signature,
        })
    }
}

impl TryFrom<Vec<u8>> for Cwt {
    type Error = CwtParseError;

    fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
        data.as_slice().try_into()
    }
}

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

    // test data from https://dgc.a-sit.at/ehn/generate
    use super::*;

    #[test]
    fn it_parses_cose_data() {
        let raw_hex_cose_data = "d2844da204481c10ebbbc49f78310126a0590111a4041a61657980061a6162d90001624145390103a101a4617481a862736374323032312d31302d30395431323a30333a31325a627474684c50363436342d3462746376416c686f736e204f6e6520446179205375726765727962636f624145626369782955524e3a555643493a56313a41453a384b5354305248303537484938584b57334d384b324e41443036626973781f4d696e6973747279206f66204865616c746820262050726576656e74696f6e6274676938343035333930303662747269323630343135303030636e616da463666e7465424c414b4562666e65424c414b4563676e7466414c53544f4e62676e66414c53544f4e6376657265312e332e3063646f626a313939302d30312d3031584034fc1cee3c4875c18350d24ccd24dd67ce1bda84f5db6b26b4b8a97c8336e159294859924afa7894a45a5af07a8cf536a36be67912d79f5a93540b86bb7377fb";
        let expected_sig_structure = "846a5369676e6174757265314da204481c10ebbbc49f7831012640590111a4041a61657980061a6162d90001624145390103a101a4617481a862736374323032312d31302d30395431323a30333a31325a627474684c50363436342d3462746376416c686f736e204f6e6520446179205375726765727962636f624145626369782955524e3a555643493a56313a41453a384b5354305248303537484938584b57334d384b324e41443036626973781f4d696e6973747279206f66204865616c746820262050726576656e74696f6e6274676938343035333930303662747269323630343135303030636e616da463666e7465424c414b4562666e65424c414b4563676e7466414c53544f4e62676e66414c53544f4e6376657265312e332e3063646f626a313939302d30312d3031";
        let expected_kid: Vec<u8> = vec![28, 16, 235, 187, 196, 159, 120, 49];
        let expected_alg = EcAlg::Es256;
        let raw_cose_data = hex::decode(raw_hex_cose_data).unwrap();

        let cwt: Cwt = raw_cose_data.as_slice().try_into().unwrap();

        assert_eq!(Some(expected_kid), cwt.header.kid);
        assert_eq!(Some(expected_alg), cwt.header.alg);
        assert_eq!(
            expected_sig_structure,
            hex::encode(cwt.make_sig_structure())
        );
    }
}