X-Git-Url: http://nsz.repo.hu/git/?a=blobdiff_plain;f=document%2Fdocument.go;h=0ac99b66cb6a6269c2a2949fcbb04c716c11ae60;hb=726f6af292a7ec7a1698b6bec11ea0ee48bfd001;hp=c410f61833f7c675b8c1c46b4525d5a9e9a87dd3;hpb=75502b9220b35cdc4fe15977f8aeb794535bcd35;p=epoint diff --git a/document/document.go b/document/document.go index c410f61..0ac99b6 100644 --- a/document/document.go +++ b/document/document.go @@ -26,10 +26,14 @@ // -----END PGP SIGNATURE----- package document -// TODO: error types -// TODO: optional fields -// TODO: fields of notice (last notice, serial) -// TODO: space vs not to space +// TODO: error wrapper (so reporting to user or creating bounce cert is simple) +// TODO: optional fields: exact semantics ("" vs "-" vs nil) +// TODO: trailing space handling in ParseFields +// TODO: fields of notice (last notice, serial, failure notice,..) +// TODO: limits and cert type specific input validation +// TODO: fix Cert mess +// TODO: nonce is id, id is even number of hex digits +// TODO: denom, issuer from key (key representation: armor?) import ( "bytes" @@ -37,6 +41,7 @@ import ( "crypto/openpgp" "crypto/openpgp/armor" "crypto/openpgp/packet" + "crypto/sha1" "encoding/hex" "fmt" "reflect" @@ -45,6 +50,15 @@ import ( "time" ) +// limits +const ( + MaxFields = 20 + MaxLineLength = 160 // 1 sha512 + 1 key (without \n) + MaxValueLength = 1300 // 20 sha256 space separated (without \n) + MaxNonceLength = 20 + MaxDenominationLength = 100 +) + const ClearSignedHeader = "-----BEGIN PGP SIGNED MESSAGE-----" // MIME type for epoint documents, see RFC 4288 @@ -92,7 +106,7 @@ var fieldtype = map[string]string{ "Last-Credit-Serial": "int", "Last-Debit-Serial": "int", "Maturity-Date": "date", - "Nonce": "text", + "Nonce": "id", "Notes": "text", "References": "ids", "Serial": "int", @@ -115,17 +129,38 @@ type Draft struct { Denomination string Issuer string AuthorizedBy string - MaturityDate int64 // optional - ExpiryDate int64 // optional - Nonce string // optional - Notes string // optional + MaturityDate *int64 // optional + ExpiryDate *int64 // optional + Nonce string + Notes *string // optional } type Notice struct { Date int64 AuthorizedBy string - Notes string - References []string + Notes *string // optional + References []string // may be empty (startup notice) +} + +type Cert struct { + IsDebit bool + Holder string + Serial int64 + Balance int64 + Denomination string + Issuer string + Date int64 + Difference int64 + Draft string + Beneficiary *string // only in debit cert + Drawer *string // only in credit cert + DebitCert *string // only in credit cert + AuthorizedBy string + Notes *string // optional + LastDebitSerial int64 // 0 if none + LastCreditSerial int64 // 0 if none + LastCert *string // nil if serial == 1 + References []string } type DebitCert struct { @@ -137,12 +172,13 @@ type DebitCert struct { Date int64 Difference int64 Draft string + Beneficiary string AuthorizedBy string - Notes string // optional - LastDebitSerial int64 // 0 if none - LastCreditSerial int64 // 0 if none - LastCert string // ? if serial == 1 - References []string // may be empty + Notes *string // optional + LastDebitSerial int64 // 0 if none + LastCreditSerial int64 // 0 if none + LastCert *string // nil if serial == 1 + References []string } type CreditCert struct { @@ -157,24 +193,76 @@ type CreditCert struct { Drawer string DebitCert string AuthorizedBy string - Notes string // optional - LastDebitSerial int64 // 0 if none - LastCreditSerial int64 // 0 if none - LastCert string // ? if serial == 1 - References []string // may be empty + Notes *string // optional + LastDebitSerial int64 // 0 if none + LastCreditSerial int64 // 0 if none + LastCert *string // ? if serial == 1 + References []string } type BounceCert struct { Drawer string Draft string - LastCert string // optional - Balance int64 // 0 if none - Denomination string - Issuer string + LastCert *string // optional + Balance int64 // 0 if none Date int64 AuthorizedBy string - Notes string // optional - References []string // may be empty + Notes *string // optional + References []string +} + +func ToCert(v interface{}) (cert *Cert, err error) { + cert = new(Cert) + switch x := v.(type) { + case *DebitCert: + cert.IsDebit = true + cert.Beneficiary = &x.Beneficiary + + cert.Holder = x.Holder + cert.Serial = x.Serial + cert.Balance = x.Balance + cert.Denomination = x.Denomination + cert.Issuer = x.Issuer + cert.Date = x.Date + cert.Difference = x.Difference + cert.Draft = x.Draft + cert.AuthorizedBy = x.AuthorizedBy + cert.Notes = x.Notes + cert.LastDebitSerial = x.LastDebitSerial + cert.LastCreditSerial = x.LastCreditSerial + cert.LastCert = x.LastCert + cert.References = x.References + + case *CreditCert: + cert.IsDebit = false + cert.Drawer = &x.Drawer + cert.DebitCert = &x.DebitCert + + cert.Holder = x.Holder + cert.Serial = x.Serial + cert.Balance = x.Balance + cert.Denomination = x.Denomination + cert.Issuer = x.Issuer + cert.Date = x.Date + cert.Difference = x.Difference + cert.Draft = x.Draft + cert.AuthorizedBy = x.AuthorizedBy + cert.Notes = x.Notes + cert.LastDebitSerial = x.LastDebitSerial + cert.LastCreditSerial = x.LastCreditSerial + cert.LastCert = x.LastCert + cert.References = x.References + default: + err = fmt.Errorf("ToCert: only debit or credit document can be converted to cert") + } + return +} + +// sha1 sum of the (cleaned) document body as uppercase hex string +func Id(c *Signed) string { + h := sha1.New() + h.Write(c.Body) + return fmt.Sprintf("%040X", h.Sum()) } // parse an epoint document without checking the signature and format details @@ -192,7 +280,7 @@ func Parse(s []byte) (iv interface{}, c *Signed, err error) { } // format and sign an epoint document -func Format(iv interface{}, key *openpgp.Entity) (s []byte, err error) { +func Format(iv interface{}, key *openpgp.Entity) (s []byte, c *Signed, err error) { doc, err := FormatStruct(iv) if err != nil { return @@ -201,32 +289,21 @@ func Format(iv interface{}, key *openpgp.Entity) (s []byte, err error) { if err != nil { return } - c, err := Sign(body, key) + c, err = Sign(body, key) if err != nil { return } - return FormatSigned(c) + s, err = FormatSigned(c) + return } // verify an epoint document, return the cleaned version as well -func Verify(c *Signed, key *openpgp.Entity) (cleaned []byte, err error) { - err = CleanSigned(c) - if err != nil { - return - } - err = VerifyCleaned(c, key) - if err != nil { - return - } - return FormatSigned(c) -} - -// verify signature of body with given key -func VerifyCleaned(c *Signed, key *openpgp.Entity) (err error) { - kr := openpgp.EntityList{key} +func Verify(c *Signed, key openpgp.KeyRing) (err error) { msg := bytes.NewBuffer(c.Body) sig := bytes.NewBuffer(c.Signature) - _, err = openpgp.CheckArmoredDetachedSignature(kr, msg, sig) + // TODO: verify signature + _, _ = msg, sig + // _, err = openpgp.CheckArmoredDetachedSignature(key, msg, sig) return } @@ -237,6 +314,11 @@ func Sign(body []byte, key *openpgp.Entity) (c *Signed, err error) { c.Body = body w := new(bytes.Buffer) err = openpgp.ArmoredDetachSignText(w, key, bytes.NewBuffer(c.Body)) + if err != nil { + return + } + // close armored document with a \n + _, _ = w.Write([]byte{'\n'}) c.Signature = w.Bytes() return } @@ -421,6 +503,7 @@ func ParseStruct(doc *Document) (iv interface{}, err error) { v := reflect.ValueOf(iv).Elem() t := v.Type() n := v.NumField() + nokey := 0 for i := 0; i < n; i++ { ft := t.Field(i) fv := v.Field(i) @@ -431,8 +514,22 @@ func ParseStruct(doc *Document) (iv interface{}, err error) { seen[key] = true s, ok := doc.Fields[key] if !ok { + if fv.Kind() == reflect.Ptr { + // missing optional key: leave the pointer as nil + nokey++ + continue + } return nil, fmt.Errorf("ParseStruct: field %s of %s is missing\n", key, t.Name()) } + if fv.Kind() == reflect.Ptr { + if s == "" || s == "-" { + // TODO + // empty optional key: same as missing + continue + } + fv.Set(reflect.New(fv.Type().Elem())) + fv = fv.Elem() + } switch fieldtype[key] { case "id": var val string @@ -451,6 +548,7 @@ func ParseStruct(doc *Document) (iv interface{}, err error) { val, err = parseDate(s) fv.SetInt(val) case "ids": + // TODO: empty slice? ids := strings.Split(s, " ") val := make([]string, len(ids)) for j, id := range ids { @@ -467,7 +565,7 @@ func ParseStruct(doc *Document) (iv interface{}, err error) { return } } - if len(doc.Fields) != n { + if len(doc.Fields)+nokey != n { for k := range doc.Fields { if !seen[k] { err = fmt.Errorf("ParseStruct: unknown field %s in %s", k, t.Name()) @@ -498,6 +596,14 @@ func FormatStruct(iv interface{}) (doc *Document, err error) { key = ft.Name } val := "" + if fv.Kind() == reflect.Ptr { + if fv.IsNil() { + // keep empty optional fields but mark them + val = "-" + goto setval + } + fv = fv.Elem() + } switch fieldtype[key] { case "id": val = formatId(fv.String()) @@ -518,6 +624,7 @@ func FormatStruct(iv interface{}) (doc *Document, err error) { default: panic("bad field type " + key + " " + fieldtype[key]) } + setval: doc.Fields[key] = val doc.Order = append(doc.Order, key) } @@ -527,35 +634,49 @@ func FormatStruct(iv interface{}) (doc *Document, err error) { func ParseFields(s []byte) (fields map[string]string, rest []byte, err error) { rest = s fields = make(map[string]string) - lastkey := "" + key := "" + // \n is optional after the last field and an extra \n is allowed as well for len(rest) > 0 { var line []byte line, rest = getLine(rest) - // empty line after the parsed fields is consumed + // empty line after the last field is consumed if len(line) == 0 { break } - // TODO: empty line: " \n" - if line[0] == ' ' { - if lastkey == "" { - err = fmt.Errorf("ParseFields: expected a field, not ' '") - return - } - fields[lastkey] += string(line) + if line[0] == ' ' && key != "" { + // "Key: v1\n v2\n" is equivalent to "Key: v1 v2\n" + fields[key] += string(line) continue } - // TODO: empty value: "Key: \n" - i := bytes.Index(line, []byte(": ")) + if line[0] < 'A' || line[0] > 'Z' { + err = fmt.Errorf("ParseFields: field name must start with an upper-case ascii letter") + return + } + i := bytes.IndexByte(line, ':') if i < 0 { - err = fmt.Errorf("ParseFields: missing ': '") - break + err = fmt.Errorf("ParseFields: missing ':'") + return } - lastkey = string(line[:i]) - if _, ok := fields[lastkey]; ok { + key = string(line[:i]) + if _, ok := fields[key]; ok { err = fmt.Errorf("ParseFields: repeated fields are not allowed") return } - fields[lastkey] = string(line[i+2:]) + fields[key] = string(line[i+1:]) + } + for key, v := range fields { + // either a single space follows ':' or the value is empty + // good: "Key:\n", "Key:\n value\n", "Key: value\n", "Key: v1\n v2\n" + // bad: "Key:value\n", "Key: \nvalue\n" + // bad but not checked here: "Key: \n", "Key: value \n", "Key:\n \n value\n" + if len(v) == 0 { + continue + } + if v[0] != ' ' { + err = fmt.Errorf("ParseFields: ':' is not followed by ' '") + return + } + fields[key] = v[1:] } return } @@ -563,9 +684,8 @@ func ParseFields(s []byte) (fields map[string]string, rest []byte, err error) { // TODO: limit errors func parseId(s string) (string, error) { - if len(s) != 40 { - return "", fmt.Errorf("parseId: expected 40 characters; got %d", len(s)) - } + // check if hex decodable + // TODO: length check dst := make([]byte, len(s)/2) _, err := hex.Decode(dst, []byte(s)) return s, err @@ -576,8 +696,8 @@ func formatId(s string) string { } func parseString(s string) (string, error) { - if len(s) > 140 { - return "", fmt.Errorf("parseString: 140 chars limit is exceeded") + if len(s) > MaxValueLength { + return "", fmt.Errorf("parseString: length limit is exceeded") } return s, nil } @@ -587,6 +707,7 @@ func formatString(s string) string { } func parseDate(s string) (int64, error) { + // TODO: fractional seconds? t, err := time.Parse(time.RFC3339, s) if err != nil { return 0, err @@ -599,7 +720,7 @@ func formatDate(i int64) string { } func getLine(data []byte) (line, rest []byte) { - i := bytes.Index(data, []byte{'\n'}) + i := bytes.IndexByte(data, '\n') j := i + 1 if i < 0 { i = len(data)