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