Decoding New Zealand's COVID Pass
We have moved past the time of border closures, airport restrictions, and disruptions to daily life. Reflecting on that period in tech, I had the opportunity to work on an app to verify COVID passes. It's easy to forget what was accomplished to meet the regulations.
I want to use a series of articles to document how to decode and verify COVID passes. First, let's start with decoding. I won't cover the mobile app front-end implementation to keep it simple.
Example of QR code data
I can't find official documentation anymore.
NZCP:/1/2KCEVIQEIVVWK6JNGEASNICZAEP2KALYDZSGSZB2O5SWEOTOPJRXALTDN53GSZBRHEXGQZLBNR2GQLTOPICRUYMBTIFAIGTUKBAAUYTWMOSGQQDDN5XHIZLYOSBHQJTIOR2HA4Z2F4XXO53XFZ3TGLTPOJTS6MRQGE4C6Y3SMVSGK3TUNFQWY4ZPOYYXQKTIOR2HA4Z2F4XW46TDOAXGG33WNFSDCOJONBSWC3DUNAXG46RPMNXW45DFPB2HGL3WGFTXMZLSONUW63TFGEXDALRQMR2HS4DFQJ2FMZLSNFTGSYLCNRSUG4TFMRSW45DJMFWG6UDVMJWGSY2DN53GSZCQMFZXG4LDOJSWIZLOORUWC3CTOVRGUZLDOSRWSZ3JOZSW4TTBNVSWISTBMNVWUZTBNVUWY6KOMFWWKZ2TOBQXE4TPO5RWI33CNIYTSNRQFUYDILJRGYDVAYFE6VGU4MCDGK7DHLLYWHVPUS2YIDJOA6Y524TD3AZRM263WTY2BE4DPKIF27WKF3UDNNVSVWRDYIYVJ65IRJJJ6Z25M2DO4YZLBHWFQGVQR5ZLIWEQJOZTS3IQ7JTNCFDX
Observe there are three parts to it
NZCP: - This makes sure the scanned QR code is of NZ standard
1 - Version of QR code
CBOR payload
- The actual data of the customer is present here. CBOR is a method of transferring data, similar to JSON, but in a binary format. This makes it smaller and faster to transfer and process. If you're interested, you can check out this reference. The downside is that it isn't very readable.
Language and libraries
I am going to use Kotlin and the libraries as listed below.
Expected Data model
@Serializable
data class CertData(
@SerialName("1") val issuer: String,
@SerialName("5") val notBefore : Long,
@SerialName("4") val expiry : Long,
@SerialName("vc") val vc : VerifiableCredential,
) {
@Serializable
data class VerifiableCredential(
@SerialName("@context") val context: List<String?>,
val version : String,
val type: List<String?>,
val credentialSubject: CredentialSubject
)
@Serializable
data class CredentialSubject(
val givenName: String,
val familyName: String,
val dob: String
)
}
How to decode the QR code
There are three parts to it
Split the QR code data by
/
and use the third part.Extract the Sign1Message as content.
Decode the content into the data model defined above as
CertData
.
suspend fun decode(payload: String): CertData {
// sign1 message from payload
val sign1Message = getSign1Message(payload)
// Serialise content as CertData
val sign1Content = CBORObject.DecodeFromBytes(sign1Message.GetContent())
val json = Json { ignoreUnknownKeys = true }
return json.decodeFromString(CertData.serializer(), sign1Content.ToJSONString())
}
private fun getSign1Message(code: String): Sign1Message {
// actual encoded payload portion
val base32EncodedPayload = getCodePart(code)
// decode base32 payload
val decodedPayload = Base32().decode(base32EncodedPayload)
// validate sign1 message
val unVerifiedCborPayload = CBORObject.DecodeFromBytes(decodedPayload) as CBORObject
val isSign1Message = unVerifiedCborPayload.mostOuterTag.ToInt32Unchecked() == MessageTag.Sign1.value
if (!isSign1Message) throw IllegalArgumentException("Should be in sign1 format")
// sign1 message from payload
return Sign1Message.DecodeFromBytes(decodedPayload) as Sign1Message
}
private fun getCodePart(code: String): String? {
val parts = code.split('/')
if (parts.size == 3 && parts[0] == "NZCP:" && parts[1] == "1") {
return parts[2]
}
return null
}
// Quickly run and check
fun main() {
runBlocking {
val decoded = Decoder().decode("NZCP:/1/2KCEVIQEIVVWK6JNGEASNICZAEP2KALYDZSGSZB2O5SWEOTOPJRXALTDN53GSZBRHEXGQZLBNR2GQLTOPICRUYMBTIFAIGTUKBAAUYTWMOSGQQDDN5XHIZLYOSBHQJTIOR2HA4Z2F4XXO53XFZ3TGLTPOJTS6MRQGE4C6Y3SMVSGK3TUNFQWY4ZPOYYXQKTIOR2HA4Z2F4XW46TDOAXGG33WNFSDCOJONBSWC3DUNAXG46RPMNXW45DFPB2HGL3WGFTXMZLSONUW63TFGEXDALRQMR2HS4DFQJ2FMZLSNFTGSYLCNRSUG4TFMRSW45DJMFWG6UDVMJWGSY2DN53GSZCQMFZXG4LDOJSWIZLOORUWC3CTOVRGUZLDOSRWSZ3JOZSW4TTBNVSWISTBMNVWUZTBNVUWY6KOMFWWKZ2TOBQXE4TPO5RWI33CNIYTSNRQFUYDILJRGYDVAYFE6VGU4MCDGK7DHLLYWHVPUS2YIDJOA6Y524TD3AZRM263WTY2BE4DPKIF27WKF3UDNNVSVWRDYIYVJ65IRJJJ6Z25M2DO4YZLBHWFQGVQR5ZLIWEQJOZTS3IQ7JTNCFDX")
print("decoded is $decoded") // CertData(issuer=did:web:nzcp.covid19.health.nz, notBefore=1635883530, expiry=1951416330, vc=VerifiableCredential(context=[https://www.w3.org/2018/credentials/v1, https://nzcp.covid19.health.nz/contexts/v1], version=1.0.0, type=[VerifiableCredential, PublicCovidPass], credentialSubject=CredentialSubject(givenName=Jack, familyName=Sparrow, dob=1960-04-16)))
}
}
Conclusion & next steps
It's relatively easy to decode once we know the specifications published by Health NZ. In the next article, I will cover verifying that a valid party generates the QR code. Article coming soon.
Subscribe to my newsletter
Read articles from Sree P directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by