How do Verifiable Vaccination Records with SMART Health Cards Work?
Many organizations, including Walmart and the State of California, are now providing vaccination records in digital form using the SMART Health Card Framework. SMART Health Cards are signed digital artifacts containing health data that patients can keep and share. The framework has been developed with open and decentralized technologies and its implementation has been led by a coalition of public and private organizations.
Unlike paper records, a SMART Health Card’s authenticity can be verified using its cryptographic signature, making it difficult to tamper with or forge. These cards are designed to be small and can be stored as a file or in a digital wallet. Apple has recently announced support for importing them into the Health app. They can also be embedded into a QR code and printed. To protect privacy, SMART Health Cards are intended to carry only the information strictly necessary to verify a vaccination record or lab result.
SMART Health Cards are currently used for storing and verifying COVID-19 vaccination and testing status for travel, school, or work. But there are several use cases beyond COVID-19 as well. As SMART Health Cards can encapsulate any type of health data, patients can use them to keep and voluntarily share verifiable health data with their doctors, research studies, or public health agencies.
In this article, we’ll take a look at the technology behind SMART Health Cards and the process of creating, sharing, and verifying them. We’ll also look at some sample code to help us understand the concepts involved.
How SMART Health Cards are created
A SMART Health Card is a Verifiable Credential in the form of a JSON Web Signature (JWS) containing health data. The JWS is digitally signed by its issuer, providing verifiable proof that it has not been altered since it was created. SMART Health Cards will usually be issued by entities like hospitals, labs, pharmacies, or health departments as an alternative or in addition to paper records.
The health data in the card is in the form of a FHIR bundle. FHIR is a standard for exchanging health information that organizes data into resources, such as Patient and Immunization. A FHIR bundle is a collection of these resources, which are usually represented as JSON.
There are four steps involved in issuing a SMART Health Card:
- Preparing the data for the card as a FHIR bundle in JSON format.
- Removing any unnecessary elements and whitespace from the JSON and compressing it using the DEFLATE algorithm.
- Using a signing key to create a JSON Web Signature from the compressed data.
- Providing the card to a patient in a format that can be saved and distributed (e.g. digital file or printable QR code).
We’ll now use sample data to illustrate how this process works.
Step 1: Preparing the data
For a COVID-19 two-dose vaccination record, the FHIR bundle will have three resources. The first is a Patient resource that contains the patient’s full name and date of birth:
The remaining two are Immunization resources describing each dose in the vaccination series. The following fields are included:
coding
identifies the vaccine given by its CVX code. Code 207 in the example corresponds to the Moderna COVID-19 vaccine.occurrenceDateTime
specifies the date and time when this dose was given.performer
indicates who provided this dose.lotNumber
contains the lot number printed on the vaccine label.
The resource corresponding to the first dose is shown below:
The second dose is similar but would have a different date and lot number.
Now that we’ve converted our data into FHIR resources and compiled them into a bundle, we can insert the bundle into a new SMART Health Card.
There are three important fields to note in the SMART Health Card:
iss
“issuer” — A URL identifying the issuer that the verifier will use to obtain the public keys needed to verify the card’s authenticity.nbf
“not before” — The date/time after which the card is valid for use (represented in epoch time).vc
“verified credential” —The health data itself. Thetype
subfield identifies this as a SMART Health Card containing a COVID-19 vaccination record. ThecredentialSubject
contains the FHIR Bundle created previously and indicates the version of FHIR that was used.
Here’s the full SMART Health Card with the FHIR bundle included:
Step 2: Compressing the Card
SMART Health Cards must be as small as possible, especially if we are to convert them into QR codes. To achieve this, the specification recommends that they be minified (remove any unnecessary elements and whitespace) and compressed using the DEFLATE algorithm before being signed. In the following code, we’ll take the JSON file containing the card data and perform these two steps.
// import the card data from a JSON file
const healthCard = require('smart-health-card.json');// remove all whitespace
const minifiedCard = JSON.stringify(healthCard);// compress using "raw" DEFLATE compression (no zlib or gz headers)
const compressedCard = zlib.deflateRawSync(minifiedCard);
Step 3: Signing the Card
Once the card has been compressed, we’ll sign and encode it into a JSON Web Signature using the compact serialization format. In order to do that, we’ll first generate a key pair. (SMART Health Cards use Elliptic Curve keys with the P-256 curve.) The private (signing) key will be used to sign the JWS and the public key will be published and used by others, including digital wallets and apps, to verify that we created the JWS and it has not been altered since.
// use the node-jose package
const jose = require('node-jose');// create a key pair
const keystore = jose.JWK.createKeyStore();
const key = await keystore.generate('EC', 'P-256');// create and sign the JWS using the private key
const jws = await jose.JWS.createSign({ format: 'compact', zip: 'DEF' }, key).update(compressedCard).final();
The resulting JWS is a string comprised of three segments in order that are base64 encoded and separated by period characters (“.”) — a header, a payload, and a signature. The header identifies the cryptographic algorithm used (ES256), the compression used (DEF), and the key id indicating which key was used to sign the JWS. The payload is the minified and compressed card data. The signature will be used to verify that the JWS is authentic.
Here is a JWS created using the code shown above:
eyJhbGciOiJFUzI1NiIsImtpZCI6IlVYR2dnMjVfM3ZockNPWVcxdHlGbE9YZDlTeEFNVi1SVF9UdUFIZkpXSlkifQ.3ZJLb9swEIT_SrC9ynohjSDd6hTI41AUaNpL4QNNrS0WfAgkJcQN9N-7SztIWyQ55RTdVhx-nBnyAVQI0MEQ4xi6oggjyjwY4eOAQschl8L3ocB7YUaNoSD1hB4ysNsddNVFXV-0ZX3e5G3TZjBL6B4gHkaE7ucT83_ch-Ow4oFQL-uUMZNVv0VUzr4qlG5WfdXCJgPpsUcbldDfpu0vlJEt7Qblf6APzOngPC_zinj8dz3ZXiNrPAY3eYl3yT6cFrJTHJBOa6IdndAB_kAZiTxp_d1rEjzu70oSPA7PgL9SHNrPHQqDR4gwShMPPlnS-JDO2KsZLfd46wae1zlsFgq4VRT-s4jMqtqP1aqsVnUJy5I966Z63c3NvxWHKOIUUly-8Ih8QbOQUlm8dH0iSNcru0_GwyFENKf3Qzcz6CZ3fl9ws0VQfSHnewLItBPqsoFls2QwnipIdnbo0bK3vxskkZNy8mmJw94pc0TUKXDJsaiqnfOG3iN7ETI6z8hehVGLVOf68uwKLXqhz65dGFUUmoqiErWLXyaz5a1Qpq96scH6XTZYt2_dYMMLC31_AA.j5H0TMm7eY4TkiOwfmke9Z-nH4vqflgeIB-EX4DmcCVeKKooqLKEXsdC3hPZdtUZ4NPkBfeVLV7g2XJiKB1Rmw
Step 4: Providing the Card to a Patient
At this stage, the SMART Health Card — in the form of a JWS — is complete and ready to provide to a patient. There are three ways to do this:
- We can create a
.smart-health-card
file containing the JWS that a patient can download. - We can convert the JWS into a QR code that a patient can print or save. We’ll look at how to do this in the next step.
- We can host the JWS on a FHIR API that apps can request directly on the patient’s behalf. Major electronic health record systems including Epic and Cerner support this method. The next section will walk through how this works.
The patient can store their card as a file on their device or on the cloud, print it out as a QR code, or import it into a digital wallet. As of iOS 15, Apple Health supports the import of SMART Health Cards. The CommonHealth app also provides similar functionality on Android.
Generating a QR code
Let’s look at some code that generates a QR from our JWS. The QR is composed of two segments, a byte segment that contains a string identifying this QR as a SMART Health Card, and a numeric segment representing the JWS. The specification states that if the JWS is longer than 1195 characters, it needs to be split into separate QR codes. Fortunately, our example doesn’t require this.
// adapted from https://github.com/dvci/health-cards-walkthrough/blob/main/SMART%20Health%20Cards.ipynb const QRCode = require('qrcode'); const byteSegment = 'shc:/'; const numericSegment = jws.split('')
.map(c => c.charCodeAt(0) - 45)
.flatMap(c => [Math.floor(c / 10), c % 10])
.join(''); const segs = [
{ data: byteSegment, mode: 'byte' },
{ data: numericSegment, mode: 'numeric' }
]; QRCode.toFile('smart-health-card.png', segs);
The code above will generate this QR and save it as a PNG file. The image can also be printed on a physical card if desired. We will go over scanning this QR in the next section.
How SMART Health Cards are verified and used
In this section, we’ll take the role of a verifier and look at how we can build an app that can decode, verify, and use the data within a SMART Health Card shared by a patient. For example, we could be building a digital wallet to help patients manage their records or an app for a school to check incoming students’ vaccination status.
A patient can share their card by uploading the .smart-health-card
file, scanning the QR code, or authorizing the app to download the card from a FHIR API. Upon obtaining a card by any of these methods, the app can decode it and validate its authenticity. iOS apps can also obtain cards imported into Apple Health via a HealthKit query.
Extracting a Card from a QR Code
The following code demonstrates how to extract a SMART Health Card from a saved QR code. We will read in the image file, use the jsQR library to extract the QR code, separate the numeric segment, and then convert it back into a JWS that we can decode:
// adapted from https://github.com/dvci/health-cards-walkthrough/blob/main/SMART%20Health%20Cards.ipynb // scan QR code from image file
const jsQR = require('jsqr');
const PNG = require('pngjs').PNG;
const imageFile = fs.readFileSync('smart-health-card.png');
const image = PNG.sync.read(imageFile);
const imageData = new Uint8ClampedArray(image.data.buffer);
const scannedQR = jsQR(imageData, image.width, image.height); // separate the numeric segment
const numericCode = scannedQR.chunks.filter(chunk => chunk.type === "numeric")[0];// convert from numeric encoding to JWS
const JWSchars = numericChunk.text.match(/(..?)/g);
const JWS = JWSchars.map(num => String.fromCharCode(parseInt(num, 10) + 45)).join('');
Now, with a JWS in hand, we can continue on to decode and verify it.
Getting a Card from a FHIR API
Let’s also look at a different method — requesting a SMART Health Card directly from an electronic health record system’s FHIR API on behalf of our patient. This is done using the API’s $health-cards-issue operation.
To do this, we will make a POST request to the /Patient/:id/$health-cards-issue
endpoint on the FHIR API with the header containing a patient’s OAuth token and the body containing parameters specifying the type of card we are looking for. In the code below, we’ve listed three types (health-card, immunization, and covid19) and the result will be cards that meet all three types.
// define the type of health card to retrieve
const parameters = {
"resourceType": "Parameters",
"parameter": [
{
"name": "credentialType",
"valueUri": "https://smarthealth.cards#health-card"
},
{ "name": "credentialType",
"valueUri": "https://smarthealth.cards#immunization"
},
{ "name": "credentialType",
"valueUri": "https://smarthealth.cards#covid19"
}
]
};
We’ll assume that our app has been registered with the API and our patient has already logged in using their patient portal credentials and granted us access to their demographics and immunization record. (Check out the complete app to see how these steps are done.)
Now let’s request the card from the FHIR API, demonstrated here using Cerner’s sandbox. With our request, we will pass the previously defined parameters in the body and the patient’s OAuth2 bearer token in the Authorization
header. We will use Cerner’s sample patient ‘Alice S. Gilbert’.
// request the card from the FHIR server using the patient's token
const axios = require('axios');const FHIR_URL = 'https://fhir-myrecord.cerner.com/r4/ec2458f2-1e24-41c8-b71b-0e701af7583d'; // Cerner sandbox FHIR API URLlet jws;
axios({
method: 'post',
url: `${FHIR_URL}/Patient/${fhirClient.patient.id}/$health-cards-issue`,
data: parameters,
headers: {
'Authorization': fhirClient.getAuthorizationHeader(),
'Accept': 'application/fhir+json',
'Content-Type': 'application/fhir+json'
}
}).then((response) => {
jws = response.data.parameter.filter(
parameter =>
parameter.name === "verifiableCredential")[0].valueString;
});
Our request will result in the following response from the FHIR API (shortened for readability). The JWS can be found in the verifiableCredential
parameter. In our code above we have extracted and assigned it to the jws
variable.
{
"resourceType": "Parameters",
"parameter": [
{
"name": "verifiableCredential",
"valueString": "eyJhbGciOiJFUzI1NiIsInppcCI6IkRFRiIsImtpZCI6Ik4ybWFHOHFPaElUZV96eVBxY3JDYTZMVF9tVzE4WnRGUHkwOWNHQUIyNHcifQ.3ZJPj9MwEMW_Chqu-ec0bdLc2GUFlRaE2C4X1IPjTFojO45sp6Ks8t0Zp10EYncPHJF88cz45_ee_QDSOajh4P3g6jTtDtLG-mRRGNsmAm2PNhFGp7ZIUeTFsurymGFexAUTVdyUrIkzLDPGu3JZLVqIoG86qNkqz1flMlusIzgKqB_AnwaE-uuvm5zm1h-QK39IBLete33exGFDmOfnpNZjL39wL03_4qAwR9myNewiEBZb7L3k6m5svqHwQVIw-wWtC5waiiRLMuKF6tXYtwrDjEVnRitwO8uHSyO62AFhlCLaWQldYE_kkcijUvdW0cDj-TqgHzdPgD-RHTof8uMazxCupSIevNvcXt183lJvL4_YhxTf3G6ub6hwB7uJ7DWSrL_lPpDYusxixmK2gmmKntTCXtay-TNg57kf3WxWDwo9huc5ciFkj9emnQnCtLLfz7LdyXnUlz9F73JQZWLsfv5aqZNtKo7fCSDmk5BnJUy7KYLhEsAsp0OLfdD2e340ZIQY7dwKZrdSnxE5i7NFzCrCKuM_jrpBS42yKlcF1Qa0nbE61EgfF97YcE0r3aB4CPgDaVGv3hs3SM8VxbZ7Lrn8P0wu_zu5Il-uSvav0dGapp8.UY3V4aWsmxf3an_TvmqDT16kjSX2QAv6E0cbOXS1d1wY5viR5710H39wNmIyuqRAKdNH7CMzxY-U6n7y-qzgSw"
},
...
}
Decoding and Verifying a SMART Health Card
Regardless of the method the patient used to share their Health Card, at this point we will have a JWS to decode. Recall that the JWS is a string comprised of three parts — header, payload, and signature — each encoded in base64 and separated by period (“.”) characters. The payload contains the card, compressed using the DEFLATE algorithm.
Let’s work with the JWS obtained from the Cerner sandbox FHIR API above. First, we will extract the payload and decompress it.
// Split the JWS into its three components
const [ header, payload, signature ] = jws.split('.');// Decode the payload
const decodedPayload = Buffer.from(payload, 'base64');// Decompress the card and extract the data into an object
const decompressedCard = zlib.inflateRawSync(decodedPayload);
const card = JSON.parse(decompressedCard.toString());
Next, we will read the iss
field which contains the issuer’s web address. The public key should be published at this address under /.well-known/jwks.json
.
In this case, iss
points to Cerner’s sandbox FHIR API:
https://fhir-myrecord.cerner.com/r4/ec2458f2-1e24-41c8-b71b-0e701af7583d
The public key will then be found at the following URL:
https://fhir-myrecord.cerner.com/r4/ec2458f2-1e24-41c8-b71b-0e701af7583d/.well-known/jwks.json
Since anyone can issue a SMART Health Card, we should maintain a list of trusted issuers (trust directory) and verify the contents of the iss
field against this list prior to continuing. The Vaccine Credential Initiative (VCI), a coalition of public and private organizations, manages a directory that can be found here.
We’ll use a trust directory to verify our issuer. If the issuer is trusted, we will proceed to download the key and use it to verify the signature, as below:
// use axios to get the key and node-jose to perform verification
const axios = require('axios');
const jose = require('node-jose');
const trustDirectory = require('./vci-issuers.json');// Get the issuer's URL from the card
const issuer = card.iss;// Check if the issuer is trusted using trust directory
if(trustDirectory.participating_issuers.some(x => x.iss === issuer)){
// Download the public key and verify the signature
const result =
await axios.get(`${issuer}/.well-known/jwks.json`);
const keystore = await jose.JWK.asKeyStore(result.data.keys);
const verified =
await jose.JWS.createVerify(keystore).verify(jws);
} else {
// Issuer is not trusted, notify the user.
console.log("Issuer is not trusted.");
}
If the issuer cannot be trusted or the signature cannot be verified, the above step will fail. Once we’ve successfully verified the card’s authenticity, we can use the clinical data within the enclosed FHIR bundle.
Using the data in a SMART Health Card
As in the example from the previous section, this COVID-19 immunization card’s FHIR bundle contains three resources — the first is the Patient resource, and the next two are Immunization resources representing the first and second doses of the vaccine.
The code below will print out the patient’s name, the vaccine’s CVX code, and the date of each dose in the series:
// extract the FHIR bundle from the card
const fhirBundle = card.vc.credentialSubject.fhirBundle;// get the patient resource and print out the patient's name
const patient = fhirBundle.entry[0];
const name =
patient.resource.name[0].given.join(" ") + " " +
patient.resource.name[0].family;
console.log("Name:", name);// get the immunization resources, extract the code and dates of each vaccination and print them out
const doses = fhirBundle.entry.slice(1);
doses.forEach(dose =>
console.log(`Date - ${dose.resource.occurrenceDateTime}, CVX - ${dose.resource.vaccineCode.coding[0].code}`)
);
Here’s the expected output. CVX code 207 stands for the Moderna COVID-19 vaccine.
Patient - ALICE S GILBERT
Date - 2021-03-18, CVX - 207
Date - 2021-02-18, CVX - 207
At this point, we’ve successfully verified that our patient is fully vaccinated against COVID-19!
It’s important to note that while SMART Health Cards are verifiable, they are not encrypted. The data in a card can be decoded and read by anyone who has a copy of it. Therefore, it’s important to protect cards and share them only when necessary.
I hope this article helped you to understand how SMART Health Cards work. The information in this article is based on the SMART Health Cards website, technical specification, and notebook, as well as the Cerner Health Cards API documentation. Please check out these sources for more information. If you’re interested in implementing SMART Health Cards on iOS, I recommend watching this video from WWDC21.
Check out my website for more digital health projects!