46f41ab07117d9c439efc68aefbe61a43645f360
[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 // TODO: allow visually aligned field values
10 // TODO: handling repeated fields
11 // TODO: representation of a list (references)
12 //
13 // Example:
14 //
15 // -----BEGIN PGP SIGNED MESSAGE-----
16 // Hash: SHA1
17 //
18 // Content-Type: text/plain.epoint.cert; charset=utf-8
19 //
20 // Key: Value1
21 // Another-Key: Value2
22 // -----BEGIN PGP SIGNATURE-----
23 // pgp signature
24 // -----END PGP SIGNATURE-----
25
26 import (
27         "bytes"
28         "strconv"
29 )
30
31 const ClearSignedHeader = "-----BEGIN PGP SIGNED MESSAGE-----\n"
32
33 // Non-standard MIME subtype see RFC 2045 and RFC 2046
34 // TODO: verify that Content-Transfer-Encoding is not needed
35 // TODO: text/epoint.cert would be shorter
36 var ContentType = map[string]string{
37         "cert":  "text/plain.epoint.cert; charset=utf-8",
38         "draft": "text/plain.epoint.draft; charset=utf-8",
39 }
40
41 // OpenPGP signed cleartext document representation
42 type ClearSigned struct {
43         Hash string
44         // Signed text (no dash escape, no trailing space)
45         Body []byte
46         // Armored detached text signature of the Body
47         ArmoredSignature []byte
48 }
49
50 // Draft document represents an obligation transfer order
51 type Draft struct {
52         Drawer      string // ID of the payer (signer of the document)
53         Beneficiary string // ID of the payee
54         Amount      int64  // amount transfered
55         // TODO: issuer keys is enough?
56         Denomination string
57         // TODO: maturity date is enough if the only constraint is <= now
58         IssueDate    int64
59         MaturityDate int64 // Draft is bounced before this date
60         // TODO: implement limits
61         Notes string // Arbitrary text notes of the drawer
62         // TODO: hack to make signed draft unique (not required for DSA)
63         Nonce string // unique number
64         // TODO: server ID might change, do we need it?
65         Server string // ID of the server (drawee?)
66         //TODO: naming: drawee vs issuer
67         Drawee string // ID of the obligation issuer
68         // TODO: reference cert ids in the draft
69         // useful if more strict date of issue information is needed
70         //References []string
71 }
72
73 // TODO: cert references: fpr+serial, fpr+certID
74 // Certification of obligation after a transfer
75 // References previous certificate (if any)
76 // and the transfer related other documents
77 type Cert struct {
78         Holder           string // ID of the creditor
79         Serial           uint32 // serial number, number of certs of the holder
80         Date             int64  // date of issue
81         Balance          int64  // current obligation value
82         Denomination     string
83         Issuer           string // ID of the obligation issuer (drawee?)
84         LastDebitSerial  uint32 // serial of the last draft cert or 0
85         LastCreditSerial uint32 // serial of the last credit cert or 0
86         // TODO: move to References?
87         LastCert string // ID of the previous cert if any
88         // TODO: determine cert type from diff value only?
89         // (>0: credit cert, <0: debit cert, ==0: special)
90         Difference int64 // difference from previous balance
91         // TODO: enough on the debit side
92         Draft string // draft ID related to the transfer
93         // TODO: credit side, redundant references
94         Drawer       string // ID of the drawer in the transaction
95         DrawerSerial uint32 // serial of the drawer's related debit cert
96         DrawerCert   string // ID of the drawer's related debit cert
97         // TODO: fingerprint?
98         References []string // cert IDs for timestamping the system
99 }
100
101 func DecodeClearSigned(s []byte) (c *ClearSigned, err error) {
102         hash, body, sig := split(s)
103         if len(sig) == 0 {
104                 // TODO: split errors
105                 return
106         }
107         c = &ClearSigned{string(hash), trimspace(dashunesc(body)), sig}
108         return
109 }
110
111 func EncodeClearSigned(c *ClearSigned) (data []byte, err error) {
112         s := ClearSignedHeader
113         if c.Hash != "" {
114                 s += "Hash: " + c.Hash + "\n"
115         }
116         // TODO: check if space was trimmed from body before signature
117         s += "\n"
118         s += string(dashesc(c.Body))
119         s += "\n"
120         s += string(c.ArmoredSignature)
121         data = []byte(s)
122         return
123 }
124
125 func ParseFields(s []byte) (fields map[string]string, rest []byte, err error) {
126         // TODO: error
127         fields = make(map[string]string)
128         rest = s
129         for len(rest) > 0 {
130                 var line []byte
131                 line, rest = getLine(rest)
132                 // empty line after the parsed fields (consumed)
133                 if len(line) == 0 {
134                         break
135                 }
136                 i := bytes.Index(line, []byte(": "))
137                 // TODO: long lines can be broken up in MIME
138                 if i < 0 {
139                         return nil, nil, nil
140                 }
141                 // TODO: repeated fields
142                 fields[string(line[:i])] = string(line[i+2:])
143         }
144         return
145 }
146
147 func ParseBody(s []byte) (t string, fields map[string]string, err error) {
148         // parse content type header first
149         mime, s, err := ParseFields(s)
150         if len(mime) != 1 {
151                 return
152         }
153         for k, v := range ContentType {
154                 if mime["Content-Type"] == v {
155                         t = k
156                         fields, s, err = ParseFields(s)
157                         if len(s) > 0 {
158                                 fields = nil
159                                 break
160                         }
161                         return
162                 }
163         }
164         // TODO: error
165         return
166 }
167
168 /* rendering with reflect
169 func render(d interface{}) (s []byte, err error) {
170         a := []string{}
171         v := reflect.ValueOf(d)
172         t := v.Type()
173         n := v.NumField()
174         for i := 0; i < n; i++ {
175                 f := t.Field(i)
176                 fv := v.Field(i)
177                 fs := ""
178                 switch fv.Type() {
179                 case reflect.String:
180                         fs = fv.String() // TODO: quote, esc (\n..)
181                 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
182                         fs = strconv.Itoa64(fv.Int())
183                 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
184                         fs = strconv.Uitoa64(fv.Uint())
185                 default:
186                         return // TODO: error
187                 }
188                 a = append(a, f.Name, ": ", fs, "\n")
189         }
190         s = strings.Join(a, "")
191         return
192 }
193 */
194
195 func checkID(s string) (string, error) {
196         return s, nil
197 }
198
199 func ParseDraft(s []byte) (draft *Draft, err error) {
200         t, fields, err := ParseBody(s)
201         if err != nil {
202                 return
203         }
204         if t != "draft" {
205                 return
206         }
207
208         draftFields := []string{
209                 "Drawer",
210                 "Beneficiary",
211                 "Amount",
212                 "Denomination",
213                 "IssueDate",
214                 "MaturityDate",
215                 "Notes",
216                 "Nonce",
217                 "Server",
218                 "Drawee",
219         }
220         if len(fields) != len(draftFields) {
221                 return
222         }
223         for _, f := range draftFields {
224                 _, ok := fields[f]
225                 if !ok {
226                         return
227                 }
228         }
229
230         draft = new(Draft)
231         draft.Drawer, _ = checkID(fields["Drawer"])
232         draft.Beneficiary, _ = checkID(fields["Beneficiary"])
233         draft.Amount, _ = strconv.Atoi64(fields["Amount"])
234         draft.Denomination = fields["Denomination"]
235         draft.IssueDate, _ = strconv.Atoi64(fields["IssueDate"])
236         draft.MaturityDate, _ = strconv.Atoi64(fields["MaturityDate"])
237         draft.Notes = fields["Notes"]
238         draft.Nonce = fields["Nonce"]
239         draft.Server, _ = checkID(fields["Server"])
240         draft.Drawee, _ = checkID(fields["Drawee"])
241
242         // more checks..
243
244         return
245 }
246
247 func RenderDraft(draft *Draft) (data []byte, err error) {
248         s := "Content-Type: " + ContentType["draft"] + "\n"
249         s += "\n"
250         s += "Drawer: " + draft.Drawer + "\n"
251         s += "Beneficiary: " + draft.Beneficiary + "\n"
252         s += "Amount: " + strconv.Itoa64(draft.Amount) + "\n"
253         s += "Denomination: " + draft.Denomination + "\n"
254         s += "IssueDate: " + strconv.Itoa64(draft.IssueDate) + "\n"
255         s += "MaturityDate: " + strconv.Itoa64(draft.MaturityDate) + "\n"
256         s += "Notes: " + draft.Notes + "\n"
257         s += "Nonce: " + draft.Nonce + "\n"
258         s += "Server: " + draft.Server + "\n"
259         s += "Drawee: " + draft.Drawee + "\n"
260         data = []byte(s)
261         return
262 }
263
264 func ParseCert(s []byte) (cert Cert, err error) {
265         return
266 }
267
268 func RenderCert(cert Cert) (s []byte, err error) {
269         return
270 }
271
272 func splitline(s []byte) (line, rest []byte) {
273         i := bytes.IndexByte(s, '\n')
274         if i < 0 {
275                 line = s
276         } else {
277                 rest = s[i+1:]
278                 if i > 0 && s[i-1] == '\r' {
279                         i--
280                 }
281                 line = s[:i]
282         }
283         return
284 }
285
286 func getLine(data []byte) (line, rest []byte) {
287         i := bytes.Index(data, []byte{'\n'})
288         j := i + 1
289         if i < 0 {
290                 i = len(data)
291                 j = i
292         } else if i > 0 && data[i-1] == '\r' {
293                 i--
294         }
295         return data[:i], data[j:]
296 }
297
298 func trimspace(s []byte) []byte {
299         a := bytes.Split(s, []byte("\n"))
300         for i := range a {
301                 a[i] = bytes.TrimRight(a[i], " \t\r")
302         }
303         return bytes.Join(a, []byte("\n"))
304 }
305
306 func dashesc(s []byte) []byte {
307         r := bytes.Replace(s, []byte("\n-"), []byte("\n- -"), -1)
308         if len(r) > 0 && r[0] == '-' {
309                 r = append([]byte("- "), r...)
310         }
311         return r
312 }
313
314 func dashunesc(s []byte) []byte {
315         r := bytes.Replace(s, []byte("\n- "), []byte("\n"), -1)
316         if len(r) >= 2 && r[0] == '-' && r[1] == ' ' {
317                 r = r[2:]
318         }
319         return r
320 }
321
322 // RFC 4880 is unclear about multiple Hash header semantics, section 7. says
323 //  "One or more "Hash" Armor Headers"
324 // then
325 //  "If more than one message digest is used in the signature, the "Hash"
326 //   armor header contains a comma-delimited list of used message digests."
327 // in section 6.2.
328 //  "there is no limit to the length of Armor Headers.  Care should
329 //   be taken that the Armor Headers are short enough to survive
330 //   transport.  One way to do this is to repeat an Armor Header key
331 //   multiple times with different values for each so that no one line is
332 //   overly long."
333 // we accept a single Hash header with a list of hash algorithms for now
334 // but use the one specified by the signature
335
336 func split(s []byte) (hash, body, sig []byte) {
337         if !bytes.HasPrefix(s, []byte(ClearSignedHeader)) {
338                 return
339         }
340         s = s[len(ClearSignedHeader):]
341         // only allow a single Hash: header
342         if bytes.HasPrefix(s, []byte("Hash: ")) {
343                 s = s[len("Hash: "):]
344                 hash, s = getLine(s)
345         }
346         // skip empty line
347         empty, s := getLine(s)
348         if len(empty) != 0 {
349                 return
350         }
351         i := bytes.Index(s, []byte("\n-----BEGIN"))
352         if i < 0 {
353                 return
354         }
355         body, sig = s[:i], s[i+1:]
356         if i > 0 && body[i-1] == '\r' {
357                 body = body[:i-1]
358         }
359         return
360 }