code reorganization
[epoint] / document / document.go
diff --git a/document/document.go b/document/document.go
deleted file mode 100644 (file)
index 0c8b0f4..0000000
+++ /dev/null
@@ -1,695 +0,0 @@
-// Package document implements epoint document parsing and creation.
-//
-// An epoint document is an OpenPGP (RFC 4880) clear signed
-// utf-8 text of key-value pairs.
-// The body contains a content-type MIME header so the document
-// can be used in OpenPGP/MIME (RFC 3156) emails.
-// The format of the key-value pairs are similar to MIME header
-// fields: keys and values are separated by ": ", repeated keys
-// are not allowed, long values can be split before a space.
-//
-// Example:
-//
-// -----BEGIN PGP SIGNED MESSAGE-----
-// Hash: SHA1
-//
-// Content-Type: text/plain.epoint.type; charset=utf-8
-//
-// Key: Value1
-// Another-Key: Value2
-// Last-Key: Long
-//  value that spans
-//  multiple lines
-// -----BEGIN PGP SIGNATURE-----
-//
-// pgp signature
-// -----END PGP SIGNATURE-----
-package document
-
-// 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 (require only drawer.nonce to be uniq)
-// TODO: denom, issuer from key (key representation: armor?)
-
-import (
-       "bytes"
-       "crypto"
-       "crypto/openpgp"
-       "crypto/openpgp/armor"
-       "crypto/openpgp/packet"
-       "crypto/sha1"
-       "encoding/hex"
-       "fmt"
-       "reflect"
-       "strconv"
-       "strings"
-       "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
-var ContentType = map[string]string{
-       "Draft":      "text/vnd.epoint.draft; charset=utf-8",
-       "Notice":     "text/vnd.epoint.notice; charset=utf-8",
-       "DebitCert":  "text/vnd.epoint.debit; charset=utf-8",
-       "CreditCert": "text/vnd.epoint.credit; charset=utf-8",
-       "BounceCert": "text/vnd.epoint.bounce; charset=utf-8",
-}
-
-// OpenPGP signed cleartext document representation
-type Signed struct {
-       // Sign and CleanSigned sets Hash for FormatSigned
-       // TODO: CreationDate
-       Hash string
-       // Signed text (no dash escape, no trailing space, \n new lines)
-       Body []byte
-       // Armored detached text signature of the Body
-       Signature []byte
-}
-
-// parsed epoint document
-type Document struct {
-       Type   string
-       Fields map[string]string
-       Order  []string
-}
-
-var fieldtype = map[string]string{
-       "Amount":             "int",
-       "Authorized-By":      "id",
-       "Balance":            "int",
-       "Beneficiary":        "id",
-       "Date":               "date",
-       "Debit-Cert":         "id",
-       "Denomination":       "text",
-       "Difference":         "int",
-       "Draft":              "id",
-       "Drawer":             "id",
-       "Expiry-Date":        "date",
-       "Holder":             "id",
-       "Issuer":             "id",
-       "Last-Cert":          "id",
-       "Last-Credit-Serial": "int",
-       "Last-Debit-Serial":  "int",
-       "Maturity-Date":      "date",
-       "Nonce":              "id",
-       "Notes":              "text",
-       "References":         "ids",
-       "Serial":             "int",
-}
-
-var fieldname = map[string]string{
-       "AuthorizedBy":     "Authorized-By",
-       "DebitCert":        "Debit-Cert",
-       "ExpiryDate":       "Expiry-Date",
-       "LastCert":         "Last-Cert",
-       "LastCreditSerial": "Last-Credit-Serial",
-       "LastDebitSerial":  "Last-Debit-Serial",
-       "MaturityDate":     "Maturity-Date",
-}
-
-type Draft struct {
-       Drawer       string
-       Beneficiary  string
-       Amount       int64
-       Denomination string
-       Issuer       string
-       AuthorizedBy string
-       MaturityDate *int64 // optional
-       ExpiryDate   *int64 // optional
-       Nonce        string
-       Notes        *string // optional
-}
-
-type Notice struct {
-       Date         int64
-       AuthorizedBy string
-       Notes        *string  // optional
-       References   []string // may be empty (startup notice)
-}
-
-type Cert struct {
-       Holder           string
-       Serial           int64
-       Balance          int64
-       Denomination     string
-       Issuer           string
-       Date             int64
-       AuthorizedBy     string
-       Notes            *string // optional
-       LastDebitSerial  int64   // 0 if none
-       LastCreditSerial int64   // 0 if none
-       LastCert         *string // nil if serial == 1
-       References       []string
-       Difference       int64
-       Draft            string
-}
-
-type DebitCert struct {
-       Cert
-       Beneficiary      string
-}
-
-type CreditCert struct {
-       Cert
-       Drawer           string
-       DebitCert        string
-}
-
-type BounceCert struct {
-       Drawer       string
-       Draft        string
-       LastCert     *string // optional
-       Balance      int64   // 0 if none
-       Date         int64
-       AuthorizedBy string
-       Notes        *string // optional
-       References   []string
-}
-
-func ToCert(v interface{}) (cert *Cert, err error) {
-       cert = new(Cert)
-       switch x := v.(type) {
-       case *DebitCert:
-               cert = &x.Cert
-       case *CreditCert:
-               cert = &x.Cert
-       default:
-               err = fmt.Errorf("ToCert: only debit or credit document can be converted to cert")
-       }
-       return
-}
-
-func cleanBody(s []byte) []byte {
-       nl := []byte{'\n'}
-       a := bytes.Split(s, nl)
-       for i := range a {
-               a[i] = bytes.TrimRight(a[i], " \t")
-       }
-       return bytes.Join(a, nl)
-}
-
-// 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
-func Parse(s []byte) (iv interface{}, c *Signed, err error) {
-       c, err = ParseSigned(s)
-       if err != nil {
-               return
-       }
-       doc, err := ParseDocument(c.Body)
-       if err != nil {
-               return
-       }
-       iv, err = ParseStruct(doc)
-       return
-}
-
-// format and sign an epoint document
-func Format(iv interface{}, key *openpgp.Entity) (s []byte, c *Signed, err error) {
-       doc, err := FormatStruct(iv)
-       if err != nil {
-               return
-       }
-       body, err := FormatDocument(doc)
-       if err != nil {
-               return
-       }
-       c, err = Sign(body, key)
-       if err != nil {
-               return
-       }
-       s, err = FormatSigned(c)
-       return
-}
-
-// verify an epoint document, return the cleaned version as well
-func Verify(c *Signed, key openpgp.KeyRing) (err error) {
-       msg := bytes.NewBuffer(c.Body)
-       sig := bytes.NewBuffer(c.Signature)
-       // TODO: verify signature
-       _, _ = msg, sig
-       //      _, err = openpgp.CheckArmoredDetachedSignature(key, msg, sig)
-       return
-}
-
-// sign body with given secret key
-func Sign(body []byte, key *openpgp.Entity) (c *Signed, err error) {
-       c = new(Signed)
-       c.Hash = "SHA256"
-       c.Body = cleanBody(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
-}
-
-// split a clear signed document into body and armored signature
-func ParseSigned(s []byte) (c *Signed, err error) {
-       // look for clear signed header
-       for !bytes.HasPrefix(s, []byte(ClearSignedHeader)) {
-               _, s = getLine(s)
-               if len(s) == 0 {
-                       err = fmt.Errorf("ParseSigned: clear signed header is missing")
-                       return
-               }
-       }
-       s = s[len(ClearSignedHeader):]
-       // end of line after the header
-       empty, s := getLine(s)
-       if len(empty) != 0 {
-               err = fmt.Errorf("ParseSigned: bad clear signed header")
-               return
-       }
-       // skip all hash headers, section 7.
-       for bytes.HasPrefix(s, []byte("Hash: ")) {
-               _, s = getLine(s)
-       }
-       // skip empty line
-       empty, s = getLine(s)
-       if len(empty) != 0 {
-               err = fmt.Errorf("ParseSigned: expected an empty line after armor headers")
-               return
-       }
-       lines := [][]byte{}
-       for !bytes.HasPrefix(s, []byte("-----BEGIN")) {
-               var line []byte
-               line, s = getLine(s)
-               // dash unescape, section 7.1.
-               if bytes.HasPrefix(line, []byte("- ")) {
-                       line = line[2:]
-               }
-               // empty values are not supported: "Key: \n"
-               lines = append(lines, bytes.TrimRight(line, " \t"))
-       }
-       c = new(Signed)
-       // last line is not closed by \n
-       c.Body = bytes.Join(lines, []byte("\n"))
-       // signature is just the rest of the input data
-       c.Signature = s
-       return
-}
-
-// clean up, check and reencode signature
-// used on drafts before calculating the signed document hash
-func CleanSigned(c *Signed) (err error) {
-       b, err := armor.Decode(bytes.NewBuffer(c.Signature))
-       if err != nil {
-               return
-       }
-       if b.Type != openpgp.SignatureType {
-               err = fmt.Errorf("CleanSigned: invalid armored signature type")
-               return
-       }
-       p, err := packet.Read(b.Body)
-       if err != nil {
-               return
-       }
-       sig, ok := p.(*packet.Signature)
-       if !ok {
-               err = fmt.Errorf("CleanSigned: invalid signature packet")
-               return
-       }
-       // section 5.2.3
-       if sig.SigType != packet.SigTypeText {
-               err = fmt.Errorf("CleanSigned: expected text signature")
-               return
-       }
-       switch sig.Hash {
-       case crypto.SHA1:
-               c.Hash = "SHA1"
-       case crypto.SHA256:
-               c.Hash = "SHA256"
-       default:
-               err = fmt.Errorf("CleanSigned: expected SHA1 or SHA256 signature hash")
-               return
-       }
-       // TODO: check CreationTime and other subpackets
-       if sig.SigLifetimeSecs != nil && *sig.SigLifetimeSecs != 0 {
-               err = fmt.Errorf("CleanSigned: signature must not expire")
-               return
-       }
-       out := new(bytes.Buffer)
-       w, err := armor.Encode(out, openpgp.SignatureType, nil)
-       if err != nil {
-               return
-       }
-       err = sig.Serialize(w)
-       if err != nil {
-               return
-       }
-       err = w.Close()
-       if err != nil {
-               return
-       }
-       c.Signature = out.Bytes()
-       return
-}
-
-// create clear signed document
-func FormatSigned(c *Signed) (data []byte, err error) {
-       s := ClearSignedHeader + "\n"
-       if c.Hash != "" {
-               s += "Hash: " + c.Hash + "\n"
-       }
-       s += "\n"
-       s += string(c.Body)
-       s += "\n"
-       s += string(c.Signature)
-       data = []byte(s)
-       return
-}
-
-// parse type and fields of a document body
-func ParseDocument(body []byte) (doc *Document, err error) {
-       // parse content type header first
-       fields, s, err := ParseFields(body)
-       if err != nil {
-               return
-       }
-       ctype, ok := fields["Content-Type"]
-       if len(fields) != 1 || !ok {
-               return nil, fmt.Errorf("ParseBody: expected a single Content-Type header field")
-       }
-       doc = new(Document)
-       for k, v := range ContentType {
-               if ctype == v {
-                       doc.Type = k
-                       break
-               }
-       }
-       if doc.Type == "" {
-               return nil, fmt.Errorf("ParseBody: unknown Content-Type: %s", ctype)
-       }
-       // TODO: doc.Order
-       doc.Fields, s, err = ParseFields(s)
-       if err == nil && len(s) > 0 {
-               err = fmt.Errorf("ParseBody: extra data after fields: %q", s)
-       }
-       return
-}
-
-// create document body
-func FormatDocument(doc *Document) (body []byte, err error) {
-       ctype, ok := ContentType[doc.Type]
-       if !ok {
-               err = fmt.Errorf("FormatDocument: unknown document type: %s", doc.Type)
-               return
-       }
-       s := "Content-Type: " + ctype + "\n\n"
-       for _, k := range doc.Order {
-               s += k + ": " + doc.Fields[k] + "\n"
-       }
-       return []byte(s), nil
-}
-
-// parse doc fields into a struct according to the document type
-func parseStruct(v reflect.Value, fields map[string]string, seen map[string]bool) (err error) {
-       t := v.Type()
-       n := v.NumField()
-       for i := 0; i < n && err == nil; i++ {
-               ft := t.Field(i)
-               fv := v.Field(i)
-               if ft.Anonymous && fv.Kind() == reflect.Struct {
-                       err = parseStruct(fv, fields, seen)
-                       continue
-               }
-               key := fieldname[ft.Name]
-               if key == "" {
-                       key = ft.Name
-               }
-               s, ok := fields[key]
-               if !ok {
-                       if fv.Kind() == reflect.Ptr {
-                               // missing optional key: leave the pointer as nil
-                               continue
-                       }
-                       return fmt.Errorf("ParseStruct: field %s of %s is missing\n", key, t.Name())
-               }
-               seen[key] = true
-               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
-                       val, err = parseId(s)
-                       fv.SetString(val)
-               case "text":
-                       var val string
-                       val, err = parseString(s)
-                       fv.SetString(val)
-               case "int":
-                       var val int64
-                       val, err = strconv.Atoi64(s)
-                       fv.SetInt(val)
-               case "date":
-                       var val int64
-                       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 {
-                               val[j], err = parseId(id)
-                               if err != nil {
-                                       return
-                               }
-                       }
-                       fv.Set(reflect.ValueOf(val))
-               default:
-                       panic("bad field type " + key + " " + fieldtype[key])
-               }
-       }
-       return
-}
-
-func ParseStruct(doc *Document) (iv interface{}, err error) {
-       switch doc.Type {
-       case "Draft":
-               iv = new(Draft)
-       case "Notice":
-               iv = new(Notice)
-       case "DebitCert":
-               iv = new(DebitCert)
-       case "CreditCert":
-               iv = new(CreditCert)
-       case "BounceCert":
-               iv = new(BounceCert)
-       default:
-               err = fmt.Errorf("ParseStruct: unkown doc type: %s", doc.Type)
-               return
-       }
-       seen := make(map[string]bool)
-       err = parseStruct(reflect.ValueOf(iv).Elem(), doc.Fields, seen)
-       if err != nil {
-               return
-       }
-       if len(doc.Fields) != len(seen) {
-               for f := range doc.Fields {
-                       if !seen[f] {
-                               err = fmt.Errorf("ParseStruct: unknown field %s in %s", f, doc.Type)
-                               return
-                       }
-               }
-       }
-       return
-}
-
-// turn a struct into a document
-func formatStruct(v reflect.Value, doc *Document) (err error) {
-       t := v.Type()
-       n := v.NumField()
-       for i := 0; i < n; i++ {
-               ft := t.Field(i)
-               fv := v.Field(i)
-               if ft.Anonymous && fv.Kind() == reflect.Struct {
-                       err = formatStruct(fv, doc)
-                       if err != nil {
-                               return
-                       }
-                       continue
-               }
-               key := fieldname[ft.Name]
-               if key == "" {
-                       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())
-               case "text":
-                       val = formatString(fv.String())
-               case "int":
-                       val = strconv.Itoa64(fv.Int())
-               case "date":
-                       val = formatDate(fv.Int())
-               case "ids":
-                       k := fv.Len()
-                       for j := 0; j < k; j++ {
-                               if j > 0 {
-                                       val += "\n "
-                               }
-                               val += formatId(fv.Index(j).String())
-                       }
-               default:
-                       panic("bad field type " + key + " " + fieldtype[key])
-               }
-       setval:
-               doc.Fields[key] = val
-               doc.Order = append(doc.Order, key)
-       }
-       return
-}
-
-// turn a struct into a document
-func FormatStruct(iv interface{}) (doc *Document, err error) {
-       v := reflect.ValueOf(iv)
-       if v.Kind() != reflect.Ptr || v.IsNil() || v.Elem().Kind() != reflect.Struct {
-               panic("input is not a pointer to struct")
-       }
-       doc = new(Document)
-       doc.Type = v.Elem().Type().Name()
-       doc.Fields = make(map[string]string)
-       err = formatStruct(v.Elem(), doc)
-       return
-}
-
-func ParseFields(s []byte) (fields map[string]string, rest []byte, err error) {
-       rest = s
-       fields = make(map[string]string)
-       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 last field is consumed
-               if len(line) == 0 {
-                       break
-               }
-               if line[0] == ' ' && key != "" {
-                       // "Key: v1\n v2\n" is equivalent to "Key: v1 v2\n"
-                       fields[key] += string(line)
-                       continue
-               }
-               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 ':'")
-                       return
-               }
-               key = string(line[:i])
-               if _, ok := fields[key]; ok {
-                       err = fmt.Errorf("ParseFields: repeated fields are not allowed")
-                       return
-               }
-               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
-}
-
-// TODO: limit errors
-
-func parseId(s string) (string, error) {
-       // check if hex decodable
-       // TODO: length check
-       dst := make([]byte, len(s)/2)
-       _, err := hex.Decode(dst, []byte(s))
-       return s, err
-}
-
-func formatId(s string) string {
-       return s
-}
-
-func parseString(s string) (string, error) {
-       if len(s) > MaxValueLength {
-               return "", fmt.Errorf("parseString: length limit is exceeded")
-       }
-       return s, nil
-}
-
-func formatString(s string) string {
-       return s
-}
-
-func parseDate(s string) (int64, error) {
-       // TODO: fractional seconds?
-       t, err := time.Parse(time.RFC3339, s)
-       if err != nil {
-               return 0, err
-       }
-       return t.Seconds(), nil
-}
-
-func formatDate(i int64) string {
-       return time.SecondsToUTC(i).Format(time.RFC3339)
-}
-
-func getLine(data []byte) (line, rest []byte) {
-       i := bytes.IndexByte(data, '\n')
-       j := i + 1
-       if i < 0 {
-               i = len(data)
-               j = i
-       } else if i > 0 && data[i-1] == '\r' {
-               i--
-       }
-       return data[:i], data[j:]
-}