Vaccination Paywalls
It was just announced a few days ago that if you were vaccinated in California, you can get a digital copy of your vaccination record in the form of a QR code by going to the Digital COVID-19 Vaccine Record portal. The QR code looks like this:
But if you try to scan it in with the iOS Camera, it just says “No usable data found”. iOS 15 will support reading Smart Health cards but it won’t be released until the fall, so until then we have these mostly useless QR codes. So let’s do something with them!
First of all, it’s a giant QR code. That’s because it contains a lot of information including:
- Your name
- Your date of birth
- The organization that issued it
- What type of shots you got
- When you got your shots
- Where you got your shots
- The lot number of your shots
Basically it has everything you see on your physical COVID-19 Vaccination card, as well as some stuff to make it verifiable.
When you scan in the QR code, you’re left with a long string of numbers:
shc:/567629095243206034602924374044603122295953265460346029254077280433602870286471674522280928613331456437653141590640220306450459085643550341424541364037063665417137241236380304375622046737407532323925433443326057360106453131537170742424415029455972454462384130574231626537750944666231385374207252266370320021230732342826007357347620675225242542434443365967764574067073123636093772595838266432434143252441404569367567362665384022664500636159661038617441593755114420215361552769670906533871242972053171577335753774413320123639683322330612270052507227576577446606702204202156773729380633535507126624673205361109503236367143566269760425425534045727685543420036567429266507352272506326422069256658224033074566392260203158586562624362533276354430615956530739043522115636271143384533110575534261752367334226563521294568426754712127552677107668432172753255064552746554615739673550617728756871097033767337695966210304381130304500435475435210353973003960307652455467775511593263103465743029433823641211357534401010401077316961303852037607733907747764707339594027267441553712674409352125237729233960246853714132220729232331054233207512690023672252732528087722400556095771265527403011245744405432567511705040267037526962207633102740546134540569090872317700587438273575755455372905626630206937003536710575053558682341736729581176443953235350330326750665215966734053015903522428303161087254306800082240413276370671614523382904224405756140440576530835397175712910434009297334444500266824390007747123677140617403097326520442732041343460207258
You can take those numbers and convert them to a base64 string by splitting them into pairs, adding each of those pairs to the ASCII decimal of -
(45), and taking the ASCII character of the decimal sum. For example, the first two pairs are 56 and 76. So we add 45 + 56 = 101 which is represented as e
and 45 + 76 = 121 which is represented as y
and keep doing that until all the pairs are done and we have another long string:
eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IjNLZmRnLVh3UC03Z1h5eXd0VWZVQUR3QnVtRE9QS01ReC1pRUxMMTFXOXMifQ.3ZLLbtswEEV_JZhuZYkSVKfWLknRx6YokLSbwAuaGlsM-BD4MOIG-vfOyApaFEFWXYXQhpyZw3sv9QQ6RuhgSGmMXVXFEVUZrQxpQGnSUCoZ-ljho7SjwVhRd8YABbjdHrp63bStEJu2LtfvPxRwVNA9QTqNCN39H-a_uHfnzYo3sC1ABezRJS3Nbd49oEpM2Q86_MQQtXekry1FWdO1fHqdXW-QewJGn4PCu_lGWArFogCUN4ZoTCiALggnkkXkbMyPYKjheb4T1PC8eQH8XSZN82xbWjxDpNWGePBJZqWpctBHdGz7yqXBuxMd3ZawncjfTpP_jzIxqt6sNyvRrhoB01S8KKZ-XcxXa7PTv-TiKyaZcpzd8hMl7OnwKJXSDm98PxOU77U7zLrjKSa0y4vT4wzmsvThUHGwVdR9pY6PBFDzJDTiEqbtVMC4JDDL2WNAx9r-DpCavFI5zCU2e6ftGdHUK8EfYUcMex8s_UGsRarkAyN7HUcjOc2r65uLz-gwSHPxxcdRJ2koKArR-PQt2x2PgqDVvpJg8yYTbDb_N0Gx3nBhovUb.h0aEIKLj5ucKq-5CUVMyR3tjZDSJ1CY2xjUY2yb5PTtxtJ7XU6JvOYZ-GqET-4wtDptUjw06vGa1WvAVOOiAug
Readers that have spent too much time debugging JWTs or OpenID Connect will recognize the telltale ey
prefix of a JWT. In fact, throwing this into jwt.io will spit out the header for us:
{
"zip": "DEF",
"alg": "ES256",
"kid": "3Kfdg-XwP-7gXyywtUfUADwBumDOPKMQx-iELL11W9s"
}
The payload looks a little wacky though. That’s because it’s a base64url encoding. We can decode this encoding to get a bunch of bytes and then decompress those bytes using the INFLATE algorithm to a JSON string.
Here’s what that looks like in Node:
const pako = require('pako');
var payload = '3ZLLbtswEEV_JZhuZYkSVKfWLknRx6YokLSbwAuaGlsM-BD4MOIG-vfOyApaFEFWXYXQhpyZw3sv9QQ6RuhgSGmMXVXFEVUZrQxpQGnSUCoZ-ljho7SjwVhRd8YABbjdHrp63bStEJu2LtfvPxRwVNA9QTqNCN39H-a_uHfnzYo3sC1ABezRJS3Nbd49oEpM2Q86_MQQtXekry1FWdO1fHqdXW-QewJGn4PCu_lGWArFogCUN4ZoTCiALggnkkXkbMyPYKjheb4T1PC8eQH8XSZN82xbWjxDpNWGePBJZqWpctBHdGz7yqXBuxMd3ZawncjfTpP_jzIxqt6sNyvRrhoB01S8KKZ-XcxXa7PTv-TiKyaZcpzd8hMl7OnwKJXSDm98PxOU77U7zLrjKSa0y4vT4wzmsvThUHGwVdR9pY6PBFDzJDTiEqbtVMC4JDDL2WNAx9r-DpCavFI5zCU2e6ftGdHUK8EfYUcMex8s_UGsRarkAyN7HUcjOc2r65uLz-gwSHPxxcdRJ2koKArR-PQt2x2PgqDVvpJg8yYTbDb_N0Gx3nBhovUb';
var bytes = Buffer.from(payload, 'base64');
var inflatedPayload = pako.inflateRaw(bytes, { to: 'string' });
console.log(inflatedPayload);
which outputs:
{
"iss": "https://spec.smarthealth.cards/examples/issuer",
"nbf": 1624400941.658,
"vc": {
"type": [
"https://smarthealth.cards#health-card"
],
"credentialSubject": {
"fhirVersion": "4.0.1",
"fhirBundle": {
"resourceType": "Bundle",
"type": "collection",
"entry": [
{
"fullUrl": "resource:0",
"resource": {
"resourceType": "Patient",
"name": [
{
"family": "Fauci",
"given": [
"Anthony",
"S."
]
}
],
"birthDate": "1969-04-20"
}
},
{
"fullUrl": "resource:1",
"resource": {
"resourceType": "Immunization",
"status": "completed",
"vaccineCode": {
"coding": [
{
"system": "http://hl7.org/fhir/sid/cvx",
"code": "207"
}
]
},
"patient": {
"reference": "resource:0"
},
"occurrenceDateTime": "2021-01-01",
"performer": [
{
"actor": {
"display": "ABC General Hospital"
}
}
],
"lotNumber": "0000420"
}
},
{
"fullUrl": "resource:2",
"resource": {
"resourceType": "Immunization",
"status": "completed",
"vaccineCode": {
"coding": [
{
"system": "http://hl7.org/fhir/sid/cvx",
"code": "207"
}
]
},
"patient": {
"reference": "resource:0"
},
"occurrenceDateTime": "2021-01-29",
"performer": [
{
"actor": {
"display": "ABC General Hospital"
}
}
],
"lotNumber": "0000069"
}
}
]
}
}
}
}
The key iss
is the issuer. In this case, the issuer is https://spec.smarthealth.cards/examples/issuer
. Issuers publish their public keys at iss
value + /.well-known/jwks.json
so we should be able to go to https://spec.smarthealth.cards/examples/issuer/.well-known/jwks.json
and download them:
{
"keys": [
{
"kty": "EC",
"kid": "3Kfdg-XwP-7gXyywtUfUADwBumDOPKMQx-iELL11W9s",
"use": "sig",
"alg": "ES256",
"crv": "P-256",
"x": "11XvRWy1I2S0EyJlyf_bWfw_TQ5CJJNLw78bHXNxcgw",
"y": "eZXwxvO1hvCY0KucrPfKo7yAyMT6Ajc3N7OkAB6VYy8"
},
...
]
}
Unsurprisingly, the kid
matches the kid
found in the header of our JWT. By using the public key, we can verify that this card came from the issuer because only they have the private key necessary to sign it.
When you get a QR code from the Digital COVID-19 Vaccine Record portal, these are issued by https://myvaccinerecord.cdph.ca.gov/creds
and thus their public keys can be found at https://myvaccinerecord.cdph.ca.gov/creds/.well-known/jwks.json
. Since anyone can be an issuer by generating public/private key pairs, it’s important that you identify which issuers you actually trust.
If we can take a QR code, extract the vaccination record, and verify that it was issued by the state, we can integrate that into almost anything! Hear me out.
What if we prevented people from accessing content based on their vaccination status? What if you couldn’t read Reddit, watch Youtube, or post on Instagram without first proving that you’ve been vaccinated? I think we’d up our numbers pretty quick.
As a proof-of-concept, I’ve built a vaccination paywall to provide an example of how this can be done. In this example, the contents of the book The Great Gatsby are protected by a vaccination paywall. In order to read it, you simply have to get your digital vaccination record from Digital COVID-19 Vaccine Record portal, press the Scan Digital COVID-19 Vaccination Record button and scan it in.
Here’s a quick video of how it works:
You can try it out here: https://vaxpaywall.bert.org/.
The source code is available here.
No one has asked any questions yet, so this FAQ section below stands for Frequently Anticipated Questions.
FAQ
What about people with medical conditions that can’t get vaccinated?!
This is a joke.
What about people that are too old or too young to get vaccinated?
See above, this is a joke.
Do you think that a person that won’t get vaccinated even wants to read The Great Gatsby?
No.
I’m not a resident of California so I have no way of testing this vaccination paywall.
The QR code at the top of this blog post may be used to test the paywall.
I don’t want to give you my digital vaccine record!
That’s not a question. The State of California suggests that you can ask organizations that will scan the QR code in your Digital COVID-19 Vaccine Record how they will use your data or if they will keep it. Only you can decide how and when to share your record.
How will you use my data?
The data is only used to verify your vaccination status and display it, in a joking matter, above the text of The Great Gatsby.
Will you keep it?
No. You can look at the source code but there’s no way for you to verify that the website is using the same code so I don’t know what else to tell you.
This is a bad idea and you should feel bad.
I do not. If you can be vaccinated and a vaccine is available to you and you’re not getting one, you should feel bad.
Can’t someone just use someone else’s digital vaccination card?
In a physical setting, the digital vaccination card is supposed to be used in conjunction with another proof of identification, like a driver’s license. This is trickier online. One way to prevent abuse would be to only allow a vaccination record to be associated with a single account. Instead of storing the entire payload, you could store a hash of the payload and check if it’s already been used.
Do you really expect anyone else to do this?
Not really, but I could see it being used as a promotion, like as a discount or credit for an online service. If Dunkin’ Donuts and Krispy Kreme can have vaccine incentive programs, maybe online services could too? Frankly, I don’t really get vaccine incentive programs, or lotteries, or giving people guns for getting vaccinated. If not dying is not enough of an incentive to get the vaccine, I’m not sure what will be.