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