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ย ๐
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
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!
โค๏ธ