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.