Educated Guesswork

A quick look at the New Zealand Vaccine Pass

Posted 2021-11-23

A reader alerted me to New Zealand's vaccine pass system (spec here). Like the other vaccine passport systems I've seen (New York, California, EU), it's a digitally signed credential, but (of course) it's also slightly different and so incompatible. In this case, it's a CBOR Web Token (CWT). The NZ system is straight CBOR and encodes data in Base32 without any compression. They argue that this is better than the alternatives for some implementation and interoperability reasons.

Here's a look at their example credential converted to JSON:

{
"iss": "did:web:example.nz",
"nbf": 1516239022,
"exp": 1516239922,
"jti": "urn:uuid:cc599d04-0d51-4f7e-8ef5-d7b5f8461c5f",
"vc": {
"@context": [ "https://www.w3.org/2018/credentials/v1", "https://nzcp.covid19.health.nz/contexts/v1" ],
"version": "1.0.0",
"type": [ "VerifiableCredential", "PublicCovidPass" ],
"credentialSubject": {
"givenName": "John Andrew",
"familyName": "Doe",
"dob": "1979-04-14"
}
}
}

Information In the Pass #

The first thing to note here is that the only real user-specific information here is in the credentialSubject field, which just contains the vaccinated person's identity. This is different from the other systems I've looked at which contain information about the person's medical status, such as when they were vaccinated, recovered from COVID, or had a negative test. I can understand why you might want a more limited credential for privacy reasons, but this seems like an unfortunate piece of inflexibility.

This is especially true now that we know that vaccine effectiveness wanes quite a bit over time. Imagine you want to require that someone be either recently vaccinated or had a recent booster; unlike other systems this credential doesnt allow the verifier to determine that directly. There is an expiration date (more on this below, which you could sort of use for this purpose by having any pass expire in six months (the site says that's how long they are good for), but there are several problems with using it that way.

First, we don't yet know how long vaccination will be effective or what future requirements will be. For instance, there has been some speculation that boosters will confer persistent immunity past 6 months, in which case you might say that "fully vaccinated" meant "within 6 months of the initial dose(s) or after the booster". There's no way to represent this with a single "expiration" number, and so you could easily create a situation where you issue passes to people today that are either too short or too long.

Second, it's not clear that people will get their passes immediately after being vaccinated. If they don't, the issuer then has to decide between having the pass last for six months from the time of issuance or six months after they were vaccinated. the first option doesn't work well if you want the pass to reflect the duration of immunity and the second is going to result in people having passes with varying durations, as well as being kind of a record-keeping hassle if you ever have to revoke passes (one of the purposes of expiration is to allow you not to revoke credentials which have already expired). Moreover, if you have the pass expire six months from being vaccinated, you've just leaked the vaccination date, which is presumably what the system designers were trying to avoid by omitting that information from the pass.[1]

Effectively, this design just encodes all the policy decisions in the decision to issue the pass, at which point they are fixed for any individual pass and can't be addressed except by issuing new passes. Obviously, you can issue new passes, credentials, but for the reasons I've talked about before, this can be quite inconvenient. It's not clear to me if New Zealand has built a system to let people automatically update their vaccine pass, but based on their site, it looks like it's just a QR code that you print out or add to Apple Wallet, so presumably not. And of course, one of the big advantages of these signed QR codes is that you can just print them out, and you can't automatically update paper. And of course, if the policy gets more restrictive, then you might be faced with mass revoking a lot of passes. It seems like it would be better to just put the right information in the pass upfront to allow verifiers to enforce policy (and to be updated with new policies), rather than requiring people to get new passes when policy changes.

Expiration #

The actual intended value of the exiration date (exp field) is quite confusing. Here's what the spec says:

Expiry, this claim represents the datetime at which the pass is considered expired by the party who issued it, this claim MUST be present and its value MUST be a timestamp encoded as an integer in the NumericDate format (as specified in [RFC8392] section 2). Verifying parties MUST validate that the current datetime is before the value of this claim and if not they MUST reject the pass as being expired. This claim is mapped to the Credential Expiration Date property in the W3C VC standard. The claim key for exp of 4 MUST be used.

Given that the passes are supposed to expire after six months, we might expect that this will be six months from the issuance date, but the example credential above appears to expire in 15 minutes)[2] On the other hand, the "valid worked example" in the specification has the following dates:

  • not before: 1635883530 (2021-11-02T20:05:30.000Z)
  • not after: 1951416330 (2031-11-02T20:05:30.000Z)

In other words, this pass is valid for 10 years.[3] This leaves me with no idea about what is going to appear in the expiry field of a real passport.) In a design which had the vaccination date in the pass, then the purpose of the expiry field is basically to limit the period during which you have to care about a given credential. For instance, if you issue a bunch of credentials with a key and lifetime of one year and then two years later that key is compromised, you don't need to do anything because the credentials are already invalid. Having credentials expire also makes updating easier because it gives you a hard lifetime on support for old credential, thus making it somewhat easier to update to a new format you know that after a certain time all credentials will be new. With that said, 10 years is a very long time; by contrast WebPKI certificates must have a lifetime of no longer than 398 days. Given the maturity of these systems, 1-2 years seems more appropriate.

If anyone has a copy of a valid NZ pass, I'd be interested to see when it expires.

UUID #

Second, the pass contains a universally unique identifier (UUID). They recommend that it be a "version 4" UUID, which just means that it's randomly generated. It's not clear to me why this is needed: it's not required by the CWT specification and you can make a unique id from the pass just by hashing it, which (statistically) guarantees uniqueness as long as the contents are unique. I don't think this is harmful, but it's also not clear to me what it's for.

Keys #

As with the California system, the pass indicates which signing keys were used but that must be checked against a list which is statically configured into the application. The pass carries this information with a did:web URIs, which must be on the following list:

[
"did:web:nzcp.identity.health.nz"
]

did:web is a specific binding ("method") of the W3C Decentralized Identifier (DID) specification. The way to read this is that the key file (formatted as a DID) is located at https://nzcp.identity.health.nz/.well-known/did.json, the current contents of which are:

{
"id": "did:web:nzcp.identity.health.nz",
"@context": [
"https://w3.org/ns/did/v1",
"https://w3id.org/security/suites/jws-2020/v1"
],
"verificationMethod": [
{
"id": "did:web:nzcp.identity.health.nz#z12Kf7UQ",
"controller": "did:web:nzcp.identity.health.nz",
"type": "JsonWebKey2020",
"publicKeyJwk": {
"kty": "EC",
"crv": "P-256",
"x": "DQCKJusqMsT0u7CjpmhjVGkHln3A3fS-ayeH4Nu52tc",
"y": "lxgWzsLtVI8fqZmTPPo9nZ-kzGs7w7XO8-rUU68OxmI"
}
}
],
"assertionMethod": [
"did:web:nzcp.identity.health.nz#z12Kf7UQ"
]
}

This is pulling in a whole lot of specification machinery, but at the end of the day what it means is that there is one valid signing key (z12Kf7UQ), for ECDSA with the P-256 curve. Importantly, this structure does support multiple keys, for instance for key rollover. The DID document can contain more than one key, each with a different key id, and the signed contains a key id (kid) which identifies the key used to sign it. This lets you introduce a new key or a new algorithm, as with the VCI and the EU Green Card, which is an important piece of future flexibility,

Summary #

As I mentioned earlier, this design is conceptually really similar to other vaccine passport systems. Most the choices they have made (Base32 encoding, no compression, all-CBOR, using DIDs for the keys) seem reasonable, even if they differ from other systems or you or I might have made different ones. The one decision that seems really worse is to just have the user's identity and omit their vaccination details and just have their identity. The specification doesn't provide any rationale for this, but as described above, it seems clearly less flexible.

More generally, it's kind of disappointing to see all these different vaccine passport systems be subtly different instead of everyone converging on a common specification. I'm not saying that the encoding doesn't matter at all, but it seems like it would be better if we instead had a single system (although potentially with disjoint keys). This would let us focus engineering effort and analysis on that one system, as well as providing the opportunity for interoperability between credentials issued by different jurisdictions.


  1. Note that you could also backdate the "not before" to when someone was vaccinated, but that has the same privacy issue. ↩︎

  2. The "not before" (nbf) value shown above is actually back in 2018 (2018-01-18T01:30:22.000Z), which suggests this example was constructed by hand. ↩︎

  3. Incidentally, it's quite bad to have the human readable and machine readable expiration dates mismatch, so if the printed expiration is six months and the pass says 10 years, that's not good. ↩︎


Recent Posts

More ...