cleanups
[epoint] / document / document.go
1 // Package document implements epoint document parsing and creation.
2 //
3 // An epoint document is an OpenPGP (RFC 4880) clear signed
4 // utf-8 text of key-value pairs.
5 // The body contains a content-type MIME header so the document
6 // can be used in OpenPGP/MIME (RFC 3156) emails.
7 // The format of the key-value pairs are similar to MIME header
8 // fields: keys and values are separated by ": ", repeated keys
9 // are not allowed, long values can be split before a space.
10 //
11 // Example:
12 //
13 // -----BEGIN PGP SIGNED MESSAGE-----
14 // Hash: SHA1
15 //
16 // Content-Type: text/plain.epoint.type; charset=utf-8
17 //
18 // Key: Value1
19 // Another-Key: Value2
20 // Last-Key: Long
21 //  value that spans
22 //  multiple lines
23 // -----BEGIN PGP SIGNATURE-----
24 //
25 // pgp signature
26 // -----END PGP SIGNATURE-----
27 package document
28
29 // TODO: error wrapper (so reporting to user or creating bounce cert is simple)
30 // TODO: optional fields: exact semantics ("" vs "-" vs nil)
31 // TODO: trailing space handling in ParseFields
32 // TODO: fields of notice (last notice, serial, failure notice,..)
33 // TODO: limits and cert type specific input validation
34 // TODO: fix Cert mess
35 // TODO: nonce is id, id is even number of hex digits
36 // TODO: denom, issuer from key (key representation: armor?)
37
38 import (
39         "bytes"
40         "crypto"
41         "crypto/openpgp"
42         "crypto/openpgp/armor"
43         "crypto/openpgp/packet"
44         "crypto/sha1"
45         "encoding/hex"
46         "fmt"
47         "reflect"
48         "strconv"
49         "strings"
50         "time"
51 )
52
53 // limits
54 const (
55         MaxFields             = 20
56         MaxLineLength         = 160  // 1 sha512 + 1 key (without \n)
57         MaxValueLength        = 1300 // 20 sha256 space separated (without \n)
58         MaxNonceLength        = 20
59         MaxDenominationLength = 100
60 )
61
62 const ClearSignedHeader = "-----BEGIN PGP SIGNED MESSAGE-----"
63
64 // MIME type for epoint documents, see RFC 4288
65 var ContentType = map[string]string{
66         "Draft":      "text/vnd.epoint.draft; charset=utf-8",
67         "Notice":     "text/vnd.epoint.notice; charset=utf-8",
68         "DebitCert":  "text/vnd.epoint.debit; charset=utf-8",
69         "CreditCert": "text/vnd.epoint.credit; charset=utf-8",
70         "BounceCert": "text/vnd.epoint.bounce; charset=utf-8",
71 }
72
73 // OpenPGP signed cleartext document representation
74 type Signed struct {
75         // Sign and CleanSigned sets Hash for FormatSigned
76         // TODO: CreationDate
77         Hash string
78         // Signed text (no dash escape, no trailing space, \n new lines)
79         Body []byte
80         // Armored detached text signature of the Body
81         Signature []byte
82 }
83
84 // parsed epoint document
85 type Document struct {
86         Type   string
87         Fields map[string]string
88         Order  []string
89 }
90
91 var fieldtype = map[string]string{
92         "Amount":             "int",
93         "Authorized-By":      "id",
94         "Balance":            "int",
95         "Beneficiary":        "id",
96         "Date":               "date",
97         "Debit-Cert":         "id",
98         "Denomination":       "text",
99         "Difference":         "int",
100         "Draft":              "id",
101         "Drawer":             "id",
102         "Expiry-Date":        "date",
103         "Holder":             "id",
104         "Issuer":             "id",
105         "Last-Cert":          "id",
106         "Last-Credit-Serial": "int",
107         "Last-Debit-Serial":  "int",
108         "Maturity-Date":      "date",
109         "Nonce":              "id",
110         "Notes":              "text",
111         "References":         "ids",
112         "Serial":             "int",
113 }
114
115 var fieldname = map[string]string{
116         "AuthorizedBy":     "Authorized-By",
117         "DebitCert":        "Debit-Cert",
118         "ExpiryDate":       "Expiry-Date",
119         "LastCert":         "Last-Cert",
120         "LastCreditSerial": "Last-Credit-Serial",
121         "LastDebitSerial":  "Last-Debit-Serial",
122         "MaturityDate":     "Maturity-Date",
123 }
124
125 type Draft struct {
126         Drawer       string
127         Beneficiary  string
128         Amount       int64
129         Denomination string
130         Issuer       string
131         AuthorizedBy string
132         MaturityDate *int64 // optional
133         ExpiryDate   *int64 // optional
134         Nonce        string
135         Notes        *string // optional
136 }
137
138 type Notice struct {
139         Date         int64
140         AuthorizedBy string
141         Notes        *string  // optional
142         References   []string // may be empty (startup notice)
143 }
144
145 type Cert struct {
146         IsDebit          bool
147         Holder           string
148         Serial           int64
149         Balance          int64
150         Denomination     string
151         Issuer           string
152         Date             int64
153         Difference       int64
154         Draft            string
155         Beneficiary      *string // only in debit cert
156         Drawer           *string // only in credit cert
157         DebitCert        *string // only in credit cert
158         AuthorizedBy     string
159         Notes            *string // optional
160         LastDebitSerial  int64   // 0 if none
161         LastCreditSerial int64   // 0 if none
162         LastCert         *string // nil if serial == 1
163         References       []string
164 }
165
166 type DebitCert struct {
167         Holder           string
168         Serial           int64
169         Balance          int64
170         Denomination     string
171         Issuer           string
172         Date             int64
173         Difference       int64
174         Draft            string
175         Beneficiary      string
176         AuthorizedBy     string
177         Notes            *string // optional
178         LastDebitSerial  int64   // 0 if none
179         LastCreditSerial int64   // 0 if none
180         LastCert         *string // nil if serial == 1
181         References       []string
182 }
183
184 type CreditCert struct {
185         Holder           string
186         Serial           int64
187         Balance          int64
188         Denomination     string
189         Issuer           string
190         Date             int64
191         Difference       int64
192         Draft            string
193         Drawer           string
194         DebitCert        string
195         AuthorizedBy     string
196         Notes            *string // optional
197         LastDebitSerial  int64   // 0 if none
198         LastCreditSerial int64   // 0 if none
199         LastCert         *string // ? if serial == 1
200         References       []string
201 }
202
203 type BounceCert struct {
204         Drawer       string
205         Draft        string
206         LastCert     *string // optional
207         Balance      int64   // 0 if none
208         Date         int64
209         AuthorizedBy string
210         Notes        *string // optional
211         References   []string
212 }
213
214 func ToCert(v interface{}) (cert *Cert, err error) {
215         cert = new(Cert)
216         switch x := v.(type) {
217         case *DebitCert:
218                 cert.IsDebit = true
219                 cert.Beneficiary = &x.Beneficiary
220
221                 cert.Holder = x.Holder
222                 cert.Serial = x.Serial
223                 cert.Balance = x.Balance
224                 cert.Denomination = x.Denomination
225                 cert.Issuer = x.Issuer
226                 cert.Date = x.Date
227                 cert.Difference = x.Difference
228                 cert.Draft = x.Draft
229                 cert.AuthorizedBy = x.AuthorizedBy
230                 cert.Notes = x.Notes
231                 cert.LastDebitSerial = x.LastDebitSerial
232                 cert.LastCreditSerial = x.LastCreditSerial
233                 cert.LastCert = x.LastCert
234                 cert.References = x.References
235
236         case *CreditCert:
237                 cert.IsDebit = false
238                 cert.Drawer = &x.Drawer
239                 cert.DebitCert = &x.DebitCert
240
241                 cert.Holder = x.Holder
242                 cert.Serial = x.Serial
243                 cert.Balance = x.Balance
244                 cert.Denomination = x.Denomination
245                 cert.Issuer = x.Issuer
246                 cert.Date = x.Date
247                 cert.Difference = x.Difference
248                 cert.Draft = x.Draft
249                 cert.AuthorizedBy = x.AuthorizedBy
250                 cert.Notes = x.Notes
251                 cert.LastDebitSerial = x.LastDebitSerial
252                 cert.LastCreditSerial = x.LastCreditSerial
253                 cert.LastCert = x.LastCert
254                 cert.References = x.References
255         default:
256                 err = fmt.Errorf("ToCert: only debit or credit document can be converted to cert")
257         }
258         return
259 }
260
261 // sha1 sum of the (cleaned) document body as uppercase hex string
262 func Id(c *Signed) string {
263         h := sha1.New()
264         h.Write(c.Body)
265         return fmt.Sprintf("%040X", h.Sum())
266 }
267
268 // parse an epoint document without checking the signature and format details
269 func Parse(s []byte) (iv interface{}, c *Signed, err error) {
270         c, err = ParseSigned(s)
271         if err != nil {
272                 return
273         }
274         doc, err := ParseDocument(c.Body)
275         if err != nil {
276                 return
277         }
278         iv, err = ParseStruct(doc)
279         return
280 }
281
282 // format and sign an epoint document
283 func Format(iv interface{}, key *openpgp.Entity) (s []byte, c *Signed, err error) {
284         doc, err := FormatStruct(iv)
285         if err != nil {
286                 return
287         }
288         body, err := FormatDocument(doc)
289         if err != nil {
290                 return
291         }
292         c, err = Sign(body, key)
293         if err != nil {
294                 return
295         }
296         s, err = FormatSigned(c)
297         return
298 }
299
300 // verify an epoint document, return the cleaned version as well
301 func Verify(c *Signed, key openpgp.KeyRing) (err error) {
302         msg := bytes.NewBuffer(c.Body)
303         sig := bytes.NewBuffer(c.Signature)
304         // TODO: verify signature
305         _, _ = msg, sig
306         //      _, err = openpgp.CheckArmoredDetachedSignature(key, msg, sig)
307         return
308 }
309
310 // sign body with given secret key
311 func Sign(body []byte, key *openpgp.Entity) (c *Signed, err error) {
312         c = new(Signed)
313         c.Hash = "SHA256"
314         c.Body = body
315         w := new(bytes.Buffer)
316         err = openpgp.ArmoredDetachSignText(w, key, bytes.NewBuffer(c.Body))
317         if err != nil {
318                 return
319         }
320         // close armored document with a \n
321         _, _ = w.Write([]byte{'\n'})
322         c.Signature = w.Bytes()
323         return
324 }
325
326 // split a clear signed document into body and armored signature
327 func ParseSigned(s []byte) (c *Signed, err error) {
328         // look for clear signed header
329         for !bytes.HasPrefix(s, []byte(ClearSignedHeader)) {
330                 _, s = getLine(s)
331                 if len(s) == 0 {
332                         err = fmt.Errorf("ParseSigned: clear signed header is missing")
333                         return
334                 }
335         }
336         s = s[len(ClearSignedHeader):]
337         // end of line after the header
338         empty, s := getLine(s)
339         if len(empty) != 0 {
340                 err = fmt.Errorf("ParseSigned: bad clear signed header")
341                 return
342         }
343         // skip all hash headers, section 7.
344         for bytes.HasPrefix(s, []byte("Hash: ")) {
345                 _, s = getLine(s)
346         }
347         // skip empty line
348         empty, s = getLine(s)
349         if len(empty) != 0 {
350                 err = fmt.Errorf("ParseSigned: expected an empty line after armor headers")
351                 return
352         }
353         lines := [][]byte{}
354         for !bytes.HasPrefix(s, []byte("-----BEGIN")) {
355                 var line []byte
356                 line, s = getLine(s)
357                 // dash unescape, section 7.1.
358                 if bytes.HasPrefix(line, []byte("- ")) {
359                         line = line[2:]
360                 }
361                 // empty values are not supported: "Key: \n"
362                 lines = append(lines, bytes.TrimRight(line, " \t"))
363         }
364         c = new(Signed)
365         // last line is not closed by \n
366         c.Body = bytes.Join(lines, []byte("\n"))
367         // signature is just the rest of the input data
368         c.Signature = s
369         return
370 }
371
372 // clean up, check and reencode signature
373 // used on drafts before calculating the signed document hash
374 func CleanSigned(c *Signed) (err error) {
375         b, err := armor.Decode(bytes.NewBuffer(c.Signature))
376         if err != nil {
377                 return
378         }
379         if b.Type != openpgp.SignatureType {
380                 err = fmt.Errorf("CleanSigned: invalid armored signature type")
381                 return
382         }
383         p, err := packet.Read(b.Body)
384         if err != nil {
385                 return
386         }
387         sig, ok := p.(*packet.Signature)
388         if !ok {
389                 err = fmt.Errorf("CleanSigned: invalid signature packet")
390                 return
391         }
392         // section 5.2.3
393         if sig.SigType != packet.SigTypeText {
394                 err = fmt.Errorf("CleanSigned: expected text signature")
395                 return
396         }
397         switch sig.Hash {
398         case crypto.SHA1:
399                 c.Hash = "SHA1"
400         case crypto.SHA256:
401                 c.Hash = "SHA256"
402         default:
403                 err = fmt.Errorf("CleanSigned: expected SHA1 or SHA256 signature hash")
404                 return
405         }
406         // TODO: check CreationTime and other subpackets
407         if sig.SigLifetimeSecs != nil && *sig.SigLifetimeSecs != 0 {
408                 err = fmt.Errorf("CleanSigned: signature must not expire")
409                 return
410         }
411         out := new(bytes.Buffer)
412         w, err := armor.Encode(out, openpgp.SignatureType, nil)
413         if err != nil {
414                 return
415         }
416         err = sig.Serialize(w)
417         if err != nil {
418                 return
419         }
420         err = w.Close()
421         if err != nil {
422                 return
423         }
424         c.Signature = out.Bytes()
425         return
426 }
427
428 // create clear signed document
429 func FormatSigned(c *Signed) (data []byte, err error) {
430         s := ClearSignedHeader + "\n"
431         if c.Hash != "" {
432                 s += "Hash: " + c.Hash + "\n"
433         }
434         s += "\n"
435         s += string(c.Body)
436         s += "\n"
437         s += string(c.Signature)
438         data = []byte(s)
439         return
440 }
441
442 // parse type and fields of a document body
443 func ParseDocument(body []byte) (doc *Document, err error) {
444         // parse content type header first
445         fields, s, err := ParseFields(body)
446         if err != nil {
447                 return
448         }
449         ctype, ok := fields["Content-Type"]
450         if len(fields) != 1 || !ok {
451                 return nil, fmt.Errorf("ParseBody: expected a single Content-Type header field")
452         }
453         doc = new(Document)
454         for k, v := range ContentType {
455                 if ctype == v {
456                         doc.Type = k
457                         break
458                 }
459         }
460         if doc.Type == "" {
461                 return nil, fmt.Errorf("ParseBody: unknown Content-Type: %s", ctype)
462         }
463         // TODO: doc.Order
464         doc.Fields, s, err = ParseFields(s)
465         if err == nil && len(s) > 0 {
466                 err = fmt.Errorf("ParseBody: extra data after fields: %q", s)
467         }
468         return
469 }
470
471 // create document body
472 func FormatDocument(doc *Document) (body []byte, err error) {
473         ctype, ok := ContentType[doc.Type]
474         if !ok {
475                 err = fmt.Errorf("FormatDocument: unknown document type: %s", doc.Type)
476                 return
477         }
478         s := "Content-Type: " + ctype + "\n\n"
479         for _, k := range doc.Order {
480                 s += k + ": " + doc.Fields[k] + "\n"
481         }
482         return []byte(s), nil
483 }
484
485 // parse doc fields into a struct according to the document type
486 func ParseStruct(doc *Document) (iv interface{}, err error) {
487         switch doc.Type {
488         case "Draft":
489                 iv = new(Draft)
490         case "Notice":
491                 iv = new(Notice)
492         case "DebitCert":
493                 iv = new(DebitCert)
494         case "CreditCert":
495                 iv = new(CreditCert)
496         case "BounceCert":
497                 iv = new(BounceCert)
498         default:
499                 err = fmt.Errorf("ParseStruct: unkown doc type: %s", doc.Type)
500                 return
501         }
502         seen := make(map[string]bool)
503         v := reflect.ValueOf(iv).Elem()
504         t := v.Type()
505         n := v.NumField()
506         nokey := 0
507         for i := 0; i < n; i++ {
508                 ft := t.Field(i)
509                 fv := v.Field(i)
510                 key := fieldname[ft.Name]
511                 if key == "" {
512                         key = ft.Name
513                 }
514                 seen[key] = true
515                 s, ok := doc.Fields[key]
516                 if !ok {
517                         if fv.Kind() == reflect.Ptr {
518                                 // missing optional key: leave the pointer as nil
519                                 nokey++
520                                 continue
521                         }
522                         return nil, fmt.Errorf("ParseStruct: field %s of %s is missing\n", key, t.Name())
523                 }
524                 if fv.Kind() == reflect.Ptr {
525                         if s == "" || s == "-" {
526                                 // TODO
527                                 // empty optional key: same as missing
528                                 continue
529                         }
530                         fv.Set(reflect.New(fv.Type().Elem()))
531                         fv = fv.Elem()
532                 }
533                 switch fieldtype[key] {
534                 case "id":
535                         var val string
536                         val, err = parseId(s)
537                         fv.SetString(val)
538                 case "text":
539                         var val string
540                         val, err = parseString(s)
541                         fv.SetString(val)
542                 case "int":
543                         var val int64
544                         val, err = strconv.Atoi64(s)
545                         fv.SetInt(val)
546                 case "date":
547                         var val int64
548                         val, err = parseDate(s)
549                         fv.SetInt(val)
550                 case "ids":
551                         // TODO: empty slice?
552                         ids := strings.Split(s, " ")
553                         val := make([]string, len(ids))
554                         for j, id := range ids {
555                                 val[j], err = parseId(id)
556                                 if err != nil {
557                                         return
558                                 }
559                         }
560                         fv.Set(reflect.ValueOf(val))
561                 default:
562                         panic("bad field type " + key + " " + fieldtype[key])
563                 }
564                 if err != nil {
565                         return
566                 }
567         }
568         if len(doc.Fields)+nokey != n {
569                 for k := range doc.Fields {
570                         if !seen[k] {
571                                 err = fmt.Errorf("ParseStruct: unknown field %s in %s", k, t.Name())
572                                 return
573                         }
574                 }
575         }
576         return
577 }
578
579 // turn a struct into a document
580 func FormatStruct(iv interface{}) (doc *Document, err error) {
581         v := reflect.ValueOf(iv)
582         if v.Kind() != reflect.Ptr || v.IsNil() || v.Elem().Kind() != reflect.Struct {
583                 panic("input is not a pointer to struct")
584         }
585         v = v.Elem()
586         t := v.Type()
587         n := v.NumField()
588         doc = new(Document)
589         doc.Type = t.Name()
590         doc.Fields = make(map[string]string)
591         for i := 0; i < n; i++ {
592                 ft := t.Field(i)
593                 fv := v.Field(i)
594                 key := fieldname[ft.Name]
595                 if key == "" {
596                         key = ft.Name
597                 }
598                 val := ""
599                 if fv.Kind() == reflect.Ptr {
600                         if fv.IsNil() {
601                                 // keep empty optional fields but mark them
602                                 val = "-"
603                                 goto setval
604                         }
605                         fv = fv.Elem()
606                 }
607                 switch fieldtype[key] {
608                 case "id":
609                         val = formatId(fv.String())
610                 case "text":
611                         val = formatString(fv.String())
612                 case "int":
613                         val = strconv.Itoa64(fv.Int())
614                 case "date":
615                         val = formatDate(fv.Int())
616                 case "ids":
617                         k := fv.Len()
618                         for j := 0; j < k; j++ {
619                                 if j > 0 {
620                                         val += "\n "
621                                 }
622                                 val += formatId(fv.Index(j).String())
623                         }
624                 default:
625                         panic("bad field type " + key + " " + fieldtype[key])
626                 }
627         setval:
628                 doc.Fields[key] = val
629                 doc.Order = append(doc.Order, key)
630         }
631         return
632 }
633
634 func ParseFields(s []byte) (fields map[string]string, rest []byte, err error) {
635         rest = s
636         fields = make(map[string]string)
637         key := ""
638         // \n is optional after the last field and an extra \n is allowed as well
639         for len(rest) > 0 {
640                 var line []byte
641                 line, rest = getLine(rest)
642                 // empty line after the last field is consumed
643                 if len(line) == 0 {
644                         break
645                 }
646                 if line[0] == ' ' && key != "" {
647                         // "Key: v1\n v2\n" is equivalent to "Key: v1 v2\n"
648                         fields[key] += string(line)
649                         continue
650                 }
651                 if line[0] < 'A' || line[0] > 'Z' {
652                         err = fmt.Errorf("ParseFields: field name must start with an upper-case ascii letter")
653                         return
654                 }
655                 i := bytes.IndexByte(line, ':')
656                 if i < 0 {
657                         err = fmt.Errorf("ParseFields: missing ':'")
658                         return
659                 }
660                 key = string(line[:i])
661                 if _, ok := fields[key]; ok {
662                         err = fmt.Errorf("ParseFields: repeated fields are not allowed")
663                         return
664                 }
665                 fields[key] = string(line[i+1:])
666         }
667         for key, v := range fields {
668                 // either a single space follows ':' or the value is empty
669                 // good: "Key:\n", "Key:\n value\n", "Key: value\n", "Key: v1\n v2\n"
670                 // bad: "Key:value\n", "Key: \nvalue\n"
671                 // bad but not checked here: "Key: \n", "Key: value \n", "Key:\n \n value\n"
672                 if len(v) == 0 {
673                         continue
674                 }
675                 if v[0] != ' ' {
676                         err = fmt.Errorf("ParseFields: ':' is not followed by ' '")
677                         return
678                 }
679                 fields[key] = v[1:]
680         }
681         return
682 }
683
684 // TODO: limit errors
685
686 func parseId(s string) (string, error) {
687         // check if hex decodable
688         // TODO: length check
689         dst := make([]byte, len(s)/2)
690         _, err := hex.Decode(dst, []byte(s))
691         return s, err
692 }
693
694 func formatId(s string) string {
695         return s
696 }
697
698 func parseString(s string) (string, error) {
699         if len(s) > MaxValueLength {
700                 return "", fmt.Errorf("parseString: length limit is exceeded")
701         }
702         return s, nil
703 }
704
705 func formatString(s string) string {
706         return s
707 }
708
709 func parseDate(s string) (int64, error) {
710         // TODO: fractional seconds?
711         t, err := time.Parse(time.RFC3339, s)
712         if err != nil {
713                 return 0, err
714         }
715         return t.Seconds(), nil
716 }
717
718 func formatDate(i int64) string {
719         return time.SecondsToUTC(i).Format(time.RFC3339)
720 }
721
722 func getLine(data []byte) (line, rest []byte) {
723         i := bytes.IndexByte(data, '\n')
724         j := i + 1
725         if i < 0 {
726                 i = len(data)
727                 j = i
728         } else if i > 0 && data[i-1] == '\r' {
729                 i--
730         }
731         return data[:i], data[j:]
732 }