README ¶
EU Digital Certificate Verifier written in Go
This library is a basic implementation of verification process of the EU digital health certificates defined on https://ec.europa.eu/health/ehealth/covid-19_en and the official repository https://github.com/eu-digital-green-certificates
Sample usage
The following test file is used https://github.com/eu-digital-green-certificates/dgc-testdata/blob/main/GR/2DCode/raw/1.json
{
"JSON": {
"ver": "1.0.0",
"nam": {
"fn": "ΜΕΝΕΞΕΣ",
"fnt": "MENEXES",
"gn": "ΜΑΡΙΟΣ",
"gnt": "MARIOS"
},
"dob": "1980-11-11",
"v": [
{
"tg": "840539006",
"vp": "1119349007",
"mp": "EU/1/20/1528",
"ma": "ORG-100030215",
"dn": 2,
"sd": 2,
"dt": "2021-02-19",
"co": "GR",
"is": "IDIKA / Ministry of Digital Governance",
"ci": "URN:UVCI:01:GR:7UXL2ZTSS6KUZAF2XAAA3A4C4I#E"
}
]
},
"CBOR": "a4061a60bc9b1c041a645df85101624752390103a101a46376657265312e302e30636e616da462666e6ece9cce95ce9dce95ce9ece95cea363666e74674d454e4558455362676e6cce9cce91cea1ce99ce9fcea363676e74664d4152494f5363646f626a313938302d31312d3131617681aa627467693834303533393030366276706a31313139333439303037626d706c45552f312f32302f31353238626d616d4f52472d31303030333032313562646e02627364026264746a323032312d30322d313962636f62475262697378264944494b41202f204d696e6973747279206f66204469676974616c20476f7665726e616e6365626369782b55524e3a555643493a30313a47523a3755584c325a545353364b555a414632584141413341344334492345",
"COSE": "d28450a3012603183d0448bb1be5f9db32ac1ca0590125a4061a60bc9b1c041a645df85101624752390103a101a46376657265312e302e30636e616da462666e6ece9cce95ce9dce95ce9ece95cea363666e74674d454e4558455362676e6cce9cce91cea1ce99ce9fcea363676e74664d4152494f5363646f626a313938302d31312d3131617681aa627467693834303533393030366276706a31313139333439303037626d706c45552f312f32302f31353238626d616d4f52472d31303030333032313562646e02627364026264746a323032312d30322d313962636f62475262697378264944494b41202f204d696e6973747279206f66204469676974616c20476f7665726e616e6365626369782b55524e3a555643493a30313a47523a3755584c325a545353364b555a4146325841414133413443344923455840e8b57700be1a410b0cc896e8be57075f8ba54cefcba0de51c0185f4168487b92fbbdf4bc7732948db6b263ec517db5ca297ad05d2a953fa328305c64f41c04a7",
"COMPRESSED": "78dabbd412b098518d59c296c563b7f4d39fb78dd6c82c8864545dc22695b067b60c8b544aec8f40c624f7204b46e6858c4b92cb528b520df50cf40c92f312739724a5e5e59d9b736eeab9b9403c0f881727a7e595a4fbbafab946b80627a5e7e50065279e5b786ee6b9f940b9f4bc92345fc7204fffe0e494fca42c434b0b035d4343204a2c6b5c9554929e696162606a6c6960609654569065686868696c02e49927e516e4b886ea1bea1b19e81b9a1a5924e526e6fa07b9eb1a1a1818181b18199a26a5e4312515a73025a59464190105740d8c740d2d9392f3810e4fca2cae50f374f1f47654d057f0cdcccb2c2e29aa54c84f5370c94ccf2c49cc5170cf077a2a2f312f39352939b3423b34c8cf2a34ccd9d3cac0d0ca3dc8ca3c34c2c7282a2438d8cc3b34cad1cd28c2d1d1d1d8d1c4d9c453d935c2e1c5d672867d528edc3c27a6bdd817ce1edfbdd4e7fde905f7020f48c43b6678544ffabdf7cb9e72a329bddb3625bf09acdd7a4ab3ea42acd654fbc51a0631295f645896030036468d98",
"BASE45": "NCFOXNEG2NBJ5*H:QO-.OMBN+XQ99N*6RFS5YUCH%BM*4ODMT0NSRHAL9.4I92P*AVAN9I6T5XH4PIQJAZGA2:UG%U:PI/E2$4JY/KB1TFTJ:0EPLNJ58G/1W-26ALD-I2$VFVVE.80Z0 /KY.SKZC*0K5AFP7T/MV*MNY$N.R6 7P45AHJSP$I/XK$M8TH1PZB*L8/G9YPDN*I4OIMEDTJCJKDLEDL9CZTAKBI/8D:8DKTDL+S/15A+2XEN QT QTHC31M3+E3+T4D-4HRVUMNMD3323623423.LJX/KQ968X2+36/-KKTC 509UE1YH/T1NTICZUI 16PPT1M:YUKQU7/EAFQ+JU2+PFQ51C5EWAC1ASBE/V9.Q5F$PYBEO.0:E5 96KA7N95ZTM L7HHP4F5G+P%YQ+GONPPCHPMR73SOM352Q4FIR L7 SP5.PDSOSNQKIR%*O* OUKRTSOL0PNLE.$FW2I9R7P3LEERQ2Q$CS8-QL4W.X0WB0/89-M7O9F:4AV0OGXP7MEKC53WRXY41A1/:R/J9URTB%LKXAD-OAZ0GA5%UCI/I910G-8H3",
"PREFIX": "HC1:NCFOXNEG2NBJ5*H:QO-.OMBN+XQ99N*6RFS5YUCH%BM*4ODMT0NSRHAL9.4I92P*AVAN9I6T5XH4PIQJAZGA2:UG%U:PI/E2$4JY/KB1TFTJ:0EPLNJ58G/1W-26ALD-I2$VFVVE.80Z0 /KY.SKZC*0K5AFP7T/MV*MNY$N.R6 7P45AHJSP$I/XK$M8TH1PZB*L8/G9YPDN*I4OIMEDTJCJKDLEDL9CZTAKBI/8D:8DKTDL+S/15A+2XEN QT QTHC31M3+E3+T4D-4HRVUMNMD3323623423.LJX/KQ968X2+36/-KKTC 509UE1YH/T1NTICZUI 16PPT1M:YUKQU7/EAFQ+JU2+PFQ51C5EWAC1ASBE/V9.Q5F$PYBEO.0:E5 96KA7N95ZTM L7HHP4F5G+P%YQ+GONPPCHPMR73SOM352Q4FIR L7 SP5.PDSOSNQKIR%*O* OUKRTSOL0PNLE.$FW2I9R7P3LEERQ2Q$CS8-QL4W.X0WB0/89-M7O9F:4AV0OGXP7MEKC53WRXY41A1/:R/J9URTB%LKXAD-OAZ0GA5%UCI/I910G-8H3",
"2DCODE": "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",
"TESTCTX": {
"VERSION": 1,
"SCHEMA": "1.0.0",
"CERTIFICATE": "MIIBzDCCAXGgAwIBAgIUDN8nWnn8gBmlWgL3stwhoinVD5MwCgYIKoZIzj0EAwIwIDELMAkGA1UEBhMCR1IxETAPBgNVBAMMCGdybmV0LmdyMB4XDTIxMDUxMjExMjY1OFoXDTIzMDUxMjExMjY1OFowIDELMAkGA1UEBhMCR1IxETAPBgNVBAMMCGdybmV0LmdyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBcc6ApRZrh9/qCuMnxIRpUujI19bKkG+agj/6rPOiX8VyzfWvhptzV0149AFRWdSoF/NVuQyFcrBoNBqL9zCAqOBiDCBhTAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFN6ZiC57J/yRqTJ/Tg2eRspLCHDhMB8GA1UdIwQYMBaAFNU5HfWNY37TbdZjvsvO+1y1LPJYMDMGA1UdJQQsMCoGDCsGAQQBAI43j2UBAQYMKwYBBAEAjjePZQECBgwrBgEEAQCON49lAQMwCgYIKoZIzj0EAwIDSQAwRgIhAN6rDdE4mtTt2ZuffpZ242/B0lmyvdd+Wy6VuX+J/b01AiEAvME52Y4zqkQDuj2kbfCfs+h3uwYFOepoBP14X+Rd/VM=",
"VALIDATIONCLOCK": "2021-06-08T15:56:26.670297",
"DESCRIPTION": "VALID: EC 256 key"
},
"EXPECTEDRESULTS": {
"EXPECTEDVALIDOBJECT": true,
"EXPECTEDSCHEMAVALIDATION": true,
"EXPECTEDENCODE": true,
"EXPECTEDDECODE": true,
"EXPECTEDVERIFY": true,
"EXPECTEDCOMPRESSION": true,
"EXPECTEDKEYUSAGE": true,
"EXPECTEDUNPREFIX": true,
"EXPECTEDVALIDJSON": true,
"EXPECTEDB45DECODE": true,
"EXPECTEDPICTUREDECODE": true,
"EXPECTEDEXPIRATIONCHECK": true
}
}
package main
import (
"encoding/json"
"fmt"
"github.com/xkmsoft/eu-digital-certificate-verifier/pkg/hc1_verifier"
"io"
"log"
"net/http"
)
// TestFile is the JSON structure of defined test files on https://github.com/eu-digital-green-certificates/dgc-testdata
type TestFile struct {
JSON struct {
Ver string `json:"ver"`
Nam struct {
Fn string `json:"fn"`
Fnt string `json:"fnt"`
Gn string `json:"gn"`
Gnt string `json:"gnt"`
} `json:"nam"`
Dob string `json:"dob"`
V []struct {
Tg string `json:"tg"`
Vp string `json:"vp"`
Mp string `json:"mp"`
Ma string `json:"ma"`
Dn int `json:"dn"`
Sd int `json:"sd"`
Dt string `json:"dt"`
Co string `json:"co"`
Is string `json:"is"`
Ci string `json:"ci"`
} `json:"v"`
} `json:"JSON"`
CBOR string `json:"CBOR"`
COSE string `json:"COSE"`
COMPRESSED string `json:"COMPRESSED"`
BASE45 string `json:"BASE45"`
PREFIX string `json:"PREFIX"`
DCODE string `json:"2DCODE"`
TESTCTX struct {
VERSION int `json:"VERSION"`
SCHEMA string `json:"SCHEMA"`
CERTIFICATE string `json:"CERTIFICATE"`
VALIDATIONCLOCK string `json:"VALIDATIONCLOCK"`
DESCRIPTION string `json:"DESCRIPTION"`
} `json:"TESTCTX"`
EXPECTEDRESULTS struct {
EXPECTEDVALIDOBJECT bool `json:"EXPECTEDVALIDOBJECT"`
EXPECTEDSCHEMAVALIDATION bool `json:"EXPECTEDSCHEMAVALIDATION"`
EXPECTEDENCODE bool `json:"EXPECTEDENCODE"`
EXPECTEDDECODE bool `json:"EXPECTEDDECODE"`
EXPECTEDVERIFY bool `json:"EXPECTEDVERIFY"`
EXPECTEDCOMPRESSION bool `json:"EXPECTEDCOMPRESSION"`
EXPECTEDKEYUSAGE bool `json:"EXPECTEDKEYUSAGE"`
EXPECTEDUNPREFIX bool `json:"EXPECTEDUNPREFIX"`
EXPECTEDVALIDJSON bool `json:"EXPECTEDVALIDJSON"`
EXPECTEDB45DECODE bool `json:"EXPECTEDB45DECODE"`
EXPECTEDPICTUREDECODE bool `json:"EXPECTEDPICTUREDECODE"`
EXPECTEDEXPIRATIONCHECK bool `json:"EXPECTEDEXPIRATIONCHECK"`
} `json:"EXPECTEDRESULTS"`
}
// FetchTestFile simply fetches the test file of the given url and returns the TestFile structure
func FetchTestFile(url string) (*TestFile, error) {
resp, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("error requesting GET: %s\n", err.Error())
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error reading response body: %s\n", err.Error())
}
var testFile TestFile
if err := json.Unmarshal(body, &testFile); err != nil {
return nil, fmt.Errorf("error unmarshalling test file: %s\n", err.Error())
}
return &testFile, nil
}
func main() {
url := "https://raw.githubusercontent.com/eu-digital-green-certificates/dgc-testdata/main/GR/2DCode/raw/1.json"
testFile, err := FetchTestFile(url)
if err != nil {
log.Fatal(err)
}
certificate, err := hc1_verifier.CreateCertificateFromPEM(testFile.TESTCTX.CERTIFICATE)
if err != nil {
log.Fatal(err)
}
dgc, err := hc1_verifier.VerifyWithCertificate(testFile.PREFIX, certificate)
if err != nil {
log.Fatal(err)
}
claims, err := dgc.ToJSONClaims()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Verified health certificate: %s\n", claims)
}
And the output
Certificate is verified successfully
Verified health certificate: {
"iss": "GR",
"exp": 1683880017,
"iat": 1622973212,
"hcert": {
"eu_dgc_v1": {
"ver": "1.0.0",
"nam": {
"fn": "ΜΕΝΕΞΕΣ",
"fnt": "MENEXES",
"gn": "ΜΑΡΙΟΣ",
"gnt": "MARIOS"
},
"dob": "1980-11-11",
"v": [
{
"tg": "840539006",
"vp": "1119349007",
"mp": "EU/1/20/1528",
"ma": "ORG-100030215",
"dn": 2,
"sd": 2,
"dt": "2021-02-19",
"co": "GR",
"is": "IDIKA / Ministry of Digital Governance",
"ci": "URN:UVCI:01:GR:7UXL2ZTSS6KUZAF2XAAA3A4C4I#E"
}
]
}
}
}
Or you can use your own certificate to verify with a png file containing your QR code with the help of zbar bar code reader.
Example usage (essential information omitted)
% zbarimg --quiet --raw qr.png | go run cmd/qr.go
Correct signature against known key identifier vvYa1vaWkGg= and Issuer GR
Verified health certificate: {
"iss": "GR",
"exp": 0,
"iat": 0,
"hcert": {
"eu_dgc_v1": {
"ver": "1.3.0",
"nam": {
"fn": "",
"fnt": "",
"gn": "",
"gnt": ""
},
"dob": "",
"v": [
{
"tg": "",
"vp": "",
"mp": "",
"ma": "",
"dn": 0,
"sd": 0,
"dt": "",
"co": "GR",
"is": "IDIKA / Ministry of Digital Governance",
"ci": ""
}
]
}
}
}
Or you can simply start the web server and make queries with curl or any other client.
% go run cmd/api.go
API is starting to listen the connections on :3000
% curl -d '{"qr": "HC1:6BF+70790T9WJWG.FKY*4GO0.O1CV2 O5 N2FBBRW1*70HS8WY04AC*WIFN0AHCD8KD97TK0F90KECTHGWJC0FDC:5AIA%G7X+AQB9746HS80:54IBQF60R6$A80X6S1BTYACG6M+9XG8KIAWNA91AY%67092L4WJCT3EHS8XJC$+DXJCCWENF6OF63W5NW6WF6%JC QE/IAYJC5LEW34U3ET7DXC9 QE-ED8%E.JCBECB1A-:8$96646AL60A60S6Q$D.UDRYA 96NF6L/5QW6307KQEPD09WEQDD+Q6TW6FA7C466KCN9E%961A6DL6FA7D46JPCT3E5JDLA7$Q6E464W5TG6..DX%DZJC6/DTZ9 QE5$CB$DA/D JC1/D3Z8WED1ECW.CCWE.Y92OAGY8MY9L+9MPCG/D5 C5IA5N9$PC5$CUZCY$5Y$527B+A4KZNQG5TKOWWD9FL%I8U$F7O2IBM85CWOC%LEZU4R/BXHDAHN 11$CA5MRI:AONFN7091K9FKIGIY%VWSSSU9%01FO2*FTPQ3C3F"}' -H "Content-Type: application/json" -X POST http://localhost:3000/api/query
{"status":{"verified":false,"message":"certificate for country DE and key identifier DEsVUSvpFAE= could not be found\n"},"dgc":{"iss":"DE","exp":1643356073,"iat":1622316073,"hcert":{"eu_dgc_v1":{"ver":"1.0.0","nam":{"fn":"Mustermann","fnt":"MUSTERMANN","gn":"Erika","gnt":"ERIKA"},"dob":"1964-08-12","v":[{"tg":"840539006","vp":"1119349007","mp":"EU/1/20/1507","ma":"ORG-100031184","dn":2,"sd":2,"dt":"2021-05-29","co":"DE","is":"Robert Koch-Institut","ci":"URN:UVCI:01DE/IZ12345A/5CWLU12RNOB9RXSEOP6FG8#W"}]}}}}
Click to show internal directories.
Click to hide internal directories.