What's in California's Vaccine Passport?
Posted by ekr on 23 Jun 2021
Last week, California rolled out their new digital COVID Vaccine Record (aka vaccine passport). This credential is based on the Vaccine Credentials Initiative SMART Health Cards Framework. They provide a fairly complete specification as well as sample code, so it's pretty easy to figure out what's in here.
At a high level, the credential is a digitally signed value formatted as a JSON Web Token and then encoded into a QR code.[1] A JWT consists of three pieces:
- A header value, containing some meta-information
- The payload to be signed
- The signature
We can mostly ignore the header and the signature, because what matters here is the payload. I go through this in some detail below but you don't need to wade through that to get the high points. If you ignore the Verifiable Credentials machinery, this credential contains three major pieces of information:
- The patient's identity (name and date of birth)
- The various immunization events, consisting of
- The vaccine type (I think, see below)
- The lot number
- Where it was performed
- The date of injection
This seems fairly sensible and is really all you need in a system like this. Arguably, it's more than you need: people don't need to know where you were vaccinate, the lot number or arguably even vaccine type in order to know that you were vaccinated (given that some vaccines appear to be more effective than others, I could imagine in principle wanting to know the vaccine type). Here, we have to distinguish here between what you might want to have for your own records and what others might be entitled to know about you. For instance, if it turned out that there was a bad vaccine lot, then you might want your health care provider to be able to determine that and revaccinate you, but it's not necessary for someone to know that to let you into a bar. I can imagine a number of technical approaches to addressing the desire for different levels of access, but realistically it's not clear that any of this information is that sensitive either (though you'll note that I've redacted it below).
This is all more or less as advertised by California Governor Gavin Newsom:
"It’s not a passport, it's not a requirement, it's just the ability now to have an electronic version of that paper version, so you'll hear more about that in the next couple of days," he said.
I sympathize with the desire not to call it a "passport" but this is effectively what a "vaccine passport" has come to mean: a verifiable electronic record of vaccination.
This brings me to the topic of "verifiable". This credential is digitally signed by a key which appears to belong to the State of California Department of Public health (by which I mean it's hosted on their Web Site). However, what I don't see is how you actually read or verify it. I just wrote some quick code to pull it apart (you don't even need to verify it to do that, though of course in real life you have to) but the idea here is that you're supposed to have some kind of mobile app on your smartphone and you just point it at the credential and it will tell you the contents and whether it's valid. However, after some digging around, I didn't find a recommended app to use to validate it, which kind of seems to really diminish the usefulness of the system.
Obviously, it's possible to write your own app to verify these credentials (the sample code provided by VCI gets you pretty close), but that's also clearly unreasonable to ask people to do. Moreover, as I mentioned earlier, an important part of such an app is embedding the trusted credential issuers, which, for obvious reasons, is information you need to get externally, not from the credential itself.
Hard Hat Area Below
The important part of this object is the payload, which is a JSON structure that has been compressed with zlib and then base64 encoded. First we have the outer wrapper:
{
"iss": "https://myvaccinerecord.cdph.ca.gov/creds",
"nbf": ...,
"vc": {
"type": [
"https://smarthealth.cards#health-card",
"https://smarthealth.cards#immunization",
"https://smarthealth.cards#covid19"
],
...
}
}
This section contains three things:
-
The "issuer" of the credential, in this case the California Department of Public Health. This URL also tells you where to get the public key to use to verify the credential (it's at https://myvaccinerecord.cdph.ca.gov/creds/.well-known/jwks.json).
-
The "not before" date (
nbf
) -
The "vc" structure which represents the rest of the data, which in this case is a W3C Verifiable Credentials object. The values inside tell us it's a COVID-19 immunization record.
Note that there's already something a bit inconvenient here in that you need the issuer URL in order to get its keys, but in order to get that you need to (1) decompress and parse the payload without verifying the signature (2) retrieve the keys (3) verify the signature. This is a bit clunky but once you know that it's not that bad.
Inside that container, you have a bunch of other containers:
"credentialSubject": {
"fhirVersion": "4.0.1",
"fhirBundle": {
"resourceType": "Bundle",
"type": "collection",
"entry": [
...
]
}
}
}
What's going on here is that this credential is being built on two frameworks: This is all machinery from Verifiable Credentials and Fast Health Interoperability Resources, each of which is fairly generic, so you end up pulling in a lot of machinery that we're not really making use of. Obviously, you could put a lot of different things in these containers, but in this case all there is is this list of entries, which contains everything else. First, we have:
{
"fullUrl": "resource:0",
"resource": {
"resourceType": "Patient",
"name": [
{
"family": "RESCORLA",
"given": [
"ERIC"
]
}
],
"birthDate": "..."
}
},
This is obviously just my name and birthdate the latter of which I've removed,
replacing it with ...
.
Then we have two records, indicating that I've been immunized, how, and where:
{
"fullUrl": "resource:1",
"resource": {
"resourceType": "Immunization",
"status": "completed",
"vaccineCode": {
"coding": [
{
"system": "http://hl7.org/fhir/sid/cvx",
"code": "208"
}
]
},
"occurrenceDateTime": "...",
"performer": [
{
"actor": {
"display": "Santa Clara County Mass Vaccination Site4 (Levi S)"
}
}
],
"lotNumber": "..."
}
},
{
"fullUrl": "resource:2",
"resource": {
"resourceType": "Immunization",
"status": "completed",
"vaccineCode": {
"coding": [
{
"system": "http://hl7.org/fhir/sid/cvx",
"code": "208"
}
]
},
"occurrenceDateTime": "...",
"performer": [
{
"actor": {
"display": "Santa Clara County Mass Vaccination Site4 (Levi S)"
}
}
],
"lotNumber": "..."
}
}
Everything here is pretty straightforward, except for the "system" stuff, which
describes the actual vaccine I was given. You can find the table of what it means
here.
Code 208
refers to "SARS-COV-2 (COVID-19) vaccine, mRNA, spike protein, LNP, preservative free, 30 mcg/0.3mL dose".
You'll notice that it doesn't say Pfizer, but you can infer it from the type (mRNA) and dosage (.3mL) because
Moderna has a .5mL dose whereas Pfizer is .3mL
The QR encoding is kind of... interesting: there's a string prefix starting with
shc:/
that indicates how may chunks there are followed by the actual bytes encoded as two digit decimal numbers that represent the byte value minus 45 (to get the entire range of values into the range [0...99]). Of course, the JWT itself is base64-encoded, which is a bit goofy. If you were starting from scratch, you could obviously get a more compact encoding, but that's what happens when you build on existing standards. ↩︎