Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Looks up for kid and alg also in unprotected header #25

Merged
merged 6 commits into from
Nov 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ base64 = "0.13.0"
ciborium = "0.2.0"
der-parser = "6.0.0"
inflate = "0.4.5"
ring = "0.16.20"
ring-compat = "0.3.2"
serde = "1.0.130"
serde_json = "1.0.64"
Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
[![Documentation](https://docs.rs/dgc/badge.svg)](https://docs.rs/dgc)
[![dependency status](https://deps.rs/repo/github/rust-italia/dgc/status.svg)](https://deps.rs/repo/github/rust-italia/dgc)

A parser and validator for the EU Digital Green Certificate (dgc) a.k.a. greenpass 📲✅
A parser and validator for the **EU Digital Green Certificate (dgc)** a.k.a. _greenpass_ 📲✅

- **Parses** the text content of a European Digital Green Certificate (dgc or greenpass) and extract the embedded data
- Uses a **Trustlist** of **public keys** and **Elliptic Curve** cryptography to be able to validate the signature of a given certificate
Expand All @@ -23,7 +23,6 @@ A parser and validator for the EU Digital Green Certificate (dgc) a.k.a. greenpa
Current limitations:

- It only supports EC signatures (see [#2](https://github.com/rust-italia/dgc/issues/2))
- It does not support KID in the COSE unprotected header (see [#1](https://github.com/rust-italia/dgc/issues/1))


## Usage
Expand Down
4 changes: 1 addition & 3 deletions examples/validate-signature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@ fn main() {
let raw_certificate_data = "HC1:NCF:603A0T9WTWGSLKC 4K694WJN.0J$6C-7WAB0XK3JCSGA2F3R8PP4V2F35VPP.EY50.FK8ZKO/EZKEZ96LF6/A6..DV%DZJC0/D5UA QELPCG/DYUCHY83UAGVC*JCNF6F463W5KF6VF6IECSHG4KCD3DX47B46IL6646H*6MWEWJDA6A:961A6Q47EM6B$DFOC0R63KCZPCNF6OF63W5$Q6+96/SA5R6NF61G73564KC*KETF6A46.96646B565WEC.D1$CKWEDZC6VCS446$C4WEUPC3JCUIA+ED$.EF$DMWE8$CBJEMVCB445$CBWER.CGPC4WEOPCE8FHZA1+9LZAZM81G72A62+8OG7J09U47AB8V59T%6ZHBO57X48RUIY03XQOK*FZUNM UFY4D5C S3R9UW-2R*4KZJT5M MIM:03RMZNA LKTO34PA.H51966PS0KAP-KLPH.Q6$KSTJ0-G658RL5HR1";
// This is a X509 certificate that contains a Public Key
let signature_certificate = "MIIDujCCAaKgAwIBAgIIKUgZWBL1pnMwDQYJKoZIhvcNAQELBQAwZjELMAkGA1UEBhMCRlIxHTAbBgNVBAoTFElNUFJJTUVSSUUgTkFUSU9OQUxFMR4wHAYDVQQLExVGT1IgVEVTVCBQVVJQT1NFIE9OTFkxGDAWBgNVBAMTD0lOR1JPVVBFIERTYyBDQTAeFw0yMTA2MDIxMjE0MDBaFw0yMTA5MDIxMjE0MDBaMEAxCzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDRVJUSUdOQTEeMBwGA1UEAwwVQ0VSVElHTkEgLSBURVNUIERHQyAxMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAETdygPqv/l6tWFqHFEIEZxfdhtbrBpDgVjmUN4CKOu/EQFwkVVQ/4N0BamwtI0hSnSZP72byk6XqpMErYWRTCbKNdMFswCQYDVR0TBAIwADAdBgNVHQ4EFgQUUjXs7mCY2ZgROQSsw1CN0qM4Zj8wHwYDVR0jBBgwFoAUYLoYTllzE2jOy3VMAuU4OJjOingwDgYDVR0PAQH/BAQDAgeAMA0GCSqGSIb3DQEBCwUAA4ICAQAvxuSBWNOrk+FRIbU42tnwZBllUeNH7cWcrYHV0O+1k3RbpvYa0YE2J0du301/a+0+pqlatR8o8Coe/NFt4/KSu+To+i8uZXiHJn2XrAZgwPqqTvsMUVwFPWhwJpLMCejmU0A8JEhXH7s0BN6orqIH0JKLpl0/MdVviIUksnxPnP2wdCtz6dL5zKhi+Qt8BFr55PL1dvuWxnuFOsKr89MqaexQVe/WvKhG5GXBaJFDbp4USVX9Z8vwp4SfEs5nh0ti0M2fyGrpfPvWWFra/qoRGAUJEPHHPMqZT45c1rXo12+cpme2CYM4rsliQsaqdH462p7YNNI5reBC+WHhzGr9FGq9yZ1gu/yhz1cJxNwE5gsBTWnJmSnRE75lYj1a/GAb+9wfABd1Vx68Fnww3Ngp8lG2T1vEWhwQusj/OmloVbqjJiCi6PcZ1/OSTbx58Zv9ySwDd3QGxPygfMy87FuhT6iWlPv57qTMrgtEjq89J8v3WnReAhp12ru5ehN2Zv0ZkO1Of0H3yxNBsvfHUgpgwsRn4zjLVbkU+a3hr4famOThmB1X0tuikY0mbNtVejPGS0qCgeLgj8ILlUrRtsW4R6WzZdIsz7H9AYnpyZbdMPsa856xBR9s0+AzguJI9kkJxvVcpR//GiXMhs0EdgWj2rouOEPZiFNdWpVRrxv/kw==";
// Key ID of the Public Key embedded in the certificate above
let key_id: Vec<u8> = vec![83, 155, 239, 7, 121, 54, 10, 62];

// We create a new Trustlist (container of "trusted" public keys)
let mut trustlist = dgc::TrustList::default();
// We add the public key in the certificate to the trustlist
trustlist
.add_key_from_certificate(&key_id, signature_certificate)
.add_key_from_certificate(signature_certificate)
.expect("Failed to add key from certificate");

// Now we can validate the signature (this returns)
Expand Down
167 changes: 104 additions & 63 deletions src/cwt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ use ciborium::{
ser::into_writer,
value::{Integer, Value},
};
use std::convert::{TryFrom, TryInto};
use std::iter::FromIterator;
use std::{
convert::{TryFrom, TryInto},
ops::Not,
};
use thiserror::Error;

const COSE_SIGN1_CBOR_TAG: u64 = 18;
Expand All @@ -26,12 +30,14 @@ pub enum CwtParseError {
InvalidParts,
#[error("The main CBOR array does not contain 4 parts. {0} parts found")]
InvalidPartsCount(usize),
#[error("The header section is not a binary string")]
HeaderNotBinary,
#[error("The header section is not valid CBOR-encoded data")]
HeaderNotValidCbor,
#[error("The header section does not contain key-value pairs")]
HeaderNotMap,
#[error("The unprotected header section is not a CBOR map or an emtpy sequence of bytes")]
MalformedUnProtectedHeader,
#[error("The protected header section is not a binary string")]
ProtectedHeaderNotBinary,
#[error("The protected header section is not valid CBOR-encoded data")]
ProtectedHeaderNotValidCbor,
#[error("The protected header section does not contain key-value pairs")]
ProtectedHeaderNotMap,
#[error("The payload section is not a binary string")]
PayloadNotBinary,
#[error("Cannot deserialize payload: {0}")]
Expand Down Expand Up @@ -83,22 +89,22 @@ impl CwtHeader {
}
}

impl From<&[(Value, Value)]> for CwtHeader {
fn from(data: &[(Value, Value)]) -> Self {
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 data.iter() {
if let Some(k) = key.as_integer() {
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 Some(kid) = val.as_bytes() {
header.kid(kid.clone());
if let Value::Bytes(kid) = val {
header.kid(kid);
}
} else if k == COSE_HEADER_KEY_ALG {
// found alg
if let Some(raw_alg) = val.as_integer() {
if let Value::Integer(raw_alg) = val {
let alg: EcAlg = raw_alg.into();
header.alg(alg);
}
Expand All @@ -111,33 +117,14 @@ impl From<&[(Value, Value)]> for CwtHeader {

#[derive(Debug)]
pub struct Cwt {
pub header_protected_raw: Vec<u8>,
pub header_protected: CwtHeader,
pub header_unprotected: Value,
pub payload_raw: Vec<u8>,
header_protected_raw: Vec<u8>,
payload_raw: Vec<u8>,
pub header: CwtHeader,
pub payload: DgcCertContainer,
pub signature: Vec<u8>,
}

impl Cwt {
pub fn new(
header_protected_raw: Vec<u8>,
header_protected: CwtHeader,
header_unprotected: Value,
payload_raw: Vec<u8>,
payload: DgcCertContainer,
signature: Vec<u8>,
) -> Self {
Cwt {
header_protected_raw,
header_protected,
header_unprotected,
payload_raw,
payload,
signature,
}
}

pub fn make_sig_structure(&self) -> Vec<u8> {
// https://datatracker.ietf.org/doc/html/rfc8152#section-4.4
let sig_structure_cbor = Value::Array(vec![
Expand All @@ -152,6 +139,35 @@ impl Cwt {
}
}

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;

Expand All @@ -162,41 +178,66 @@ impl TryFrom<&[u8]> for Cwt {
Value::Tag(tag_id, content) if tag_id == CBOR_WEB_TOKEN_TAG => *content,
cwt => cwt,
};
let cwt_content = match cwt_content {
Value::Tag(COSE_SIGN1_CBOR_TAG, content) => *content,
Value::Tag(tag_id, _) => return Err(InvalidTag(tag_id)),
cwt => cwt,
};
let parts = match cwt_content {
Value::Array(parts) => parts,
_ => return Err(InvalidParts),
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,
};
if parts.len() != 4 {
return Err(InvalidPartsCount(parts.len()));

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,
}
let header_protected_raw = (parts[0].as_bytes().ok_or(HeaderNotBinary)?).clone();
let header_protected: CwtHeader =
ciborium::de::from_reader::<'_, Value, _>(header_protected_raw.as_slice())
.map_err(|_| HeaderNotValidCbor)?
.as_map()
.ok_or(HeaderNotMap)?
.as_slice()
.into();
let header_unprotected = parts[1].clone();
let payload_raw = (parts[2].as_bytes().ok_or(PayloadNotBinary)?).clone();
let signature = (parts[3].as_bytes().ok_or(SignatureNotBinary)?).clone();
.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: DgcCertContainer =
ciborium::de::from_reader(payload_raw.as_slice()).map_err(InvalidPayload)?;

Ok(Cwt::new(
Ok(Cwt {
header_protected_raw,
header_protected,
header_unprotected,
payload_raw,
header,
payload,
signature,
))
})
}
}

Expand Down Expand Up @@ -225,8 +266,8 @@ mod tests {

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

assert_eq!(Some(expected_kid), cwt.header_protected.kid);
assert_eq!(Some(expected_alg), cwt.header_protected.alg);
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())
Expand Down
32 changes: 25 additions & 7 deletions src/dgc_cert.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{Recovery, Test, Vaccination};
use serde::{Deserialize, Serialize};
use serde::{Deserialize, Deserializer, Serialize};

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct DgcCertName {
Expand All @@ -13,19 +13,37 @@ pub struct DgcCertName {
pub fnt: String,
}

fn empty_if_null<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de>,
{
let opt = Option::deserialize(deserializer)?;
Ok(opt.unwrap_or_default())
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct DgcCert {
pub ver: String,
pub nam: DgcCertName,
pub dob: String,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(
default,
skip_serializing_if = "Vec::is_empty",
deserialize_with = "empty_if_null"
)]
pub t: Vec<Test>,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(
default,
skip_serializing_if = "Vec::is_empty",
deserialize_with = "empty_if_null"
)]
pub v: Vec<Vaccination>,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(
default,
skip_serializing_if = "Vec::is_empty",
deserialize_with = "empty_if_null"
)]
pub r: Vec<Recovery>,
}

Expand Down
16 changes: 8 additions & 8 deletions src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,20 +112,20 @@ pub fn validate(

let cwt = parse_cwt_payload(decompressed)?;

if cwt.header_protected.kid.is_none() {
if cwt.header.kid.is_none() {
return Ok((cwt.payload, SignatureValidity::MissingKid));
}
let kid = cwt.header_protected.kid.clone().unwrap();
let kid = cwt.header.kid.clone().unwrap();

if cwt.header_protected.alg.is_none() {
if cwt.header.alg.is_none() {
return Ok((cwt.payload, SignatureValidity::MissingSigningAlgorithm));
}
if !matches!(cwt.header_protected.alg, Some(EcAlg::Ecdsa256)) {
if !matches!(cwt.header.alg, Some(EcAlg::Ecdsa256)) {
return Ok((
cwt.payload,
SignatureValidity::UnsupportedSigningAlgorithm(format!(
"{:?}",
cwt.header_protected.alg.unwrap()
cwt.header.alg.unwrap()
)),
));
}
Expand Down Expand Up @@ -221,10 +221,10 @@ mod tests {
#[test]
fn it_parses_cwt_payload() {
let data = hex::decode("d2844da20448d919375fc1e7b6b20126a0590133a4041a60d9b00c061a60d70d0c01624154390103a101a4617681aa62646e01626d616d4f52472d3130303033303231356276706a313131393334393030376264746a323032312d30322d313862636f624154626369783155524e3a555643493a30313a41543a31303830373834334639344145453045453530393346424332353442443831332342626d706c45552f312f32302f31353238626973781b4d696e6973747279206f66204865616c74682c20417573747269616273640262746769383430353339303036636e616da463666e74754d5553544552465241553c474f455353494e47455262666e754d7573746572667261752d47c3b6c39f696e67657263676e74684741425249454c4562676e684761627269656c656376657265312e322e3163646f626a313939382d30322d32365840a91d6ed0869c0ca4d7896a37d77ab7ef406e6469adfdba1ecb336f84b77145bcfa852fe3a4af3cca0e0f7770e1c034d5d2facad829f6fec65b3c5321b9eeca88").unwrap();
let payload = hex::encode(parse_cwt_payload(data).unwrap().payload_raw);
let sig_structure = hex::encode(parse_cwt_payload(data).unwrap().make_sig_structure());

let expected = "a4041a60d9b00c061a60d70d0c01624154390103a101a4617681aa62646e01626d616d4f52472d3130303033303231356276706a313131393334393030376264746a323032312d30322d313862636f624154626369783155524e3a555643493a30313a41543a31303830373834334639344145453045453530393346424332353442443831332342626d706c45552f312f32302f31353238626973781b4d696e6973747279206f66204865616c74682c20417573747269616273640262746769383430353339303036636e616da463666e74754d5553544552465241553c474f455353494e47455262666e754d7573746572667261752d47c3b6c39f696e67657263676e74684741425249454c4562676e684761627269656c656376657265312e322e3163646f626a313939382d30322d3236";
assert_eq!(expected, payload);
let expected = "846a5369676e6174757265314da20448d919375fc1e7b6b2012640590133a4041a60d9b00c061a60d70d0c01624154390103a101a4617681aa62646e01626d616d4f52472d3130303033303231356276706a313131393334393030376264746a323032312d30322d313862636f624154626369783155524e3a555643493a30313a41543a31303830373834334639344145453045453530393346424332353442443831332342626d706c45552f312f32302f31353238626973781b4d696e6973747279206f66204865616c74682c20417573747269616273640262746769383430353339303036636e616da463666e74754d5553544552465241553c474f455353494e47455262666e754d7573746572667261752d47c3b6c39f696e67657263676e74684741425249454c4562676e684761627269656c656376657265312e322e3163646f626a313939382d30322d3236";
assert_eq!(expected, sig_structure);
}

#[test]
Expand Down
Loading