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