A look inside the

European Covid Green Certificate

Luciano Mammino (@loige)

22-02-2022 😱

Get these slides!

👋 I'm Luciano

👨‍💻 Senior Architect @ fourTheorem (Dublin 🇮🇪)

📔 Co-Author of Node.js Design Patterns  👉

Let's connect!

  loige.co (blog)

  @loige (twitter)

  loige (twitch)

  lmammino (github)

We are business focused technologists that deliver.


Accelerated Serverless | AI as a Service | Platform Modernisation

We are hiring: do you want to work with us?

🦀 I'm learning Rust as a hobby...

Disclaimers

🤓 I am not involved with the DGC working group

 

😢 COVID has been tough on everyone,
      we'll try to focus only on the tech here!

Agenda + Goals

1. 👩‍🏫 Needs and principles

2. 🗝 Cryptographic model

3. 📦 The data

4. 🧅 Layers of encoding

5. 🛠 Decoding in Rust

🤨 Learn some cool technologies

🧐 Learn a tiny bit of Rust

🤓 Be nerdy and have fun!

The need for a digital certificate in the COVID age

The need for a digital certificate in the COVID age

😷 We need a system to quickly provide a proof against COVID
      (Vaccination, negative test, proof of recovery) 

💁‍♂️ It needs to be personal, easy to carry around (digital),
       easy to issue and to validate

🌎 It needs to be secure against forgery and work across countries

The EU Covid Green Pass

a.k.a. 

Electronic Health Certificates (HCERT)

 

loige.link/hcert-spec

Electronic Health Certificates (HCERT)

Requirements & Guiding Principles

✍️ Signed data with machine readable content

📃 Use compact encoding

🤲 Based on open standards

Asymmetric cryptographic signatures

🤫 Private Key

📢 Public Key

🔗

101010101000101010010...
0101010101010101010101...

Asymmetric cryptographic signatures

🤫 Private Key

📢 Public Key

101010101000101010010...
0101010101010101010101...

The owner of the private key signs the document

Anyone can validate the signature using the public key

What's inside a certificate?

Cryptographic header (Key Id, Algorithm)

DGC container

Header (Issuer, Issue date, expiry date)

Certificates list

Cryptographic Signature

vaccine, test, or recovery data

Personal data (name, surname, DoB)

An example

An example

Personal info

Vaccine info

{
  "1": "DK",
  "4": 1625054000,
  "6": 1622462000,
  "-260": {
    "ver":"1.0.0",
    "nam":{
      "fn":"Klaus",
      "fnt":"KLAUS",
      "gn":"Jørgensen",
      "gnt":"JOERGENSEN"
    },
    "dob":"1983-01-06",
    "v":[
      {
        "tg":"840539006",
        "vp":"1119349007",
        "mp":"EU/1/20/1528",
        "ma":"ORG-100030215",
        "dn":2,
        "sd":2,
        "dt":"2021-05-03",
        "co":"DK",
        "is":"Danish Health Data Authority",
        "ci":"URN:UVCI:01:DK:B986830007345F99AE898FB82C6C61F2#A"
      }
    ]
  }
}

DGC Header

Layered encoding

​QRCODE ASCII mode

Prefix + Base45 encoding

Zlib compression

CWT signed data (COSE)

CBOR document

🧅

HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-36
5KN-TMLV4.P7*ZOP-IMJTI94F/8X*G3M9JUPY0BZW4Z
*AK.GNNVR*G0C7PHBO335KN/NBEDBVBJ623323EAJ7U
J5PNDIB6PNS7B1DN%BBWC7WC7GB3683ML7SZ4ZI00T9
UKPSH9WC5PF6846A$Q 76QW6A/98T5WBI$E9$UPV3Q.
GUQ$9WC5R7ACB97C968ELZ5$DP6PP5IL*PP:+P*.1D9
R8Q02-DE%QHOJ+PB/VSQOL9DLKWCZ3EBKDYGIZ J$XI
4OIMEDTJCJKDLEDL9CZTAKBI/8D:8DKTDL+SQ05.$S6
ZC0JBY63-C3F+LBQ99Q9E$BDZIA9JJ-JS7BYZJ92KG0
TB9FNDA5KD9FED.B4JB3E9B9NNPCV9E6LFSD9C8J-QD
SWNG4C-TLNKE$JDVPLW1KD0KCZGBKQCJE%RH5WAMSSR
$F-75NXONQ84QV9/7/-LT1AIYBZGD$9RCLV-PTZ-K63
ET-D1757H3GF9MV2N7WNQSY1SBZT-:81JJLHFQ-VG$H
K00XWPD2

QRCode content

"HC1:" (prefix)

Binary data encoded in Base45

Base45

Allows to encode binary data in text format (ASCII)

Like Base64, but it uses 45 characters instead:

Base45

Base45

01001001 00100000 01100111 01101111 01110100 00100000 01101101 01111001 00100000 01110011 01101000 01101111 01110100 01110011 00100000 11011000 00111101

UTF8 (17 bytes)

I got my shots 💉

Hex (38 bytes)

49 20 67 6f 74 20 6d 79 20 73 68 6f 74 73 20 f0 9f 92 89

Base64 (28 bytes)

SSBnb3QgbXkgc2hvdHMg8J+SiQ==

Base45 (29 bytes)

0B9J3DSUEZ$DR4459DLWEH74Z7K23

Some binary data

"A QR-code is used to encode text as a graphical image. [...] QR-codes cannot be used to encode arbitrary binary data directly.  [...] Compared to already established Base64, Base32 and Base16 encoding schemes [...], the Base45 scheme described in this document offer a more compact QR-code encoding"

Base45

Base45

NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-
TMLV4.P7*ZOP-IMJTI94F/8X*G3M9JUPY0BZW4Z*AK.
GNNVR*G0C7PHBO335KN/NBEDBVBJ623323EAJ7UJ5PN
DIB6PNS7B1DN%BBWC7WC7GB3683ML7SZ4ZI00T9UKPS
H9WC5PF6846A$Q 76QW6A/98T5WBI$E9$UPV3Q.GUQ$
9WC5R7ACB97C968ELZ5$DP6PP5IL*PP:+P*.1D9R8Q0
2-DE%QHOJ+PB/VSQOL9DLKWCZ3EBKDYGIZ J$XI4OIM
EDTJCJKDLEDL9CZTAKBI/8D:8DKTDL+SQ05.$S6ZC0J
BY63-C3F+LBQ99Q9E$BDZIA9JJ-JS7BYZJ92KG0TB9F
NDA5KD9FED.B4JB3E9B9NNPCV9E6LFSD9C8J-QDSWNG
4C-TLNKE$JDVPLW1KD0KCZGBKQCJE%RH5WAMSSR$F-7
5NXONQ84QV9/7/-LT1AIYBZGD$9RCLV-PTZ-K63ET-D
1757H3GF9MV2N7WNQSY1SBZT-:81JJLHFQ-VG$HK00X
WPD2

Base45

Decode

Compressed binary data

Zlib compression

Zlib compression

"zlib is designed to be a free, general-purpose, legally unencumbered -- that is, not covered by any patents -- lossless data-compression library for use on virtually any computer hardware and operating system"

Zlib compression

Zlib inflate

Compressed binary data

Decompressed binary data

Zlib compression

👀 We start to see some "readable" information!

COSE / CWT

CBOR Object Signing and Encryption / CBOR Web Token

CBOR !? 🤔

But wait, what the heck is

CBOR

TLDR; Like JSON but in binary!

JSON

A schema-less data format where a value can be:

Null

Boolean

Number

String

Array

Object

null
true
-17.34
"A programmer walks into a bar..."
["foo", 1.23, null, false, [22]]
{"foo": "bar", "manyvals": [1,2,3], "nested": {}}

CBOR

A schema-less binary data format where a value can be:

Null

Boolean

Number

String Text

Array

Object Map

F6
F5
fbc031570a3d70a3d7
7820412070726f6772616d6d65722077616c6b7320696e746f2061206261722e2e2e
8563666f6ffb3ff3ae147ae147aef6f48116
a363666f6f63626172686d616e7976616c7383010203666e6573746564a0

CBOR

It also supports:

Byte String (a sequence of raw bytes)

Tags (annotations that allow to create new types)

CBOR

An example:

A3 63666F6F 63626172 686D616E7976616C73 83 01 02 03 666E6573746564 A0
{
"foo":
"bar",
"manyvals":
[
}
],
"nested":
{}
1,
2,
3

COSE / CWT

CBOR Object Signing and Encryption / CBOR Web Token

ok... again 😅

COSE

CBOR Object Signing and Encryption

COSE (inspired by JOSE) defines CBOR-based protocols for:

Encrypted data

Cryptographic signed data

MACed data

CWT

Like JWT but for CBOR

Defines a protocol for transferring claims between parties

CBOR Web Token

Claims are digitally signed for authenticity

CWT

A CWT is made of 4 parts:

1️⃣ Protected header

CBOR Web Token

2️⃣ Non protected header

3️⃣ Payload

4️⃣ Signature

CWT

A CWT is encoded as a (tagged) CBOR array with 4 values:

1️⃣ Protected header (binary string)

CBOR Web Token

2️⃣ Non protected header (map)

3️⃣ Payload (binary string)

4️⃣ Signature (binary string)

CWT tag

Array (length 4)

Unprotected header
Map (length 0)

Protected header
Binary String

Payload
Binary String

Signature
Binary String

CWT payload

{
  "1": "DK",
  "4": 1625054000,
  "6": 1622462000,
  "-260": {
    "ver":"1.0.0",
    "nam":{
      "fn":"Klaus",
      "fnt":"KLAUS",
      "gn":"Jørgensen",
      "gnt":"JOERGENSEN"
    },
    "dob":"1983-01-06",
    "v":[
      {
        "tg":"840539006",
        "vp":"1119349007",
        "mp":"EU/1/20/1528",
        "ma":"ORG-100030215",
        "dn":2,
        "sd":2,
        "dt":"2021-05-03",
        "co":"DK",
        "is":"Danish Health Data Authority",
        "ci":"URN:UVCI:01:DK:B986830007345F99AE898FB82C6C61F2#A"
      }
    ]
  }
}

CBOR decode

    (to JSON)

Binary String follows (CBOR Encoded)

How to decode - quick recap 🏃‍♂️

1. Remove "HC1:" prefix

2. Base45 decode

3. Zlib decompress

4. Parse CWT

5. Parse CWT Payload as CBOR

6. Party hard! 🥳

Hey, let's implement this...

in Rust!

cargo new dgc-decode

Project bootstrap

// src/main.rs

fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4.P7*ZOP-IMJTI94F/8X*G3M9JUPY0BZW4Z*AK.GNNVR*G0C7PHBO335KN/NBEDBVBJ623323EAJ7UJ5PNDIB6PNS7B1DN%BBWC7WC7GB3683ML7SZ4ZI00T9UKPSH9WC5PF6846A$Q 76QW6A/98T5WBI$E9$UPV3Q.GUQ$9WC5R7ACB97C968ELZ5$DP6PP5IL*PP:+P*.1D9R8Q02-DE%QHOJ+PB/VSQOL9DLKWCZ3EBKDYGIZ J$XI4OIMEDTJCJKDLEDL9CZTAKBI/8D:8DKTDL+SQ05.$S6ZC0JBY63-C3F+LBQ99Q9E$BDZIA9JJ-JS7BYZJ92KG0TB9FNDA5KD9FED.B4JB3E9B9NNPCV9E6LFSD9C8J-QDSWNG4C-TLNKE$JDVPLW1KD0KCZGBKQCJE%RH5WAMSSR$F-75NXONQ84QV9/7/-LT1AIYBZGD$9RCLV-PTZ-K63ET-D1757H3GF9MV2N7WNQSY1SBZT-:81JJLHFQ-VG$HK00XWPD2";
    todo!()
    // 1. Remove "HC1:" prefix
    // 2. Base45 decode
    // 3. Zlib decompress
    // 4. Parse CWT
    // 5. Parse CWT Payload as CBOR
}
fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4.P7*ZOP-IMJTI94F/8X*G3M9JUPY0BZW4Z*AK.GNNVR*G0C7PHBO335KN/NBEDBVBJ623323EAJ7UJ5PNDIB6PNS7B1DN%BBWC7WC7GB3683ML7SZ4ZI00T9UKPSH9WC5PF6846A$Q 76QW6A/98T5WBI$E9$UPV3Q.GUQ$9WC5R7ACB97C968ELZ5$DP6PP5IL*PP:+P*.1D9R8Q02-DE%QHOJ+PB/VSQOL9DLKWCZ3EBKDYGIZ J$XI4OIMEDTJCJKDLEDL9CZTAKBI/8D:8DKTDL+SQ05.$S6ZC0JBY63-C3F+LBQ99Q9E$BDZIA9JJ-JS7BYZJ92KG0TB9FNDA5KD9FED.B4JB3E9B9NNPCV9E6LFSD9C8J-QDSWNG4C-TLNKE$JDVPLW1KD0KCZGBKQCJE%RH5WAMSSR$F-75NXONQ84QV9/7/-LT1AIYBZGD$9RCLV-PTZ-K63ET-D1757H3GF9MV2N7WNQSY1SBZT-:81JJLHFQ-VG$HK00XWPD2";
    let no_prefix = remove_prefix(cert_data);
    todo!();
    // 2. Base45 decode
    // 3. Zlib decompress
    // 4. Parse CWT
    // 5. Parse CWT Payload as CBOR
}

fn remove_prefix(data: &str) -> &str {
    todo!()
    // remove "HC1:" prefix and return remaining string
}
fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4.P7*ZOP-IMJTI94F/8X*G3M9JUPY0BZW4Z*AK.GNNVR*G0C7PHBO335KN/NBEDBVBJ623323EAJ7UJ5PNDIB6PNS7B1DN%BBWC7WC7GB3683ML7SZ4ZI00T9UKPSH9WC5PF6846A$Q 76QW6A/98T5WBI$E9$UPV3Q.GUQ$9WC5R7ACB97C968ELZ5$DP6PP5IL*PP:+P*.1D9R8Q02-DE%QHOJ+PB/VSQOL9DLKWCZ3EBKDYGIZ J$XI4OIMEDTJCJKDLEDL9CZTAKBI/8D:8DKTDL+SQ05.$S6ZC0JBY63-C3F+LBQ99Q9E$BDZIA9JJ-JS7BYZJ92KG0TB9FNDA5KD9FED.B4JB3E9B9NNPCV9E6LFSD9C8J-QDSWNG4C-TLNKE$JDVPLW1KD0KCZGBKQCJE%RH5WAMSSR$F-75NXONQ84QV9/7/-LT1AIYBZGD$9RCLV-PTZ-K63ET-D1757H3GF9MV2N7WNQSY1SBZT-:81JJLHFQ-VG$HK00XWPD2";
    let no_prefix = remove_prefix(cert_data);
    todo!();
    // 2. Base45 decode
    // 3. Zlib decompress
    // 4. Parse CWT
    // 5. Parse CWT Payload as CBOR
}

fn remove_prefix(data: &str) -> &str {
    if data.len() < 4 || !data.starts_with("HC1:") {
        panic!("Invalid prefix"); // IRL use a Result!
    }

    &data[4..]
}
fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4.P7*ZOP-IMJTI94F/8X*G3M9JUPY0BZW4Z*AK.GNNVR*G0C7PHBO335KN/NBEDBVBJ623323EAJ7UJ5PNDIB6PNS7B1DN%BBWC7WC7GB3683ML7SZ4ZI00T9UKPSH9WC5PF6846A$Q 76QW6A/98T5WBI$E9$UPV3Q.GUQ$9WC5R7ACB97C968ELZ5$DP6PP5IL*PP:+P*.1D9R8Q02-DE%QHOJ+PB/VSQOL9DLKWCZ3EBKDYGIZ J$XI4OIMEDTJCJKDLEDL9CZTAKBI/8D:8DKTDL+SQ05.$S6ZC0JBY63-C3F+LBQ99Q9E$BDZIA9JJ-JS7BYZJ92KG0TB9FNDA5KD9FED.B4JB3E9B9NNPCV9E6LFSD9C8J-QDSWNG4C-TLNKE$JDVPLW1KD0KCZGBKQCJE%RH5WAMSSR$F-75NXONQ84QV9/7/-LT1AIYBZGD$9RCLV-PTZ-K63ET-D1757H3GF9MV2N7WNQSY1SBZT-:81JJLHFQ-VG$HK00XWPD2";
    let no_prefix = remove_prefix(cert_data);
    let decoded = decode_base45(no_prefix);
    todo!()
    // 3. Zlib decompress
    // 4. Parse CWT
    // 5. Parse CWT Payload as CBOR
}

fn decode_base45(data: &str) -> Vec<u8> {
    todo!()
    // parse the string as base45 encoded and return the
    // resulting raw bytes
}

// ...
cargo add base45
# Cargo.toml
# ...

[dependencies]
base45 = "3.0.0"
fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4.P7*ZOP-IMJTI94F/8X*G3M9JUPY0BZW4Z*AK.GNNVR*G0C7PHBO335KN/NBEDBVBJ623323EAJ7UJ5PNDIB6PNS7B1DN%BBWC7WC7GB3683ML7SZ4ZI00T9UKPSH9WC5PF6846A$Q 76QW6A/98T5WBI$E9$UPV3Q.GUQ$9WC5R7ACB97C968ELZ5$DP6PP5IL*PP:+P*.1D9R8Q02-DE%QHOJ+PB/VSQOL9DLKWCZ3EBKDYGIZ J$XI4OIMEDTJCJKDLEDL9CZTAKBI/8D:8DKTDL+SQ05.$S6ZC0JBY63-C3F+LBQ99Q9E$BDZIA9JJ-JS7BYZJ92KG0TB9FNDA5KD9FED.B4JB3E9B9NNPCV9E6LFSD9C8J-QDSWNG4C-TLNKE$JDVPLW1KD0KCZGBKQCJE%RH5WAMSSR$F-75NXONQ84QV9/7/-LT1AIYBZGD$9RCLV-PTZ-K63ET-D1757H3GF9MV2N7WNQSY1SBZT-:81JJLHFQ-VG$HK00XWPD2";
    let no_prefix = remove_prefix(cert_data);
    let decoded = decode_base45(no_prefix);
    todo!()
    // 3. Zlib decompress
    // 4. Parse CWT
    // 5. Parse CWT Payload as CBOR
}

fn decode_base45(data: &str) -> Vec<u8> {
    base45::decode(data).unwrap() // IRL use a Result!
}

// ...
fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4.P7*ZOP-IMJTI94F/8X*G3M9JUPY0BZW4Z*AK.GNNVR*G0C7PHBO335KN/NBEDBVBJ623323EAJ7UJ5PNDIB6PNS7B1DN%BBWC7WC7GB3683ML7SZ4ZI00T9UKPSH9WC5PF6846A$Q 76QW6A/98T5WBI$E9$UPV3Q.GUQ$9WC5R7ACB97C968ELZ5$DP6PP5IL*PP:+P*.1D9R8Q02-DE%QHOJ+PB/VSQOL9DLKWCZ3EBKDYGIZ J$XI4OIMEDTJCJKDLEDL9CZTAKBI/8D:8DKTDL+SQ05.$S6ZC0JBY63-C3F+LBQ99Q9E$BDZIA9JJ-JS7BYZJ92KG0TB9FNDA5KD9FED.B4JB3E9B9NNPCV9E6LFSD9C8J-QDSWNG4C-TLNKE$JDVPLW1KD0KCZGBKQCJE%RH5WAMSSR$F-75NXONQ84QV9/7/-LT1AIYBZGD$9RCLV-PTZ-K63ET-D1757H3GF9MV2N7WNQSY1SBZT-:81JJLHFQ-VG$HK00XWPD2";
    let no_prefix = remove_prefix(cert_data);
    let decoded = decode_base45(no_prefix);
    let decompressed = decompress(decoded);
    todo!()
    // 4. Parse CWT
    // 5. Parse CWT Payload as CBOR
}

fn decompress(data: Vec<u8>) -> Vec<u8> {
    todo!()
    // decompress using zlib inflate
}

// ...
cargo add inflate
# Cargo.toml
# ...

[dependencies]
base45 = "3.0.0"
inflate = "0.4.5"
fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4.P7*ZOP-IMJTI94F/8X*G3M9JUPY0BZW4Z*AK.GNNVR*G0C7PHBO335KN/NBEDBVBJ623323EAJ7UJ5PNDIB6PNS7B1DN%BBWC7WC7GB3683ML7SZ4ZI00T9UKPSH9WC5PF6846A$Q 76QW6A/98T5WBI$E9$UPV3Q.GUQ$9WC5R7ACB97C968ELZ5$DP6PP5IL*PP:+P*.1D9R8Q02-DE%QHOJ+PB/VSQOL9DLKWCZ3EBKDYGIZ J$XI4OIMEDTJCJKDLEDL9CZTAKBI/8D:8DKTDL+SQ05.$S6ZC0JBY63-C3F+LBQ99Q9E$BDZIA9JJ-JS7BYZJ92KG0TB9FNDA5KD9FED.B4JB3E9B9NNPCV9E6LFSD9C8J-QDSWNG4C-TLNKE$JDVPLW1KD0KCZGBKQCJE%RH5WAMSSR$F-75NXONQ84QV9/7/-LT1AIYBZGD$9RCLV-PTZ-K63ET-D1757H3GF9MV2N7WNQSY1SBZT-:81JJLHFQ-VG$HK00XWPD2";
    let no_prefix = remove_prefix(cert_data);
    let decoded = decode_base45(no_prefix);
    let decompressed = decompress(decoded);
    todo!()
    // 4. Parse CWT
    // 5. Parse CWT Payload as CBOR
}

fn decompress(data: Vec<u8>) -> Vec<u8> {
    inflate::inflate_bytes_zlib(data.as_slice()).unwrap()
    // IRL use a Result!
}

// ...

Are we on the right track?

fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4.P7*ZOP-IMJTI94F/8X*G3M9JUPY0BZW4Z*AK.GNNVR*G0C7PHBO335KN/NBEDBVBJ623323EAJ7UJ5PNDIB6PNS7B1DN%BBWC7WC7GB3683ML7SZ4ZI00T9UKPSH9WC5PF6846A$Q 76QW6A/98T5WBI$E9$UPV3Q.GUQ$9WC5R7ACB97C968ELZ5$DP6PP5IL*PP:+P*.1D9R8Q02-DE%QHOJ+PB/VSQOL9DLKWCZ3EBKDYGIZ J$XI4OIMEDTJCJKDLEDL9CZTAKBI/8D:8DKTDL+SQ05.$S6ZC0JBY63-C3F+LBQ99Q9E$BDZIA9JJ-JS7BYZJ92KG0TB9FNDA5KD9FED.B4JB3E9B9NNPCV9E6LFSD9C8J-QDSWNG4C-TLNKE$JDVPLW1KD0KCZGBKQCJE%RH5WAMSSR$F-75NXONQ84QV9/7/-LT1AIYBZGD$9RCLV-PTZ-K63ET-D1757H3GF9MV2N7WNQSY1SBZT-:81JJLHFQ-VG$HK00XWPD2";
    let no_prefix = remove_prefix(cert_data);
    let decoded = decode_base45(no_prefix);
    let decompressed = decompress(decoded);

    println!("{}", String::from_utf8_lossy(&decompressed));
}

// ...

Are we on the right track?

We are starting to see some readable info! 🤩

fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4.P7*ZOP-IMJTI94F/8X*G3M9JUPY0BZW4Z*AK.GNNVR*G0C7PHBO335KN/NBEDBVBJ623323EAJ7UJ5PNDIB6PNS7B1DN%BBWC7WC7GB3683ML7SZ4ZI00T9UKPSH9WC5PF6846A$Q 76QW6A/98T5WBI$E9$UPV3Q.GUQ$9WC5R7ACB97C968ELZ5$DP6PP5IL*PP:+P*.1D9R8Q02-DE%QHOJ+PB/VSQOL9DLKWCZ3EBKDYGIZ J$XI4OIMEDTJCJKDLEDL9CZTAKBI/8D:8DKTDL+SQ05.$S6ZC0JBY63-C3F+LBQ99Q9E$BDZIA9JJ-JS7BYZJ92KG0TB9FNDA5KD9FED.B4JB3E9B9NNPCV9E6LFSD9C8J-QDSWNG4C-TLNKE$JDVPLW1KD0KCZGBKQCJE%RH5WAMSSR$F-75NXONQ84QV9/7/-LT1AIYBZGD$9RCLV-PTZ-K63ET-D1757H3GF9MV2N7WNQSY1SBZT-:81JJLHFQ-VG$HK00XWPD2";
    let no_prefix = remove_prefix(cert_data);
    let decoded = decode_base45(no_prefix);
    let decompressed = decompress(decoded);
    let cwt_payload = get_cwt_payload(decompressed);
    todo!()
    // 5. Parse CWT Payload as CBOR
}

fn get_cwt_payload(data: Vec<u8>) -> Vec<u8> {
    todo!()
    // Parse raw bytes as CBOR.
    // Extract and return the raw bytes representing
    // the CWT payload
}

// ...
cargo add ciborium
# Cargo.toml
# ...

[dependencies]
base45 = "3.0.0"
ciborium = "0.2.0"
inflate = "0.4.5"
use ciborium::{de::from_reader, value::Value};

fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4.P7*ZOP-IMJTI94F/8X*G3M9JUPY0BZW4Z*AK.GNNVR*G0C7PHBO335KN/NBEDBVBJ623323EAJ7UJ5PNDIB6PNS7B1DN%BBWC7WC7GB3683ML7SZ4ZI00T9UKPSH9WC5PF6846A$Q 76QW6A/98T5WBI$E9$UPV3Q.GUQ$9WC5R7ACB97C968ELZ5$DP6PP5IL*PP:+P*.1D9R8Q02-DE%QHOJ+PB/VSQOL9DLKWCZ3EBKDYGIZ J$XI4OIMEDTJCJKDLEDL9CZTAKBI/8D:8DKTDL+SQ05.$S6ZC0JBY63-C3F+LBQ99Q9E$BDZIA9JJ-JS7BYZJ92KG0TB9FNDA5KD9FED.B4JB3E9B9NNPCV9E6LFSD9C8J-QDSWNG4C-TLNKE$JDVPLW1KD0KCZGBKQCJE%RH5WAMSSR$F-75NXONQ84QV9/7/-LT1AIYBZGD$9RCLV-PTZ-K63ET-D1757H3GF9MV2N7WNQSY1SBZT-:81JJLHFQ-VG$HK00XWPD2";
    let no_prefix = remove_prefix(cert_data);
    let decoded = decode_base45(no_prefix);
    let decompressed = decompress(decoded);
    let cwt_payload = get_cwt_payload(decompressed);
    todo!()
    // 5. Parse CWT Payload as CBOR
}

fn get_cwt_payload(data: Vec<u8>) -> Vec<u8> {
    let parsed: Value = from_reader(data.as_slice()).unwrap();
    println!("{:?}", parsed);
    todo!()
}

// ...

Payload  ✅

Non protected header

Protected header

The content is an array

CWT Tag (18)

Signature

// ...

fn get_cwt_payload(data: Vec<u8>) -> Vec<u8> {
    // IRL avoid .unwrap() like hell and propagate errors correctly!
    let parsed: Value = from_reader(data.as_slice()).unwrap();
    let (tag, arr) = parsed.as_tag().unwrap();
    assert_eq!(tag, 18);
    let arr = arr.as_array().unwrap();
    let payload = arr[2].as_bytes().unwrap();
    payload.clone()
}

// ...
use ciborium::{de::from_reader, value::Value};

fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4.P7*ZOP-IMJTI94F/8X*G3M9JUPY0BZW4Z*AK.GNNVR*G0C7PHBO335KN/NBEDBVBJ623323EAJ7UJ5PNDIB6PNS7B1DN%BBWC7WC7GB3683ML7SZ4ZI00T9UKPSH9WC5PF6846A$Q 76QW6A/98T5WBI$E9$UPV3Q.GUQ$9WC5R7ACB97C968ELZ5$DP6PP5IL*PP:+P*.1D9R8Q02-DE%QHOJ+PB/VSQOL9DLKWCZ3EBKDYGIZ J$XI4OIMEDTJCJKDLEDL9CZTAKBI/8D:8DKTDL+SQ05.$S6ZC0JBY63-C3F+LBQ99Q9E$BDZIA9JJ-JS7BYZJ92KG0TB9FNDA5KD9FED.B4JB3E9B9NNPCV9E6LFSD9C8J-QDSWNG4C-TLNKE$JDVPLW1KD0KCZGBKQCJE%RH5WAMSSR$F-75NXONQ84QV9/7/-LT1AIYBZGD$9RCLV-PTZ-K63ET-D1757H3GF9MV2N7WNQSY1SBZT-:81JJLHFQ-VG$HK00XWPD2";
    let no_prefix = remove_prefix(cert_data);
    let decoded = decode_base45(no_prefix);
    let decompressed = decompress(decoded);
    let cwt_payload = get_cwt_payload(decompressed);
    let parsed_payload = parse_cwt_payload(cwt_payload);
}

fn parse_cwt_payload(data: Vec<u8>) -> Value {
    // parse the binary data as CBOR
    todo!()
}

// ...
use ciborium::{de::from_reader, value::Value};

fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4.P7*ZOP-IMJTI94F/8X*G3M9JUPY0BZW4Z*AK.GNNVR*G0C7PHBO335KN/NBEDBVBJ623323EAJ7UJ5PNDIB6PNS7B1DN%BBWC7WC7GB3683ML7SZ4ZI00T9UKPSH9WC5PF6846A$Q 76QW6A/98T5WBI$E9$UPV3Q.GUQ$9WC5R7ACB97C968ELZ5$DP6PP5IL*PP:+P*.1D9R8Q02-DE%QHOJ+PB/VSQOL9DLKWCZ3EBKDYGIZ J$XI4OIMEDTJCJKDLEDL9CZTAKBI/8D:8DKTDL+SQ05.$S6ZC0JBY63-C3F+LBQ99Q9E$BDZIA9JJ-JS7BYZJ92KG0TB9FNDA5KD9FED.B4JB3E9B9NNPCV9E6LFSD9C8J-QDSWNG4C-TLNKE$JDVPLW1KD0KCZGBKQCJE%RH5WAMSSR$F-75NXONQ84QV9/7/-LT1AIYBZGD$9RCLV-PTZ-K63ET-D1757H3GF9MV2N7WNQSY1SBZT-:81JJLHFQ-VG$HK00XWPD2";
    let no_prefix = remove_prefix(cert_data);
    let decoded = decode_base45(no_prefix);
    let decompressed = decompress(decoded);
    let cwt_payload = get_cwt_payload(decompressed);
    let parsed_payload = parse_cwt_payload(cwt_payload);
    println!("{:#?}", parsed_payload);
}

fn parse_cwt_payload(data: Vec<u8>) -> Value {
    from_reader(data.as_slice()).unwrap()
}

// ...

Ok, let's make it more readable... 😅

cargo add serde_json
use ciborium::{de::from_reader, value::Value};
use serde_json::to_string_pretty;

fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4.P7*ZOP-IMJTI94F/8X*G3M9JUPY0BZW4Z*AK.GNNVR*G0C7PHBO335KN/NBEDBVBJ623323EAJ7UJ5PNDIB6PNS7B1DN%BBWC7WC7GB3683ML7SZ4ZI00T9UKPSH9WC5PF6846A$Q 76QW6A/98T5WBI$E9$UPV3Q.GUQ$9WC5R7ACB97C968ELZ5$DP6PP5IL*PP:+P*.1D9R8Q02-DE%QHOJ+PB/VSQOL9DLKWCZ3EBKDYGIZ J$XI4OIMEDTJCJKDLEDL9CZTAKBI/8D:8DKTDL+SQ05.$S6ZC0JBY63-C3F+LBQ99Q9E$BDZIA9JJ-JS7BYZJ92KG0TB9FNDA5KD9FED.B4JB3E9B9NNPCV9E6LFSD9C8J-QDSWNG4C-TLNKE$JDVPLW1KD0KCZGBKQCJE%RH5WAMSSR$F-75NXONQ84QV9/7/-LT1AIYBZGD$9RCLV-PTZ-K63ET-D1757H3GF9MV2N7WNQSY1SBZT-:81JJLHFQ-VG$HK00XWPD2";
    let no_prefix = remove_prefix(cert_data);
    let decoded = decode_base45(no_prefix);
    let decompressed = decompress(decoded);
    let cwt_payload = get_cwt_payload(decompressed);
    let parsed_payload = parse_cwt_payload(cwt_payload);
    println!("{}", to_string_pretty(&parsed_payload).unwrap());
}

// ...

All the code:

loige.link/green-code

A better (& more complete) implementation
as a Rust library

Exercise for the viewer:
Try to validate the signature

🔑 You can get the Public Key from the certificate here: loige.link/green-examples

📑 Here you can find more about how the CoseSign1 protocol works: loige.link/cose-sign-verif

📦 You could use a crate like ring for crypto!

(Spoiler: We implemented some of this stuff in the dgc library!)

Is all this stuff legal? 😰

👀 You can certainly look into your certificate (and the test certificates!)

🗣 Looking into other people's certificate will disclose a lot of privacy-sensitive info (thread carefully)

📲 Building a validator app? Check your country's regulation (especially if you need to store data!)

Cover Picture by FPVmat A on Unsplash
❤️  Huge thanks to rust-italia for some precius review sessions and many pull requests!
❤️ Thanks to @gbinside, @88_eugen, @AlleviTommaso, @npmccallum, @pelger for reviews and suggestions.

☝️ nodejsdp.link

THANK YOU!

❤️

A look inside the European Covid Green Certificate - Rust Dublin

By Luciano Mammino

A look inside the European Covid Green Certificate - Rust Dublin

When I saw how dense the European Covid Green Pass QR code is, I got immediately curious: "WOW, there must be a lot of interesting data in here". So, I started to dig deeper and I found that there's really a great wealth of interesting encoding and verification technologies being used in it! In this talk, I will share what I learned! We will go on a journey where we will explore Base54 encoding, COSE tokens, CBOR serialization, elliptic curve crypto, and much more! Finally, I will also show you how to write a decoder for Green Pass certificates in the most hyped language ever: Rust!

  • 871