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