Decoding New Zealand's COVID Pass

Sree PSree P
3 min read

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.

0
Subscribe to my newsletter

Read articles from Sree P directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sree P
Sree P