How would the NZ COVID Certificate work?

The NZ Government announced today that they will be doing a vaccine passport soon, which compromises either a phone-based QR code, or a paper-based one.

The QR code is scanned by a "scanner app", which then reports if the person is vacinated or not.

Sounds good. Also sounds like it could be faked easily, or forged easily.

Here's some notes on why thats unlikely. Note that I don't know if this is the exact system the MoH is using, but given that it's a public "standard", and we want to be able to use these overseas, it's likely that it is.

I'll try and explain this without too much technical jargon, but you need some to understand it. See the end for some explanations.

If you have a rough idea of how a JWT (Javascript Web Token) works, you're 99% there already. If those letters mean nothing - don't worry.

This is purely the technical side - how it's implemented and enforced is up to the government and businesses.

Note: It appears we are going to be using the EU format, with possible NZ specific modifications. The basic flow here is still correct, its just the payload and how the keys are acquired that would differ. The EU spec is linked to below, and they have a good FAQ on their GitHub site.

Stuff you DO need to know.

You need to know what encryption is, at a very high level. And you need to know what Public Key Encryption and a Digital Signature is, again, at a high level. Jump down to the bottom if you don't, there is a glossary at the end which explains these.

How would this work?

So, the flow I see happening here is:

  • As a member of the public, I go to the vaccine passport site, and get my QR code somehow - on my phone, print a PDF, whatever. The mechanism doesn't matter as long as it's scannable.
  • I go somewhere, and I present this QR code to the door person.
  • They scan the QR code, and the screen shows "Nic Wise" and "Vacinated as of 01/10/2021", for example.
  • They either trust that, or ask for some kind of ID with my name on it, to match up, and I get in, or not.

This is a problem, because I (the MOH) have something, which I need to give to you (Citizen X), so you can give it to someone else (The Bouncer).

Because, The Bouncer needs to be able to verify what you present them is what I gave you, and hasn't been altered.

And we (the MOH and The Bouncer) have to assume you are not trustworthy at all.

What are they scanning, and why can't it be easily faked?

A QR code is a scannable representation of a block of data - and quite a bit of data, too, the most dense ones can be up to 2kb of data.

Once it's scanned, you have a block of data. Lets say it's this, which is base64 encoded:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJBQkMxMjM0Iiwib
mFtZSI6Ik5pYyBXaXNlIiwidmFjY2luYXRlZCI6dHJ1ZSwiZG9iIjoiMDEwNTc
0Iiwia2V5aWQiOjEsImlhdCI6MTUxNjIzOTAyMn0.RfATCfzGFdI9eZlmAV9Mj
vao2mR3l4rz7bxQzMDB3HQ

(this is actualy a JWT - I think this will be using a similar idea, but with different constraints. This is to illustrate what the block would look like)

which decodes to three seperate parts when you seperate on the .:

{
  "alg": "ES256",
  "typ": "???"
}
{
  "ver": "1.3.0",
  "nam": {
    "fn": "Smith-Jones",
    "fnt": "SMITH<JONES",
    "gn": "Charles Edward",
    "gnt": "CHARLES<EDWARD"
  },
  "dob": "1964-01-01",
  "v": [
    {
      "tg": "840539006",
      "vp": "1119349007",
      "mp": "EU/1/20/1507",
      "ma": "ORG-100031184",
      "dn": 1,
      "sd": 2,
      "dt": "2021-06-11",
      "co": "NL",
      "is": "Ministry of Health Welfare and Sport",
      "ci": "URN:UVCI:01:NL:DADFCC47C7334E45A906DB12FD859FB7#1"
    }
  ]
}

(This is an example from the EU spec)

The last part is a signature:

sign(base64(first part) + "." + base64(second part)) with private-key

This combines the first and second parts, and then signs them using a private key which is kept secret (by the MoH in this case).

The result is given to the user as a QR code.

If you remember back to signatures and hashing, if you change even one bit of the message (change an a to a b) the resulting signature is wildly different, and its not practical to make any change without knowning the private key.

The key property of public key cryptography - that while you can keep the private part secret, the public part can be shared - means you can publish the public key in a well known place, and it can then be used to verify that the payload has not been changed.

(I'm hand waving over the signing part here - plenty of way smarter people than me have written a lot about it, have a look for digital signatures, SHA256 and HMAC on Wikipedia)

So, you have a easily readable block of useful info (payload) and some metadata about how to validate it (the header), and the signature.

You'll notice that ci parameter there. Thats a reference to the key used to sign the document. The scanner app would look this key up in it's local store - or find it in an online registry - but it mostly comes down to "use this key".

You now have all the bits you need.

Verifying that what you have is valid

Now, you have the decoded version of the token, and you can find the public key from is ci property - either you have it in the app, or you have a way to get it by passing the ci value in, and getting a key back. Likely, this will use a variant of the JWK (Javascript Web Key) standard.

With this, you can now do the signature verification:

verify-signature(base64(first part) + "." + base64(second part)) with public-key

This function will tell you either if it's valid, or not. Again, if even one bit is changed - and a to a b - it'll fail and not be valid.

Now, you could change the ci in your certificate, and get the "scanning app" to look it up some how, but I'd assume that the MoH have their URIs encoded in the app, and if the expected ci doesn't match the one in the signed payload, they'd fail the validation.

This also means that the public keys could be encoded in the app - it doesn't need an internet connection to check that the signature is valid, only the public keys, which can be loaded in advance and cached locally. Rather handy for Splore or Rhythm and Vines, or if the MoH servers go down.

If you were crossing an international border, there would likely be an agreement between countries which included where to expect the public key to be located. The one above appears to be from the Netherlands, but the NZ one might look like:

URN:UVCI:01:NZ:DADFCXXXXXXXDECAFBADXXXXX12FD859FB7#1

Assuming there is no key locally on the device which matches, the scanner app would know where a central registry is (which might be run by the MoH in NZ, or the EU in the EU), pass that string in, and it'd get a key back. If it's fake, it gets no key back and the validation fails.

Now that the signature has been verified, it's up to the person with the scanner in their hand to do their job.

One thing the Smart Health Card and EU specs are specific about is: the payload should not contain certain information: No goverment ID (NHI in our case); no phone number or address; no other health info. And it's not designed as a form of identification. You need some other form of ID to go with it.

There are a number of other validaton options they can do, and a few ways to do key rotation, but thats the basics of it. It's quite an elegant system, especially if it's implemented with some guardrails which are all pretty well known from technologies like JWT.

  • FHIR is a standard for encoding health data.
  • Apple had a session at WWDC2021 about Verifiable Health Records. This uses the same technology, specificially Smart Health Cards.
  • The actual US standard for smart health cards: Smart Health Cards - this is what I thought we'd be using. It also integrates into iOS 15 nicely.
  • California's implementation - API, QR, UI - if you want to drop into some C# and Javascript.
  • The WHO have their own spec
  • As does the EU - this is the one we will be using, or very close to it.

Glossary

Base64 encoding: this is a basic method of converting a block of binary data into a text representation. Useful for cases where the method of transport (often email) can't handle binary data, but it also makes it 30% larger.

Encryption: Turning plaintext (hello) into a cyphertext (aGVsbG8K), when combined with a key (usually a password) - plaintext + key => cyphertext, and have it be practicly impossible to reverse the process without knowing the key.

Given the same key, it can easily be turned back into the plaintext - cyphertext + key => plaintext.

Without knowing the key, you'd have to try every possible key (brute force attack). That takes a massive amount of time and resources.

This is also called symmetric key encryption as the key is the same on both sides. This is the bases of most common forms of encryption - HTTPS, the encryption on your phone, password protected zip files.

Public Key Encryption: Same idea - plaintext + key => cyphertext and cyphertext + key => plaintext. Except that the first key (the public key) is different, but mathmaticly related to, the second key (the private key).

So you can share the public key, and anyone can use it to encrypt a message and send it to you, and only you (with the private key) can decrypt it.

This work the other way - you can "encrypt" a message with your private key, and send the plaintext message and the encrypted version, and anyone with your public key can verify that the plaintext has not been changed. This is a "digital signature", which is what is used here.

This is the basis of HTTPS - but only the initial setup, as public key encryption is a lot slower than symmetric key encryption, so we use public key encryption to exchange a symmetric key, which is then used for the rest of the conversation.

Nic Wise

Nic Wise

Auckland, NZ