Upon completing my COVID vaccination, I was pleasantly surprised to receive a digitally signed vaccination certificate. Based on my previous experience, I knew that getting a traditionally certified genuine chop stamp true copy of a certificate was not going to be cheap. A quick google search shows that it costs $10 to certify a one page document as a true copy, a further $75 for issuance of a notorial certificate and a further further $85.60 for the Singapore Academy of Law to authenticate the certified true copy, making the total cost a whopping $170.60. That is sure going to deter people from travelling, but fortunately we can now get it for free thanks to technology.
So, how does the digitally signed certificate work? The certificate, stored in a file extension ending in oa, is basically a JSON file. I do not recommend sharing that file as personal details such as birth date, IC number, passport number are stored unencrypted in the file. An encrypted copy of the certificate is also stored at https://api-vaccine.storage.aws.notarise.gov.sg
. Based on code in the the /dist/index.js
file of the oa-encryption
library, the encryption algorithm appears to be AES-GCM
with 256 bit key length, 96 bit IV and 128 bit tag length. Upon decryption, it appears that individual fields in certificate are stored separately. The field contains a uuid like string which is used as a salt, the data type and ends with the actual data.
1 2 3 4 5 6 |
|
The actual hashing is done using the keccak256
algorithm, which is very similar to SHA3 except for some changes to the padding. This operation is performed in the /dist/cjs/2.0/digest.js
file of the open-attestation
library. The data is flattened, hashed, sorted in alphabetical order and then it goes through hashing once again ending up as the targetHash
. Continuing from my sample data above, we see the following fields and their corresponding hashes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
Since the vaccination certificate is individually issued and not as a batch, the targetHash
is same as the merkleRoot
. The certificate has a field stating the verification method, which in this case is did:ethr
. The verification appears to be done in /dist/cfs/common/did/verifier.js
file of the oa-verify
library. The provider appears to be infura
and the rpc url is https://mainnet.infura.io/v3/bb46da3f80e040e8ab73c0a9ff365d18
. It is interesting to note that GovTech hardcoded their Project Id into the library. The ethers
library is used to arrayify the merkelRoot
into a byte array which is used together with the signature to compute the ethereumAddress and check if it matches. Continuing from my sample data above, we see the following being performed.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
And there we have it. After going on a journey starting from the open-attestation-cli
library all through the oa-encryption, open-attestation, oa-verify
library, we have managed to trace the entire process from reading in the certificate to the final verification. At times, the sheer amount of boilerplate code gave me nightmares from the days of writing getters and setters in Java. Maybe one day, someone will write a simple verify.py
that does it all in a single script.