Upgrade to Pro — share decks privately, control downloads, hide ads and more …

A look inside the European Covid Green Certificate - Rust Dublin

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!

Luciano Mammino

February 22, 2022
Tweet

More Decks by Luciano Mammino

Other Decks in Technology

Transcript

  1. A look inside the
    European Covid Green Certificate
    Luciano Mammino ( )
    @loige
    loige.link/rust-green
    22-02-2022
    😱
    1

    View full-size slide

  2. Get these slides!
    loige
    loige.link/rust-green
    2

    View full-size slide

  3. 👋 I'm Luciano
    Senior Architect @ fourTheorem (Dublin )
    nodejsdp.link
    📔 Co-Author of Node.js Design Patterns
    👉
    Let's connect!
    (blog)
    (twitter)
    (twitch)
    (github)
    loige.co
    @loige
    loige
    lmammino
    3

    View full-size slide

  4. 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
    loige 4

    View full-size slide

  5. loige
    🦀 I'm learning Rust as a hobby...
    Live streaming my attempts to crack Advent of Code in Rust (with
    and ): /
    @gbinside @88_eugen Twitch YouTube
    Wrote articles:
    a few
    How to to_string in Rust
    Rust shenanigans: return type polymorphism
    Where to go to learn Rust in 2021
    Published (simple) crates: , , ,
    a few jwtinfo allwords gmaps-static dgc
    5

    View full-size slide

  6. Disclaimers
    loige
    🤓 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!
    6

    View full-size slide

  7. Agenda + Goals
    loige
    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!
    7

    View full-size slide

  8. The need for a digital
    certificate in the COVID age
    loige 8

    View full-size slide

  9. The need for a digital
    certificate in the COVID age
    loige
    😷 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
    9

    View full-size slide

  10. The EU Covid Green Pass
    a.k.a.
    Electronic Health
    Certificates (HCERT)
    loige.link/hcert-spec
    loige 10

    View full-size slide

  11. Electronic Health Certificates (HCERT)
    Requirements & Guiding Principles
    loige
    ✍ Signed data with machine readable
    content
    📃 Use compact encoding
    🤲 Based on open standards
    11

    View full-size slide

  12. Asymmetric cryptographic signatures
    loige
    🤫 Private Key
    📢 Public Key
    🔗
    101010101000101010010... 0101010101010101010101...
    12

    View full-size slide

  13. Asymmetric cryptographic signatures
    loige
    🤫 Private Key
    📢 Public Key
    101010101000101010010... 0101010101010101010101...
    The owner of the
    private key signs the
    document
    Anyone can validate the
    signature using the
    public key
    13

    View full-size slide

  14. What's inside a certificate?
    loige
    DGC container
    Cryptographic header (Key Id,
    Algorithm)
    Cryptographic Signature
    Header (Issuer, Issue date, expiry date)
    14
    Certificates list
    vaccine, test, or recovery data
    Personal data (name, surname, DoB)

    View full-size slide

  15. An
    example
    loige loige.link/green-examples 15

    View full-size slide

  16. {
    "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"
    }
    ]
    }
    }
    An
    example
    loige loige.link/green-examples
    Personal info
    Vaccine info
    DGC Header
    16

    View full-size slide

  17. Layered
    encoding
    loige
    QRCODE ASCII mode
    Prefix + Base45 encoding
    Zlib compression
    CWT signed data (COSE)
    CBOR document
    17
    🧅

    View full-size slide

  18. loige
    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
    18

    View full-size slide

  19. Base45
    loige 19

    View full-size slide

  20. loige
    Allows to encode binary data in text format (ASCII)
    Like Base64, but it uses 45 characters instead:
    loige.link/base45-explained
    Base45
    20

    View full-size slide

  21. Base45
    loige
    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
    21

    View full-size slide

  22. loige
    loige.link/base45-rfc
    "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
    22

    View full-size slide

  23. loige
    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
    23

    View full-size slide

  24. Zlib compression
    loige 24

    View full-size slide

  25. Zlib compression
    loige
    "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.net
    25

    View full-size slide

  26. Zlib compression
    loige
    Zlib inflate
    Compressed binary data Decompressed binary data
    26

    View full-size slide

  27. Zlib compression
    loige
    👀 We start to see
    some "readable"
    information!
    27

    View full-size slide

  28. COSE / CWT
    loige
    CBOR Object Signing and Encryption / CBOR Web Token
    28

    View full-size slide

  29. CBOR !?
    🤔
    loige
    But wait, what the heck is

    29

    View full-size slide

  30. CBOR
    loige
    TLDR; Like JSON but in
    binary!
    loige.link/cbor-rfc
    30

    View full-size slide

  31. JSON
    loige
    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": {}}
    31

    View full-size slide

  32. CBOR
    loige
    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
    32

    View full-size slide

  33. CBOR
    loige
    It also supports:
    Byte String (a sequence of raw bytes)
    Tags (annotations that allow to create new types)
    33

    View full-size slide

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

    View full-size slide

  35. COSE / CWT
    loige
    CBOR Object Signing and Encryption / CBOR Web Token
    ok... again
    😅
    35

    View full-size slide

  36. COSE
    loige
    loige.link/cose-rfc
    CBOR Object Signing and Encryption
    COSE (inspired by ) defines CBOR-based protocols for:
    JOSE
    Encrypted data
    Cryptographic signed data
    MACed data
    36

    View full-size slide

  37. CWT
    loige
    Like but for CBOR
    JWT
    loige.link/cwt-rfc
    Defines a protocol for transferring claims between parties
    CBOR Web Token
    Claims are digitally signed for authenticity
    37

    View full-size slide

  38. CWT
    loige
    A CWT is made of 4 parts:
    Protected header
    CBOR Web Token
    Non protected header
    Payload
    Signature
    38

    View full-size slide

  39. CWT
    loige
    A CWT is encoded as a (tagged) CBOR array with 4 values:
    Protected header (binary string)
    CBOR Web Token
    Non protected header (map)
    Payload (binary string)
    Signature (binary string)
    39

    View full-size slide

  40. Protected
    header
    Binary String
    loige
    Array (length
    4)
    CWT tag
    40
    Unprotected
    header
    Map (length 0)
    Payload
    Binary String
    Signature
    Binary String

    View full-size slide

  41. loige
    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"
    }
    ]
    }
    }
    Binary String follows (CBOR Encoded)
    CBOR decode
    (to JSON)
    41

    View full-size slide

  42. How to decode - quick recap
    loige
    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!
    42

    View full-size slide

  43. loige
    cargo new dgc-decode
    Project bootstrap
    43

    View full-size slide

  44. // src/main.rs
    fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    todo!()
    // 1. Remove "HC1:" prefix
    // 2. Base45 decode
    // 3. Zlib decompress
    // 4. Parse CWT
    // 5. Parse CWT Payload as CBOR
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    // src/main.rs
    1
    2
    fn main() {
    3
    4
    todo!()
    5
    // 1. Remove "HC1:" prefix
    6
    // 2. Base45 decode
    7
    // 3. Zlib decompress
    8
    // 4. Parse CWT
    9
    // 5. Parse CWT Payload as CBOR
    10
    }
    11
    todo!()
    // 1. Remove "HC1:" prefix
    // 2. Base45 decode
    // 3. Zlib decompress
    // 4. Parse CWT
    // 5. Parse CWT Payload as CBOR
    // src/main.rs
    1
    2
    fn main() {
    3
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    4
    5
    6
    7
    8
    9
    10
    }
    11
    // src/main.rs
    fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    todo!()
    // 1. Remove "HC1:" prefix
    // 2. Base45 decode
    // 3. Zlib decompress
    // 4. Parse CWT
    // 5. Parse CWT Payload as CBOR
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    loige 44

    View full-size slide

  45. fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    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
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    let no_prefix = remove_prefix(cert_data);
    fn main() {
    1
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    2
    3
    todo!();
    4
    // 2. Base45 decode
    5
    // 3. Zlib decompress
    6
    // 4. Parse CWT
    7
    // 5. Parse CWT Payload as CBOR
    8
    }
    9
    10
    fn remove_prefix(data: &str) -> &str {
    11
    todo!()
    12
    // remove "HC1:" prefix and return remaining string
    13
    }
    14
    fn remove_prefix(data: &str) -> &str {
    todo!()
    // remove "HC1:" prefix and return remaining string
    }
    fn main() {
    1
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    2
    let no_prefix = remove_prefix(cert_data);
    3
    todo!();
    4
    // 2. Base45 decode
    5
    // 3. Zlib decompress
    6
    // 4. Parse CWT
    7
    // 5. Parse CWT Payload as CBOR
    8
    }
    9
    10
    11
    12
    13
    14
    fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    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
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    loige 45

    View full-size slide

  46. fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    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..]
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    if data.len() < 4 || !data.starts_with("HC1:") {
    panic!("Invalid prefix"); // IRL use a Result!
    }
    &data[4..]
    fn main() {
    1
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    2
    let no_prefix = remove_prefix(cert_data);
    3
    todo!();
    4
    // 2. Base45 decode
    5
    // 3. Zlib decompress
    6
    // 4. Parse CWT
    7
    // 5. Parse CWT Payload as CBOR
    8
    }
    9
    10
    fn remove_prefix(data: &str) -> &str {
    11
    12
    13
    14
    15
    16
    }
    17
    fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    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..]
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    loige 46

    View full-size slide

  47. fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    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 {
    todo!()
    // parse the string as base45 encoded and return the
    // resulting raw bytes
    }
    // ...
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    let decoded = decode_base45(no_prefix);
    fn main() {
    1
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    2
    let no_prefix = remove_prefix(cert_data);
    3
    4
    todo!()
    5
    // 3. Zlib decompress
    6
    // 4. Parse CWT
    7
    // 5. Parse CWT Payload as CBOR
    8
    }
    9
    10
    fn decode_base45(data: &str) -> Vec {
    11
    todo!()
    12
    // parse the string as base45 encoded and return the
    13
    // resulting raw bytes
    14
    }
    15
    16
    // ...
    17
    fn decode_base45(data: &str) -> Vec {
    todo!()
    // parse the string as base45 encoded and return the
    // resulting raw bytes
    }
    fn main() {
    1
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    2
    let no_prefix = remove_prefix(cert_data);
    3
    let decoded = decode_base45(no_prefix);
    4
    todo!()
    5
    // 3. Zlib decompress
    6
    // 4. Parse CWT
    7
    // 5. Parse CWT Payload as CBOR
    8
    }
    9
    10
    11
    12
    13
    14
    15
    16
    // ...
    17
    fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    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 {
    todo!()
    // parse the string as base45 encoded and return the
    // resulting raw bytes
    }
    // ...
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    loige 47

    View full-size slide

  48. loige
    cargo add base45
    # Cargo.toml
    # ...
    [dependencies]
    base45 = "3.0.0"
    48

    View full-size slide

  49. fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    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 {
    base45::decode(data).unwrap() // IRL use a Result!
    }
    // ...
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    base45::decode(data).unwrap() // IRL use a Result!
    fn main() {
    1
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    2
    let no_prefix = remove_prefix(cert_data);
    3
    let decoded = decode_base45(no_prefix);
    4
    todo!()
    5
    // 3. Zlib decompress
    6
    // 4. Parse CWT
    7
    // 5. Parse CWT Payload as CBOR
    8
    }
    9
    10
    fn decode_base45(data: &str) -> Vec {
    11
    12
    }
    13
    14
    // ...
    15
    fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    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 {
    base45::decode(data).unwrap() // IRL use a Result!
    }
    // ...
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    loige 49

    View full-size slide

  50. fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    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) -> Vec {
    todo!()
    // decompress using zlib inflate
    }
    // ...
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    let decompressed = decompress(decoded);
    fn main() {
    1
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    2
    let no_prefix = remove_prefix(cert_data);
    3
    let decoded = decode_base45(no_prefix);
    4
    5
    todo!()
    6
    // 4. Parse CWT
    7
    // 5. Parse CWT Payload as CBOR
    8
    }
    9
    10
    fn decompress(data: Vec) -> Vec {
    11
    todo!()
    12
    // decompress using zlib inflate
    13
    }
    14
    15
    // ...
    16
    fn decompress(data: Vec) -> Vec {
    todo!()
    // decompress using zlib inflate
    }
    fn main() {
    1
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    2
    let no_prefix = remove_prefix(cert_data);
    3
    let decoded = decode_base45(no_prefix);
    4
    let decompressed = decompress(decoded);
    5
    todo!()
    6
    // 4. Parse CWT
    7
    // 5. Parse CWT Payload as CBOR
    8
    }
    9
    10
    11
    12
    13
    14
    15
    // ...
    16
    fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    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) -> Vec {
    todo!()
    // decompress using zlib inflate
    }
    // ...
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    loige 50

    View full-size slide

  51. loige
    cargo add inflate
    # Cargo.toml
    # ...
    [dependencies]
    base45 = "3.0.0"
    inflate = "0.4.5"
    51

    View full-size slide

  52. fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    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) -> Vec {
    inflate::inflate_bytes_zlib(data.as_slice()).unwrap()
    // IRL use a Result!
    }
    // ...
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    inflate::inflate_bytes_zlib(data.as_slice()).unwrap()
    // IRL use a Result!
    fn main() {
    1
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    2
    let no_prefix = remove_prefix(cert_data);
    3
    let decoded = decode_base45(no_prefix);
    4
    let decompressed = decompress(decoded);
    5
    todo!()
    6
    // 4. Parse CWT
    7
    // 5. Parse CWT Payload as CBOR
    8
    }
    9
    10
    fn decompress(data: Vec) -> Vec {
    11
    12
    13
    }
    14
    15
    // ...
    16
    fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    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) -> Vec {
    inflate::inflate_bytes_zlib(data.as_slice()).unwrap()
    // IRL use a Result!
    }
    // ...
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    loige 52

    View full-size slide

  53. Are we on the right track?
    loige
    fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    let no_prefix = remove_prefix(cert_data);
    let decoded = decode_base45(no_prefix);
    let decompressed = decompress(decoded);
    println!("{}", String::from_utf8_lossy(&decompressed));
    }
    // ...
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    println!("{}", String::from_utf8_lossy(&decompressed));
    fn main() {
    1
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    2
    let no_prefix = remove_prefix(cert_data);
    3
    let decoded = decode_base45(no_prefix);
    4
    let decompressed = decompress(decoded);
    5
    6
    7
    }
    8
    9
    // ...
    10
    53

    View full-size slide

  54. Are we on the right track?
    loige
    We are starting to see some
    readable info!
    🤩
    54

    View full-size slide

  55. fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    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) -> Vec {
    todo!()
    // Parse raw bytes as CBOR.
    // Extract and return the raw bytes representing
    // the CWT payload
    }
    // ...
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    let cwt_payload = get_cwt_payload(decompressed);
    fn main() {
    1
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    2
    let no_prefix = remove_prefix(cert_data);
    3
    let decoded = decode_base45(no_prefix);
    4
    let decompressed = decompress(decoded);
    5
    6
    todo!()
    7
    // 5. Parse CWT Payload as CBOR
    8
    }
    9
    10
    fn get_cwt_payload(data: Vec) -> Vec {
    11
    todo!()
    12
    // Parse raw bytes as CBOR.
    13
    // Extract and return the raw bytes representing
    14
    // the CWT payload
    15
    }
    16
    17
    // ...
    18
    fn get_cwt_payload(data: Vec) -> Vec {
    todo!()
    // Parse raw bytes as CBOR.
    // Extract and return the raw bytes representing
    // the CWT payload
    }
    fn main() {
    1
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    2
    let no_prefix = remove_prefix(cert_data);
    3
    let decoded = decode_base45(no_prefix);
    4
    let decompressed = decompress(decoded);
    5
    let cwt_payload = get_cwt_payload(decompressed);
    6
    todo!()
    7
    // 5. Parse CWT Payload as CBOR
    8
    }
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // ...
    18
    fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    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) -> Vec {
    todo!()
    // Parse raw bytes as CBOR.
    // Extract and return the raw bytes representing
    // the CWT payload
    }
    // ...
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    loige 55

    View full-size slide

  56. loige
    cargo add ciborium
    # Cargo.toml
    # ...
    [dependencies]
    base45 = "3.0.0"
    ciborium = "0.2.0"
    inflate = "0.4.5"
    56

    View full-size slide

  57. use ciborium::{de::from_reader, value::Value};
    fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    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) -> Vec {
    let parsed: Value = from_reader(data.as_slice()).unwrap();
    println!("{:?}", parsed);
    todo!()
    }
    // ...
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    use ciborium::{de::from_reader, value::Value};
    1
    2
    fn main() {
    3
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    4
    let no_prefix = remove_prefix(cert_data);
    5
    let decoded = decode_base45(no_prefix);
    6
    let decompressed = decompress(decoded);
    7
    let cwt_payload = get_cwt_payload(decompressed);
    8
    todo!()
    9
    // 5. Parse CWT Payload as CBOR
    10
    }
    11
    12
    fn get_cwt_payload(data: Vec) -> Vec {
    13
    let parsed: Value = from_reader(data.as_slice()).unwrap();
    14
    println!("{:?}", parsed);
    15
    todo!()
    16
    }
    17
    18
    // ...
    19
    let parsed: Value = from_reader(data.as_slice()).unwrap();
    println!("{:?}", parsed);
    use ciborium::{de::from_reader, value::Value};
    1
    2
    fn main() {
    3
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    4
    let no_prefix = remove_prefix(cert_data);
    5
    let decoded = decode_base45(no_prefix);
    6
    let decompressed = decompress(decoded);
    7
    let cwt_payload = get_cwt_payload(decompressed);
    8
    todo!()
    9
    // 5. Parse CWT Payload as CBOR
    10
    }
    11
    12
    fn get_cwt_payload(data: Vec) -> Vec {
    13
    14
    15
    todo!()
    16
    }
    17
    18
    // ...
    19
    loige 57

    View full-size slide

  58. // ...
    fn get_cwt_payload(data: Vec) -> Vec {
    // 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()
    }
    // ...
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    let parsed: Value = from_reader(data.as_slice()).unwrap();
    // ...
    1
    2
    fn get_cwt_payload(data: Vec) -> Vec {
    3
    // IRL avoid .unwrap() like hell and propagate errors correctly!
    4
    5
    let (tag, arr) = parsed.as_tag().unwrap();
    6
    assert_eq!(tag, 18);
    7
    let arr = arr.as_array().unwrap();
    8
    let payload = arr[2].as_bytes().unwrap();
    9
    payload.clone()
    10
    }
    11
    12
    // ...
    13
    let (tag, arr) = parsed.as_tag().unwrap();
    assert_eq!(tag, 18);
    // ...
    1
    2
    fn get_cwt_payload(data: Vec) -> Vec {
    3
    // IRL avoid .unwrap() like hell and propagate errors correctly!
    4
    let parsed: Value = from_reader(data.as_slice()).unwrap();
    5
    6
    7
    let arr = arr.as_array().unwrap();
    8
    let payload = arr[2].as_bytes().unwrap();
    9
    payload.clone()
    10
    }
    11
    12
    // ...
    13
    let arr = arr.as_array().unwrap();
    // ...
    1
    2
    fn get_cwt_payload(data: Vec) -> Vec {
    3
    // IRL avoid .unwrap() like hell and propagate errors correctly!
    4
    let parsed: Value = from_reader(data.as_slice()).unwrap();
    5
    let (tag, arr) = parsed.as_tag().unwrap();
    6
    assert_eq!(tag, 18);
    7
    8
    let payload = arr[2].as_bytes().unwrap();
    9
    payload.clone()
    10
    }
    11
    12
    // ...
    13
    let payload = arr[2].as_bytes().unwrap();
    payload.clone()
    // ...
    1
    2
    fn get_cwt_payload(data: Vec) -> Vec {
    3
    // IRL avoid .unwrap() like hell and propagate errors correctly!
    4
    let parsed: Value = from_reader(data.as_slice()).unwrap();
    5
    let (tag, arr) = parsed.as_tag().unwrap();
    6
    assert_eq!(tag, 18);
    7
    let arr = arr.as_array().unwrap();
    8
    9
    10
    }
    11
    12
    // ...
    13
    // ...
    fn get_cwt_payload(data: Vec) -> Vec {
    // 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()
    }
    // ...
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    loige 59

    View full-size slide

  59. use ciborium::{de::from_reader, value::Value};
    fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    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) -> Value {
    // parse the binary data as CBOR
    todo!()
    }
    // ...
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    let parsed_payload = parse_cwt_payload(cwt_payload);
    use ciborium::{de::from_reader, value::Value};
    1
    2
    fn main() {
    3
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    4
    let no_prefix = remove_prefix(cert_data);
    5
    let decoded = decode_base45(no_prefix);
    6
    let decompressed = decompress(decoded);
    7
    let cwt_payload = get_cwt_payload(decompressed);
    8
    9
    }
    10
    11
    fn parse_cwt_payload(data: Vec) -> Value {
    12
    // parse the binary data as CBOR
    13
    todo!()
    14
    }
    15
    16
    // ...
    17
    fn parse_cwt_payload(data: Vec) -> Value {
    // parse the binary data as CBOR
    todo!()
    }
    use ciborium::{de::from_reader, value::Value};
    1
    2
    fn main() {
    3
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    4
    let no_prefix = remove_prefix(cert_data);
    5
    let decoded = decode_base45(no_prefix);
    6
    let decompressed = decompress(decoded);
    7
    let cwt_payload = get_cwt_payload(decompressed);
    8
    let parsed_payload = parse_cwt_payload(cwt_payload);
    9
    }
    10
    11
    12
    13
    14
    15
    16
    // ...
    17
    loige 60

    View full-size slide

  60. use ciborium::{de::from_reader, value::Value};
    fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    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) -> Value {
    from_reader(data.as_slice()).unwrap()
    }
    // ...
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    from_reader(data.as_slice()).unwrap()
    use ciborium::{de::from_reader, value::Value};
    1
    2
    fn main() {
    3
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    4
    let no_prefix = remove_prefix(cert_data);
    5
    let decoded = decode_base45(no_prefix);
    6
    let decompressed = decompress(decoded);
    7
    let cwt_payload = get_cwt_payload(decompressed);
    8
    let parsed_payload = parse_cwt_payload(cwt_payload);
    9
    println!("{:#?}", parsed_payload);
    10
    }
    11
    12
    fn parse_cwt_payload(data: Vec) -> Value {
    13
    14
    }
    15
    16
    // ...
    17
    println!("{:#?}", parsed_payload);
    use ciborium::{de::from_reader, value::Value};
    1
    2
    fn main() {
    3
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    4
    let no_prefix = remove_prefix(cert_data);
    5
    let decoded = decode_base45(no_prefix);
    6
    let decompressed = decompress(decoded);
    7
    let cwt_payload = get_cwt_payload(decompressed);
    8
    let parsed_payload = parse_cwt_payload(cwt_payload);
    9
    10
    }
    11
    12
    fn parse_cwt_payload(data: Vec) -> Value {
    13
    from_reader(data.as_slice()).unwrap()
    14
    }
    15
    16
    // ...
    17
    use ciborium::{de::from_reader, value::Value};
    fn main() {
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    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) -> Value {
    from_reader(data.as_slice()).unwrap()
    }
    // ...
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    loige 61

    View full-size slide

  61. Ok, let's make it more readable...
    😅
    loige
    cargo add serde_json
    63

    View full-size slide

  62. 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
    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());
    }
    // ...
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    use serde_json::to_string_pretty;
    use ciborium::{de::from_reader, value::Value};
    1
    2
    3
    fn main() {
    4
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    5
    let no_prefix = remove_prefix(cert_data);
    6
    let decoded = decode_base45(no_prefix);
    7
    let decompressed = decompress(decoded);
    8
    let cwt_payload = get_cwt_payload(decompressed);
    9
    let parsed_payload = parse_cwt_payload(cwt_payload);
    10
    println!("{}", to_string_pretty(&parsed_payload).unwrap());
    11
    }
    12
    13
    // ...
    14
    println!("{}", to_string_pretty(&parsed_payload).unwrap());
    use ciborium::{de::from_reader, value::Value};
    1
    use serde_json::to_string_pretty;
    2
    3
    fn main() {
    4
    let cert_data = "HC1:NCFOXN%TSMAHN-H9QCGDSB5QPN9OO3:D4$X4-365KN-TMLV4
    5
    let no_prefix = remove_prefix(cert_data);
    6
    let decoded = decode_base45(no_prefix);
    7
    let decompressed = decompress(decoded);
    8
    let cwt_payload = get_cwt_payload(decompressed);
    9
    let parsed_payload = parse_cwt_payload(cwt_payload);
    10
    11
    }
    12
    13
    // ...
    14
    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
    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());
    }
    // ...
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    loige 64

    View full-size slide

  63. All the code:
    loige.link/green-code
    loige 66

    View full-size slide

  64. A better (& more complete) implementation
    as a Rust library
    loige
    github.com/rust-italia/dgc
    67

    View full-size slide

  65. Exercise for the viewer:
    Try to validate the signature
    loige
    🔑 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 for crypto!
    ring
    (Spoiler: We implemented some of this stuff in the library!)
    dgc
    68

    View full-size slide

  66. Is all this stuff legal?
    😰
    loige
    👀 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!)
    69

    View full-size slide

  67. Cover Picture by on
    ❤ Huge thanks to for some precius review sessions and many pull requests!
    ❤ Thanks to , , , , for reviews and suggestions.
    FPVmat A Unsplash
    rust-italia
    @gbinside @88_eugen @AlleviTommaso @npmccallum @pelger
    loige
    ☝ nodejsdp.link
    loige.link/rust-green
    THANK YOU!

    70

    View full-size slide