diff --git a/README.md b/README.md index b2e6212..20034ea 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Golang implementation of the covid certificates. At the moment it only includes ## Features - Decode signed DCC (European QRCodes) data ✅ -- Decode 2D-DOC data ❌ (planned) +- Decode 2D-DOC data ✅ - Pretty-print decoded data as JSON ✅ - Download public-keys from european gateway ❌ (planned) - Verify signature ❌ (planned) @@ -15,10 +15,10 @@ Golang implementation of the covid certificates. At the moment it only includes ## Usage `gocovidcertificate ` -| Flag | Type | Description | Required | Default value | -| ------ | ------ | ------------------------ | -------- | ------------- | -| -code | string | QRCode string to decode | true | none | -| -print | bool | Prints the QRCode data to console | false | true | +| Flag | Type | Description | Required | Default value | +| ------ | ------ | --------------------------------------------------------- | -------- | ------------- | +| -code | string | QRCode data to decode (put it between double-quotes `""`) | true | none | +| -print | bool | Prints the QRCode data to console | false | true | Example: `gocovidcertificate -code "HC1:..." -print` diff --git a/dcc/dcc.go b/dcc/dcc.go deleted file mode 100644 index 168a911..0000000 --- a/dcc/dcc.go +++ /dev/null @@ -1,29 +0,0 @@ -package dcc - -import ( - "errors" - "github.com/alexpresso/gocovidcertificate/types" - "github.com/alexpresso/gocovidcertificate/utils" - "strings" -) - - -var prefixes = map[string]bool{"HC1":true, "LT1":true} - -func Decode(input string) (*types.COSE, error) { - if split := strings.Split(input, ":")[0]; !prefixes[split] { - return nil, errors.New("input not starting with a DCC prefix") - } - - for prefix := range prefixes { - input = strings.TrimPrefix(input, prefix + ":") - } - - bytes := utils.Base45decode(input) - - if bytes[0] == 0x78 { - bytes = utils.ZlibDecompress(bytes) - } - - return utils.DecodeCOSE(bytes), nil -} diff --git a/decoders/2ddoc.go b/decoders/2ddoc.go new file mode 100644 index 0000000..e537e2a --- /dev/null +++ b/decoders/2ddoc.go @@ -0,0 +1,86 @@ +package decoders + +import ( + "encoding/json" + "github.com/alexpresso/gocovidcertificate/types" + "github.com/alexpresso/gocovidcertificate/utils" + "strconv" + "strings" +) + +var TwoDPrefix = "DC" + +func twoDDocDecode(input string) (certificate *types.Certificate, err error) { + header, remaining, err := decodeHeader(input) + if err != nil { + return nil, err + } + + message, signature, err := decodeData(remaining) + if err != nil { + return nil, err + } + + return &types.Certificate{ + Type: types.TWODDOC, + Data: &types.TwoDDoc{ + Header: header, + Message: message, + Signature: signature, + }, + }, nil +} + +func decodeHeader(input string) (header *types.TwoDDocHeader, remaining string, err error) { + length := 22 + version, err := strconv.Atoi(utils.Substring(input, 2, 4)) + if err != nil { + return nil, "", err + } + + header = &types.TwoDDocHeader{ + IDFlag: utils.Substring(input, 0, 2), + Version: version, + Issuer: utils.Substring(input, 4, 8), + CertID: utils.Substring(input, 8, 12), + DocumentDate: utils.Substring(input, 12, 16), + SignatureDate: utils.Substring(input, 16, 20), + DocumentTypeID: utils.Substring(input, 20, 22), + } + + switch version { + case 3: + header.PerimeterID = utils.Substring(input, 22, 24) + length = 24 + case 4: + header.PerimeterID = utils.Substring(input, 22, 24) + header.CountryID = utils.Substring(input, 24, 26) + length = 26 + } + + return header, utils.Substring(input, length, len(input)), err +} + +func decodeData(input string) (message *types.TwoDDocMessage, signature string, err error) { + data := make(map[string]interface{}) + + units := strings.Split(input, string(rune(31))) + groups := strings.Split(units[0], string(rune(29))) + signature = units[1] + + for _, group := range groups { + key, value := decodeField(group) + data[key] = value + } + + jsonString, err := json.Marshal(data) + err = json.Unmarshal(jsonString, &message) + + return +} + +func decodeField(input string) (key string, value string) { + key = utils.Substring(input, 0, 2) + value = utils.Substring(input, 2, len(input)) + return +} diff --git a/decoders/dcc.go b/decoders/dcc.go new file mode 100644 index 0000000..ccbfb78 --- /dev/null +++ b/decoders/dcc.go @@ -0,0 +1,40 @@ +package decoders + +import ( + "github.com/alexpresso/gocovidcertificate/types" + "github.com/alexpresso/gocovidcertificate/utils" + "strings" +) + +var DCCPrefixes = map[string]bool{"HC1": true, "LT1": true} + +func dccDecode(input string) (*types.Certificate, error) { + var err error + var bytes []byte + + for prefix := range DCCPrefixes { + input = strings.TrimPrefix(input, prefix+":") + } + + bytes, err = utils.Base45decode(input) + if err != nil { + return nil, err + } + + if bytes[0] == 0x78 { + bytes, err = utils.ZlibDecompress(bytes) + if err != nil { + return nil, err + } + } + + cose, err := utils.DecodeCOSE(bytes) + if err != nil { + return nil, err + } + + return &types.Certificate{ + Type: types.DCC, + Data: cose, + }, nil +} diff --git a/decoders/decoder.go b/decoders/decoder.go new file mode 100644 index 0000000..7702aba --- /dev/null +++ b/decoders/decoder.go @@ -0,0 +1,17 @@ +package decoders + +import ( + "errors" + "github.com/alexpresso/gocovidcertificate/types" + "strings" +) + +func Decode(input string) (*types.Certificate, error) { + if split := strings.Split(input, ":")[0]; DCCPrefixes[split] { + return dccDecode(input) + } else if strings.HasPrefix(input, TwoDPrefix) { + return twoDDocDecode(input) + } + + return nil, errors.New("unsupported code format") +} diff --git a/gocovidcertificate.go b/gocovidcertificate.go index 9442b5a..da59dd3 100644 --- a/gocovidcertificate.go +++ b/gocovidcertificate.go @@ -4,16 +4,15 @@ import ( "encoding/json" "flag" "fmt" - "github.com/alexpresso/gocovidcertificate/dcc" + "github.com/alexpresso/gocovidcertificate/decoders" "log" ) - func main() { var ( - err error + err error mustPrint = flag.Bool("print", true, "Prints content of the decoded certificate") - code = flag.String("code", "", "The DCC code you want to process") + code = flag.String("code", "", "The DCC code you want to process") ) flag.Parse() @@ -22,13 +21,13 @@ func main() { log.Fatal("Missing -code flag") } - cose, err := dcc.Decode(*code) + data, err := decoders.Decode(*code) if err != nil { log.Fatal(err) } if *mustPrint { - indent, err := json.MarshalIndent(cose, "", " ") + indent, err := json.MarshalIndent(data, "", " ") if err != nil { log.Fatal(err) } diff --git a/types/2dcertificate.go b/types/2dcertificate.go new file mode 100644 index 0000000..01c15cb --- /dev/null +++ b/types/2dcertificate.go @@ -0,0 +1,32 @@ +package types + +type TwoDDoc struct { + Header *TwoDDocHeader + Message *TwoDDocMessage + Signature string +} + +type TwoDDocHeader struct { + IDFlag string + Version int + Issuer string + CertID string + DocumentDate string + SignatureDate string + DocumentTypeID string + PerimeterID string + CountryID string +} + +type TwoDDocMessage struct { + Lastname string `json:"L0"` + Firstname string `json:"L1"` + DateOfBirth string `json:"L2"` + TargetedAgent string `json:"L3"` + VaccineATC string `json:"L4"` + Dose1Manufacturer string `json:"L5"` + Dose2Manufacturer string `json:"L6"` + Dose int `json:"L7,string"` + RequiredDoses int `json:"L8,string"` + Date int `json:"L9,string"` +} diff --git a/types/certificate.go b/types/certificate.go index ed7b250..e108deb 100644 --- a/types/certificate.go +++ b/types/certificate.go @@ -1,57 +1,13 @@ package types -// DccRoot see https://github.com/ehn-dcc-development/ehn-dcc-schema -type DccRoot struct { - DCC covidCertificate `cbor:"1,keyasint"` -} - -type covidCertificate struct { - Version string `cbor:"ver" json:"version"` - DateOfBirth string `cbor:"dob" json:"dateOfBirth"` - Name name `cbor:"nam" json:"name"` - Vaccines []vaccineEntry `cbor:"v" json:"vaccines"` - Tests []testEntry `cbor:"t" json:"tests"` - Recoveries []recoveryEntry `cbor:"r" json:"recoveries"` -} - -type name struct { - Surname string `cbor:"fn" json:"surname"` - StdSurname string `cbor:"fnt" json:"stdSurname"` - Forename string `cbor:"gn" json:"forename"` - StdForename string `cbor:"gnt" json:"stdForename"` -} - -type vaccineEntry struct { - TargetedAgent string `cbor:"tg" json:"targetedAgent"` - Vaccine string `cbor:"vp" json:"vaccine"` - Product string `cbor:"mp" json:"product"` - Manufacturer string `cbor:"ma" json:"manufacturer"` - Dose int64 `cbor:"dn" json:"doseNumber"` - DoseSeries int64 `cbor:"sd" json:"doseSeries"` - Date string `cbor:"dt" json:"date"` - Country string `cbor:"co" json:"country"` - Issuer string `cbor:"is" json:"issuer"` - CertificateID string `cbor:"ci" json:"certificateID"` -} +const ( + DCC CertificateType = "DCC" + TWODDOC CertificateType = "2DDOC" +) -type testEntry struct { - TargetedAgent string `cbor:"tg" json:"targetedAgent"` - TestType string `cbor:"tt" json:"testType"` - Name string `cbor:"nm" json:"name"` - Manufacturer string `cbor:"ma" json:"manufacturer"` - SampleDatetime string `cbor:"sc" json:"sampleDatetime"` - TestResult string `cbor:"tr" json:"testResult"` - TestingCentre string `cbor:"tc" json:"testingCentre"` - Country string `cbor:"co" json:"country"` - Issuer string `cbor:"is" json:"issuer"` - CertificateID string `cbor:"ci" json:"certificateID"` -} +type CertificateType string -type recoveryEntry struct { - TargetedAgent string `cbor:"tg" json:"targetedAgent"` - Country string `cbor:"co" json:"country"` - Issuer string `cbor:"is" json:"issuer"` - ValidFrom string `cbor:"df" json:"validFrom"` - ValidUntil string `cbor:"du" json:"validUntil"` - CertificateID string `cbor:"ci" json:"certificateID"` +type Certificate struct { + Type CertificateType + Data interface{} } diff --git a/types/cose.go b/types/cose.go index e6b97cc..be9bc86 100644 --- a/types/cose.go +++ b/types/cose.go @@ -2,35 +2,35 @@ package types // Signed see https://datatracker.ietf.org/doc/html/rfc8152#section-2 type Signed struct { - _ struct{} `cbor:",toarray"` - Protected []byte - Unprotected map[interface{}]interface{} - Content []byte - Signature []byte + _ struct{} `cbor:",toarray"` + Protected []byte + Unprotected map[interface{}]interface{} + Content []byte + Signature []byte } // Header see https://datatracker.ietf.org/doc/html/rfc8152#section-3.1 & https://www.iana.org/assignments/cose/cose.xhtml type Header struct { - Alg int `cbor:"1,keyasint,omitempty"` - Kid []byte `cbor:"4,keyasint,omitempty"` - IV []byte `cbor:"5,keyasint,omitempty"` + Alg int `cbor:"1,keyasint,omitempty"` + Kid []byte `cbor:"4,keyasint,omitempty"` + IV []byte `cbor:"5,keyasint,omitempty"` } // Claims see https://datatracker.ietf.org/doc/html/draft-ietf-ace-cbor-web-token-08#section-3.1 type Claims struct { - Iss string `cbor:"1,keyasint"` - Sub string `cbor:"2,keyasint"` - Aud string `cbor:"3,keyasint"` - Exp int64 `cbor:"4,keyasint"` - Nbf int `cbor:"5,keyasint"` - Iat int64 `cbor:"6,keyasint"` - Cti []byte `cbor:"7,keyasint"` - HCData DccRoot `cbor:"-260,keyasint"` - LCData DccRoot `cbor:"-250,keyasint"` + Iss string `cbor:"1,keyasint"` + Sub string `cbor:"2,keyasint"` + Aud string `cbor:"3,keyasint"` + Exp int64 `cbor:"4,keyasint"` + Nbf int `cbor:"5,keyasint"` + Iat int64 `cbor:"6,keyasint"` + Cti []byte `cbor:"7,keyasint"` + HCData DccRoot `cbor:"-260,keyasint"` + LCData DccRoot `cbor:"-250,keyasint"` } type COSE struct { - Signed Signed `json:"-"` - Header Header - Claims Claims + Signed Signed `json:"-"` + Header Header + Claims Claims } diff --git a/types/dcccertificate.go b/types/dcccertificate.go new file mode 100644 index 0000000..a02ebb7 --- /dev/null +++ b/types/dcccertificate.go @@ -0,0 +1,57 @@ +package types + +// DccRoot see https://github.com/ehn-dcc-development/ehn-dcc-schema +type DccRoot struct { + DCC covidCertificate `cbor:"1,keyasint"` +} + +type covidCertificate struct { + Version string `cbor:"ver" json:"version"` + DateOfBirth string `cbor:"dob" json:"dateOfBirth"` + Name name `cbor:"nam" json:"name"` + Vaccines []vaccineEntry `cbor:"v" json:"vaccines"` + Tests []testEntry `cbor:"t" json:"tests"` + Recoveries []recoveryEntry `cbor:"r" json:"recoveries"` +} + +type name struct { + Surname string `cbor:"fn" json:"surname"` + StdSurname string `cbor:"fnt" json:"stdSurname"` + Forename string `cbor:"gn" json:"forename"` + StdForename string `cbor:"gnt" json:"stdForename"` +} + +type vaccineEntry struct { + TargetedAgent string `cbor:"tg" json:"targetedAgent"` + Vaccine string `cbor:"vp" json:"vaccine"` + Product string `cbor:"mp" json:"product"` + Manufacturer string `cbor:"ma" json:"manufacturer"` + Dose int64 `cbor:"dn" json:"doseNumber"` + DoseSeries int64 `cbor:"sd" json:"doseSeries"` + Date string `cbor:"dt" json:"date"` + Country string `cbor:"co" json:"country"` + Issuer string `cbor:"is" json:"issuer"` + CertificateID string `cbor:"ci" json:"certificateID"` +} + +type testEntry struct { + TargetedAgent string `cbor:"tg" json:"targetedAgent"` + TestType string `cbor:"tt" json:"testType"` + Name string `cbor:"nm" json:"name"` + Manufacturer string `cbor:"ma" json:"manufacturer"` + SampleDatetime string `cbor:"sc" json:"sampleDatetime"` + TestResult string `cbor:"tr" json:"testResult"` + TestingCentre string `cbor:"tc" json:"testingCentre"` + Country string `cbor:"co" json:"country"` + Issuer string `cbor:"is" json:"issuer"` + CertificateID string `cbor:"ci" json:"certificateID"` +} + +type recoveryEntry struct { + TargetedAgent string `cbor:"tg" json:"targetedAgent"` + Country string `cbor:"co" json:"country"` + Issuer string `cbor:"is" json:"issuer"` + ValidFrom string `cbor:"df" json:"validFrom"` + ValidUntil string `cbor:"du" json:"validUntil"` + CertificateID string `cbor:"ci" json:"certificateID"` +} diff --git a/utils/base45.go b/utils/base45.go index 28c3898..be68912 100644 --- a/utils/base45.go +++ b/utils/base45.go @@ -1,14 +1,20 @@ package utils -import "bytes" - +import ( + "bytes" + "errors" +) var charset = []byte("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:") -func Base45decode(input string) []byte { +func Base45decode(input string) ([]byte, error) { length := len(input) buffer := make([]int, length) + if length%3 != 0 { + return nil, errors.New("unable to decode data, it must not have been encoded in base45") + } + var res []byte for i := 0; i < length; i++ { @@ -16,19 +22,19 @@ func Base45decode(input string) []byte { } for i := 0; i < length; i += 3 { - if length - i >= 3 { - var x = buffer[i] + buffer[i + 1] * 45 + buffer[i + 2] * 45 * 45 + if length-i >= 3 { + var x = buffer[i] + buffer[i+1]*45 + buffer[i+2]*45*45 quotient, remainder := divmod(x, 256) res = append(res, byte(quotient), byte(remainder)) } else { - res = append(res, byte(buffer[i] + buffer[i + 1] * 45)) + res = append(res, byte(buffer[i]+buffer[i+1]*45)) } } - return res + return res, nil } func divmod(numerator, denominator int) (quotient, remainder int) { return numerator / denominator, numerator % denominator -} \ No newline at end of file +} diff --git a/utils/cose.go b/utils/cose.go index 26de6eb..cbee5bc 100644 --- a/utils/cose.go +++ b/utils/cose.go @@ -1,33 +1,32 @@ package utils import ( + "errors" "github.com/alexpresso/gocovidcertificate/types" "github.com/fxamacker/cbor/v2" - "log" ) - -func DecodeCOSE(bytes []byte) *types.COSE { +func DecodeCOSE(bytes []byte) (*types.COSE, error) { var signed types.Signed if err := cbor.Unmarshal(bytes, &signed); err != nil { - log.Fatal(err) + return nil, errors.New("unable to decode cose") } var header types.Header if len(signed.Protected) > 0 { if err := cbor.Unmarshal(signed.Protected, &header); err != nil { - log.Fatal(err) + return nil, errors.New("unable to compute signed data") } } var claims types.Claims if err := cbor.Unmarshal(signed.Content, &claims); err != nil { - log.Fatal(err) + return nil, errors.New("") } return &types.COSE{ Signed: signed, Header: header, Claims: claims, - } -} \ No newline at end of file + }, nil +} diff --git a/utils/stringutils.go b/utils/stringutils.go new file mode 100644 index 0000000..9bf7435 --- /dev/null +++ b/utils/stringutils.go @@ -0,0 +1,6 @@ +package utils + +func Substring(input string, start int, length int) (out string) { + runes := []rune(input) + return string(runes[start:length]) +} diff --git a/utils/zlib.go b/utils/zlib.go index 2076da9..867f3dc 100644 --- a/utils/zlib.go +++ b/utils/zlib.go @@ -3,27 +3,26 @@ package utils import ( "bytes" "compress/zlib" + "errors" "io/ioutil" - "log" ) - -func ZlibDecompress(b []byte) []byte { +func ZlibDecompress(b []byte) ([]byte, error) { var err error z, err := zlib.NewReader(bytes.NewReader(b)) if err != nil { - log.Fatal(err) + return nil, errors.New("error while decompressing data, zlib error") } defer func() { err = z.Close() - if err != nil { - log.Fatal(err) - } }() + if err != nil { + return nil, errors.New("error while decompressing data, zlib reader refused to close") + } out, _ := ioutil.ReadAll(z) - return out + return out, nil }