Extracting Data from the Swiss Vaccination Certificate

Tags: programming

Published on
« Previous post: ‘What is a Manifold?’, Redux: Some … — Next post: A Short Round-Up of Topology-Based … »

With the Swiss COVID vaccination campaign picking up some steam, I am grateful to have finally received two doses of a vaccine.1 I was mightily impressed when I also received a nice certificate with a QR code that I may now use to transfer information about my vaccination to a smartphone app—the idea being that every vaccinated person can present this as a proof. I tried to scan the QR code with my normal camera app and instead of a link or some other textual information, I just received what looked like gibberish at first glance. My curiosity thus being piqued, I searched for some way to extract more meaningful information from this code and quickly stumbled over Tobias Girstmair’s awesome description of how he decoded the Austrian vaccination certificate. In true ‘duct-tape programmer’ fashion, I was looking for a way to extend Tobias’s great description so that I could extract all this information with command-line tools. This post shows what I came up with.

Ingredients

You need a few utilities for this:

  1. zbar, for reading the QR code.
  2. A base45 decoder such as python-base45.
  3. A way to extract zlib data, such as pigz or a version of OpenSSL with zlib support enabled.
  4. rq, for working with CBOR data.
  5. A set of POSIX core tools, namely cut and tr.

If you are on Mac OS X, all of these—except python-base45—can be installed using Homebrew.

Recipe

Assuming your QR code is a file called QR.png, this is how to extract and nicely visualise its information:

zbarimg --raw QR.png    \
    | cut -b5-          \
    | tr -d '\n'        \
    | base45 --decode   \
    | pigz -d           \
    | cut -b20-         \
    | cut -b1-384       \
    | rq -c

Let’s briefly dissect these commands to uncover their meaning:

  1. We extract the raw QR code data.
  2. We remove the outermost header (we are not interested in verifying the signature or the authenticity of that certificate; we just want the data).
  3. We remove the newline (to enable downstream processing).
  4. We decode the data (it uses base45 for efficiency purposes).
  5. We uncompress the data (it is compressed using zlib).
  6. We remove some more header information (this was based on my educated guess about the length of the header section; you can also just keep the original data and look at it using xxd to see that I did not remove anything relevant here).
  7. We remove the digital signature (we could also leave it in, but then the last command will not result in a pretty output; this is the duct-tape version, after all).
  8. We finally pretty-print everything.

This should result in an output like this:2

{
  "1": {
    "dob": "1943-02-01",
    "nam": {
      "fn": "Müller",
      "fnt": "MUELLER",
      "gn": "Céline",
      "gnt": "CELINE"
    },
    "v": [
      {
        "ci": "urn:uvci:01:CH:2987CC9617DD5593806D4285",
        "co": "CH",
        "dn": 2,
        "dt": "2021-04-30",
        "is": "Bundesamt für Gesundheit (BAG)",
        "ma": "ORG-100031184",
        "mp": "EU/1/20/1507",
        "sd": 2,
        "tg": "840539006",
        "vp": "1119349007"
      }
    ],
    "ver": "1.0.0"
  }
}

There you have it—a pretty amazing amount of information contained in a nice QR code. The format appears to follow the Electronic Health Certificate to the letter,3 which is surprising to me because the relationship between the EU and Switzerland is somewhat complicated. I am glad to see that in these times, authorities are rallying under a common banner and creating interoperable standards.

Aftermath

This was fun to fiddle around with—I find command-line data processing pipelines neat because they tend to contain a mixture of tools from different decades. That being said, if you want to parse such certificates properly, you should read the most recent specification. This duct-tape version should not be used in production.

Stay healthy, until next time!


  1. Insert obligatory 5G/telepathy joke here. ↩︎

  2. This is not my real certificate. Your output might also contain a few additional lines that only specify some information about the issuer. Adjust the first cut command to remove them. ↩︎

  3. I got my test data from this repository, but the code as shown here also works with my personal certificate. ↩︎