extend cert with server and notes
[epoint] / document / document.go
1 package document
2
3 // An epoint document is a clear signed utf-8 text of key-value pairs
4 // according to OpenPGP RFC 4880. The body contains a content-type
5 // MIME header so it can be used in OpenPGP/MIME RFC 3156 emails with
6 // the signature detached. The format of the key-value pairs are
7 // similar to MIME header fields.
8 //
9 // Example:
10 //
11 // -----BEGIN PGP SIGNED MESSAGE-----
12 // Hash: SHA1
13 //
14 // Content-Type: text/plain.epoint.cert; charset=utf-8
15 //
16 // Key: Value1
17 // Another-Key: Value2
18 // -----BEGIN PGP SIGNATURE-----
19 // pgp signature
20 // -----END PGP SIGNATURE-----
21
22 import (
23         "bytes"
24         "encoding/hex"
25         "fmt"
26         "reflect"
27         "strconv"
28         "strings"
29         "time"
30 )
31
32 const ClearSignedHeader = "-----BEGIN PGP SIGNED MESSAGE-----\n"
33
34 // (non-standard) MIME subtype for epoint documents, see RFC 2045 and RFC 2046
35 var ContentType = map[string]string{
36         "cert":  "text/plain.epoint.cert; charset=utf-8",
37         "draft": "text/plain.epoint.draft; charset=utf-8",
38 }
39
40 // OpenPGP signed cleartext document representation
41 type ClearSigned struct {
42         Hash string
43         // Signed text (no dash escape, no trailing space)
44         Body []byte
45         // Armored detached text signature of the Body
46         ArmoredSignature []byte
47 }
48
49 type Field struct {
50         Key   string
51         Value string
52 }
53
54 // Draft document represents an obligation transfer order
55 type Draft struct {
56         Drawer       string `marshal:"id"` // ID of the payer (signer of the document)
57         Beneficiary  string `marshal:"id"` // ID of the payee
58         Amount       int64  // amount transfered
59         Denomination string
60         IssueDate    int64 `marshal:"date" key:"Issue-Date"`
61         // Draft is bounced before this date
62         MaturityDate int64  `marshal:"date" key:"Maturity-Date"`
63         Notes        string // Arbitrary text notes of the drawer
64         Nonce        string // unique number
65         Server       string `marshal:"id"` // ID of the server
66         Drawee       string `marshal:"id"` // ID of the obligation issuer
67         // useful if more strict date of issue information is needed
68         //References []string
69 }
70
71 // Obligation certificate after a transfer
72 // References previous certificate (if any)
73 // and the transfer related other documents
74 type Cert struct {
75         Holder           string `marshal:"id"` // ID of the creditor
76         Serial           uint32 // serial number, number of certs of the holder
77         Date             int64  `marshal:"date"` // date of issue
78         Balance          int64  // current obligation value
79         Denomination     string
80         Server           string   `marshal:"id"`                 // ID of the server
81         Issuer           string   `marshal:"id"`                 // ID of the obligation issuer (drawee?)
82         LastDebitSerial  uint32   `key:"Last-Debit-Serial"`      // serial of the last draft cert or 0
83         LastCreditSerial uint32   `key:"Last-Credit-Serial"`     // serial of the last credit cert or 0
84         LastCert         string   `marshal:"id" key:"Last-Cert"` // ID of the previous cert if any
85         Difference       int64    // difference from previous balance
86         Draft            string   `marshal:"id"`                   // draft ID related to the transfer
87         Drawer           string   `marshal:"id"`                   // ID of the drawer in the transaction
88         DrawerSerial     uint32   `key:"Drawer-Serial"`            // serial of the drawer's related debit cert
89         DrawerCert       string   `marshal:"id" key:"Drawer-Cert"` // ID of the drawer's related debit cert
90         Notes            string   // Arbitrary text notes of the server (signer)
91         References       []string `marshal:"idlist"` // cert IDs for timestamping the system
92 }
93
94 func DecodeClearSigned(s []byte) (c *ClearSigned, err error) {
95         hash, body, sig := split(s)
96         if len(sig) == 0 {
97                 err = fmt.Errorf("DecodeClearSigned could parse the signed document")
98                 return
99         }
100         c = &ClearSigned{string(hash), trimspace(dashunesc(body)), sig}
101         return
102 }
103
104 func EncodeClearSigned(c *ClearSigned) (data []byte, err error) {
105         s := ClearSignedHeader
106         if c.Hash != "" {
107                 s += "Hash: " + c.Hash + "\n"
108         }
109         // TODO: check if space was trimmed from body before signature
110         s += "\n"
111         s += string(dashesc(c.Body))
112         s += "\n"
113         s += string(c.ArmoredSignature)
114         data = []byte(s)
115         return
116 }
117
118 func ParseFields(s []byte) (fields []Field, rest []byte, err error) {
119         rest = s
120         for len(rest) > 0 {
121                 var line []byte
122                 line, rest = getLine(rest)
123                 // empty line after the parsed fields (consumed)
124                 if len(line) == 0 {
125                         break
126                 }
127                 i := bytes.Index(line, []byte(": "))
128                 if i < 0 {
129                         err = fmt.Errorf("ParseFields: missing ': '\n")
130                         break
131                 }
132                 fields = append(fields, Field{string(line[:i]), string(line[i+2:])})
133         }
134         return
135 }
136
137 func ParseBody(s []byte) (doctype string, fields []Field, err error) {
138         // parse content type header first
139         fs, s, err := ParseFields(s)
140         if err != nil {
141                 return
142         }
143         if len(fs) != 1 || fs[0].Key != "Content-Type" {
144                 return "", nil, fmt.Errorf("ParseBody: single Content-Type header was expected\n")
145         }
146         ctype := fs[0].Value
147         for k, v := range ContentType {
148                 if ctype == v {
149                         doctype = k
150                         break
151                 }
152         }
153         if doctype == "" {
154                 return "", nil, fmt.Errorf("ParseBody: unknown Content-Type: %s", ctype)
155         }
156         fields, s, err = ParseFields(s)
157         if err == nil && len(s) > 0 {
158                 err = fmt.Errorf("ParseBody: extra data after fields: %q", s)
159         }
160         return
161 }
162
163 func parse(s []byte, doctype string) (v interface{}, err error) {
164         t, fields, err := ParseBody(s)
165         if err != nil {
166                 return
167         }
168         if doctype != t && doctype != "" {
169                 err = fmt.Errorf("parse: expected doctype %s; got %s", doctype, t)
170                 return
171         }
172         switch t {
173         case "draft":
174                 v = new(Draft)
175         case "cert":
176                 v = new(Cert)
177         default:
178                 err = fmt.Errorf("parse: unkown doc type: %s", t)
179                 return
180         }
181         err = unmarshal(fields, v)
182         return
183 }
184
185 // TODO: limit errors
186 func render(v interface{}) ([]byte, error) {
187         doctype := ""
188         switch v.(type) {
189         case *Draft:
190                 doctype = "draft"
191         case *Cert:
192                 doctype = "cert"
193         default:
194                 panic("reder: unknown type")
195         }
196         s := "Content-Type: " + ContentType[doctype] + "\n\n"
197         fields := marshal(v)
198         for _, f := range fields {
199                 s += f.Key + ": " + f.Value + "\n"
200         }
201         return []byte(s), nil
202 }
203
204 func ParseDraft(s []byte) (draft *Draft, err error) {
205         v, err := parse(s, "draft")
206         if err != nil {
207                 return
208         }
209         draft = v.(*Draft)
210         return
211 }
212
213 func RenderDraft(draft *Draft) ([]byte, error) {
214         return render(draft)
215 }
216
217 func ParseCert(s []byte) (cert *Cert, err error) {
218         v, err := parse(s, "cert")
219         if err != nil {
220                 return nil, err
221         }
222         cert = v.(*Cert)
223         return
224 }
225
226 func RenderCert(cert *Cert) ([]byte, error) {
227         return render(cert)
228 }
229
230 func formatId(s string) string {
231         return fmt.Sprintf("%040X", s)
232 }
233
234 func parseId(s string) (string, error) {
235         dst := make([]byte, 20)
236         if len(s) != 40 {
237                 return "", fmt.Errorf("parseId: expected 40 characters; got %d", len(s))
238         }
239         _, err := hex.Decode(dst, []byte(s))
240         return string(dst), err
241 }
242
243 func quoteValue(s string) string {
244         return s
245 }
246
247 func unquoteValue(s string) (string, error) {
248         return s, nil
249 }
250
251 func formatDate(i int64) string {
252         return time.SecondsToUTC(i).Format(time.RFC3339)
253 }
254
255 func parseDate(s string) (int64, error) {
256         t, err := time.Parse(time.RFC3339, s)
257         if err != nil {
258                 return 0, err
259         }
260         return t.Seconds(), nil
261 }
262
263 func marshal(iv interface{}) []Field {
264         v := reflect.ValueOf(iv)
265         if v.Kind() != reflect.Ptr || v.IsNil() || v.Elem().Kind() != reflect.Struct {
266                 panic("unmarshal: input is not a pointer to struct")
267         }
268         v = v.Elem()
269         t := v.Type()
270         n := v.NumField()
271         fields := []Field{}
272         for i := 0; i < n; i++ {
273                 ft := t.Field(i)
274                 fv := v.Field(i)
275                 m := ft.Tag.Get("marshal")
276                 k := ft.Tag.Get("key")
277                 if k == "" {
278                         k = ft.Name
279                 }
280                 val := ""
281                 switch fv.Kind() {
282                 case reflect.String:
283                         switch m {
284                         case "id":
285                                 val = formatId(fv.String())
286                         case "":
287                                 val = quoteValue(fv.String())
288                         default:
289                                 panic("bad string field tag")
290                         }
291                 case reflect.Int, reflect.Int32, reflect.Int64:
292                         switch m {
293                         case "date":
294                                 val = formatDate(fv.Int())
295                         case "":
296                                 val = strconv.Itoa64(fv.Int())
297                         default:
298                                 panic("bad int field tag")
299                         }
300                 case reflect.Uint, reflect.Uint32, reflect.Uint64:
301                         switch m {
302                         case "":
303                                 val = strconv.Uitoa64(fv.Uint())
304                         default:
305                                 panic("bad uint field tag")
306                         }
307                 case reflect.Slice:
308                         switch m {
309                         case "idlist":
310                                 if fv.Type().Elem().Kind() != reflect.String {
311                                         panic("only string slice is supported")
312                                 }
313                                 k := fv.Len()
314                                 for j := 0; j < k; j++ {
315                                         if j > 0 {
316                                                 val += " "
317                                         }
318                                         val += formatId(fv.Index(j).String())
319                                 }
320                         default:
321                                 panic("bad slice field tag")
322                         }
323                 default:
324                         panic("bad field type")
325                 }
326                 fields = append(fields, Field{k, val})
327         }
328         return fields
329 }
330
331 func unmarshal(fields []Field, iv interface{}) error {
332         v := reflect.ValueOf(iv)
333         if v.Kind() != reflect.Ptr || v.IsNil() || v.Elem().Kind() != reflect.Struct {
334                 panic("unmarshal: input is not a pointer to struct")
335         }
336         v = v.Elem()
337         t := v.Type()
338         n := v.NumField()
339         if len(fields) != n {
340                 return fmt.Errorf("unmarshal: %s has %d fields, got %d\n", t.Name(), n, len(fields))
341         }
342         for i := 0; i < n; i++ {
343                 ft := t.Field(i)
344                 fv := v.Field(i)
345                 m := ft.Tag.Get("marshal")
346                 k := ft.Tag.Get("key")
347                 if k == "" {
348                         k = ft.Name
349                 }
350                 if fields[i].Key != k {
351                         return fmt.Errorf("unmarshal: field %d of %s (%s) is missing\n", i, t.Name(), k)
352                 }
353                 s := fields[i].Value
354                 var err error
355                 switch fv.Kind() {
356                 case reflect.String:
357                         var val string
358                         switch m {
359                         case "id":
360                                 val, err = parseId(s)
361                         case "":
362                                 val, err = unquoteValue(s)
363                         default:
364                                 panic("bad string field tag")
365                         }
366                         fv.SetString(val)
367                 case reflect.Int, reflect.Int32, reflect.Int64:
368                         var val int64
369                         switch m {
370                         case "date":
371                                 val, err = parseDate(s)
372                         case "":
373                                 val, err = strconv.Atoi64(s)
374                         default:
375                                 panic("bad int field tag")
376                         }
377                         fv.SetInt(val)
378                 case reflect.Uint, reflect.Uint32, reflect.Uint64:
379                         var val uint64
380                         switch m {
381                         case "":
382                                 val, err = strconv.Atoui64(s)
383                         default:
384                                 panic("bad uint field tag")
385                         }
386                         fv.SetUint(val)
387                 case reflect.Slice:
388                         var val []string
389                         switch m {
390                         case "idlist":
391                                 if fv.Type().Elem().Kind() != reflect.String {
392                                         panic("only string slice is supported")
393                                 }
394                                 ids := strings.Split(s, " ")
395                                 val = make([]string, len(ids))
396                                 for j := range val {
397                                         val[j], err = parseId(ids[j])
398                                         if err != nil {
399                                                 return err
400                                         }
401                                 }
402                         default:
403                                 panic("bad slice field tag")
404                         }
405                         fv.Set(reflect.ValueOf(val))
406                 default:
407                         panic("bad field type")
408                 }
409                 if err != nil {
410                         return err
411                 }
412         }
413         return nil
414 }
415
416 func getLine(data []byte) (line, rest []byte) {
417         i := bytes.Index(data, []byte{'\n'})
418         j := i + 1
419         if i < 0 {
420                 i = len(data)
421                 j = i
422         } else if i > 0 && data[i-1] == '\r' {
423                 i--
424         }
425         return data[:i], data[j:]
426 }
427
428 func trimspace(s []byte) []byte {
429         a := bytes.Split(s, []byte("\n"))
430         for i := range a {
431                 a[i] = bytes.TrimRight(a[i], " \t\r")
432         }
433         return bytes.Join(a, []byte("\n"))
434 }
435
436 func dashesc(s []byte) []byte {
437         r := bytes.Replace(s, []byte("\n-"), []byte("\n- -"), -1)
438         if len(r) > 0 && r[0] == '-' {
439                 r = append([]byte("- "), r...)
440         }
441         return r
442 }
443
444 func dashunesc(s []byte) []byte {
445         r := bytes.Replace(s, []byte("\n- "), []byte("\n"), -1)
446         if len(r) >= 2 && r[0] == '-' && r[1] == ' ' {
447                 r = r[2:]
448         }
449         return r
450 }
451
452 // RFC 4880 is unclear about multiple Hash header semantics, section 7. says
453 //  "One or more "Hash" Armor Headers"
454 // then
455 //  "If more than one message digest is used in the signature, the "Hash"
456 //   armor header contains a comma-delimited list of used message digests."
457 // in section 6.2.
458 //  "there is no limit to the length of Armor Headers.  Care should
459 //   be taken that the Armor Headers are short enough to survive
460 //   transport.  One way to do this is to repeat an Armor Header key
461 //   multiple times with different values for each so that no one line is
462 //   overly long."
463 // we accept a single Hash header with a list of hash algorithms for now
464 // but use the one specified by the signature
465
466 func split(s []byte) (hash, body, sig []byte) {
467         if !bytes.HasPrefix(s, []byte(ClearSignedHeader)) {
468                 return
469         }
470         s = s[len(ClearSignedHeader):]
471         // only allow a single Hash: header
472         if bytes.HasPrefix(s, []byte("Hash: ")) {
473                 s = s[len("Hash: "):]
474                 hash, s = getLine(s)
475         }
476         // skip empty line
477         empty, s := getLine(s)
478         if len(empty) != 0 {
479                 return
480         }
481         i := bytes.Index(s, []byte("\n-----BEGIN"))
482         if i < 0 {
483                 return
484         }
485         body, sig = s[:i], s[i+1:]
486         if i > 0 && body[i-1] == '\r' {
487                 body = body[:i-1]
488         }
489         return
490 }