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.
11 // -----BEGIN PGP SIGNED MESSAGE-----
14 // Content-Type: text/plain.epoint.cert; charset=utf-8
17 // Another-Key: Value2
18 // -----BEGIN PGP SIGNATURE-----
20 // -----END PGP SIGNATURE-----
32 const ClearSignedHeader = "-----BEGIN PGP SIGNED MESSAGE-----\n"
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",
40 // OpenPGP signed cleartext document representation
41 type ClearSigned struct {
43 // Signed text (no dash escape, no trailing space)
45 // Armored detached text signature of the Body
46 ArmoredSignature []byte
54 // Draft document represents an obligation transfer order
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
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
71 // Obligation certificate after a transfer
72 // References previous certificate (if any)
73 // and the transfer related other documents
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
80 Issuer string `marshal:"id"` // ID of the obligation issuer (drawee?)
81 LastDebitSerial uint32 `key:"Last-Debit-Serial"` // serial of the last draft cert or 0
82 LastCreditSerial uint32 `key:"Last-Credit-Serial"` // serial of the last credit cert or 0
83 LastCert string `marshal:"id" key:"Last-Cert"` // ID of the previous cert if any
84 Difference int64 // difference from previous balance
85 Draft string `marshal:"id"` // draft ID related to the transfer
86 Drawer string `marshal:"id"` // ID of the drawer in the transaction
87 DrawerSerial uint32 `key:"Drawer-Serial"` // serial of the drawer's related debit cert
88 DrawerCert string `marshal:"id" key:"Drawer-Cert"` // ID of the drawer's related debit cert
89 References []string `marshal:"idlist"` // cert IDs for timestamping the system
92 func DecodeClearSigned(s []byte) (c *ClearSigned, err error) {
93 hash, body, sig := split(s)
95 err = fmt.Errorf("DecodeClearSigned could parse the signed document")
98 c = &ClearSigned{string(hash), trimspace(dashunesc(body)), sig}
102 func EncodeClearSigned(c *ClearSigned) (data []byte, err error) {
103 s := ClearSignedHeader
105 s += "Hash: " + c.Hash + "\n"
107 // TODO: check if space was trimmed from body before signature
109 s += string(dashesc(c.Body))
111 s += string(c.ArmoredSignature)
116 func ParseFields(s []byte) (fields []Field, rest []byte, err error) {
120 line, rest = getLine(rest)
121 // empty line after the parsed fields (consumed)
125 i := bytes.Index(line, []byte(": "))
127 err = fmt.Errorf("ParseFields: missing ': '\n")
130 fields = append(fields, Field{string(line[:i]), string(line[i+2:])})
135 func ParseBody(s []byte) (doctype string, fields []Field, err error) {
136 // parse content type header first
137 fs, s, err := ParseFields(s)
141 if len(fs) != 1 || fs[0].Key != "Content-Type" {
142 return "", nil, fmt.Errorf("ParseBody: single Content-Type header was expected\n")
145 for k, v := range ContentType {
152 return "", nil, fmt.Errorf("ParseBody: unknown Content-Type: %s", ctype)
154 fields, s, err = ParseFields(s)
155 if err == nil && len(s) > 0 {
156 err = fmt.Errorf("ParseBody: extra data after fields: %q", s)
161 func parse(s []byte, doctype string) (v interface{}, err error) {
162 t, fields, err := ParseBody(s)
166 if doctype != t && doctype != "" {
167 err = fmt.Errorf("parse: expected doctype %s; got %s", doctype, t)
176 err = fmt.Errorf("parse: unkown doc type: %s", t)
179 err = unmarshal(fields, v)
183 // TODO: limit errors
184 func render(v interface{}) ([]byte, error) {
192 panic("reder: unknown type")
194 s := "Content-Type: " + ContentType[doctype] + "\n\n"
196 for _, f := range fields {
197 s += f.Key + ": " + f.Value + "\n"
199 return []byte(s), nil
202 func ParseDraft(s []byte) (draft *Draft, err error) {
203 v, err := parse(s, "draft")
211 func RenderDraft(draft *Draft) ([]byte, error) {
215 func ParseCert(s []byte) (cert *Cert, err error) {
216 v, err := parse(s, "cert")
224 func RenderCert(cert *Cert) ([]byte, error) {
228 func formatId(s string) string {
229 return fmt.Sprintf("%040X", s)
232 func parseId(s string) (string, error) {
233 dst := make([]byte, 20)
235 return "", fmt.Errorf("parseId: expected 40 characters; got %d", len(s))
237 _, err := hex.Decode(dst, []byte(s))
238 return string(dst), err
241 func quoteValue(s string) string {
245 func unquoteValue(s string) (string, error) {
249 func formatDate(i int64) string {
250 return time.SecondsToUTC(i).Format(time.RFC3339)
253 func parseDate(s string) (int64, error) {
254 t, err := time.Parse(time.RFC3339, s)
258 return t.Seconds(), nil
261 func marshal(iv interface{}) []Field {
262 v := reflect.ValueOf(iv)
263 if v.Kind() != reflect.Ptr || v.IsNil() || v.Elem().Kind() != reflect.Struct {
264 panic("unmarshal: input is not a pointer to struct")
270 for i := 0; i < n; i++ {
273 m := ft.Tag.Get("marshal")
274 k := ft.Tag.Get("key")
283 val = formatId(fv.String())
285 val = quoteValue(fv.String())
287 panic("bad string field tag")
289 case reflect.Int, reflect.Int32, reflect.Int64:
292 val = formatDate(fv.Int())
294 val = strconv.Itoa64(fv.Int())
296 panic("bad int field tag")
298 case reflect.Uint, reflect.Uint32, reflect.Uint64:
301 val = strconv.Uitoa64(fv.Uint())
303 panic("bad uint field tag")
308 if fv.Type().Elem().Kind() != reflect.String {
309 panic("only string slice is supported")
312 for j := 0; j < k; j++ {
316 val += formatId(fv.Index(j).String())
319 panic("bad slice field tag")
322 panic("bad field type")
324 fields = append(fields, Field{k, val})
329 func unmarshal(fields []Field, iv interface{}) error {
330 v := reflect.ValueOf(iv)
331 if v.Kind() != reflect.Ptr || v.IsNil() || v.Elem().Kind() != reflect.Struct {
332 panic("unmarshal: input is not a pointer to struct")
337 if len(fields) != n {
338 return fmt.Errorf("unmarshal: %s has %d fields, got %d\n", t.Name(), n, len(fields))
340 for i := 0; i < n; i++ {
343 m := ft.Tag.Get("marshal")
344 k := ft.Tag.Get("key")
348 if fields[i].Key != k {
349 return fmt.Errorf("unmarshal: field %d of %s (%s) is missing\n", i, t.Name(), k)
358 val, err = parseId(s)
360 val, err = unquoteValue(s)
362 panic("bad string field tag")
365 case reflect.Int, reflect.Int32, reflect.Int64:
369 val, err = parseDate(s)
371 val, err = strconv.Atoi64(s)
373 panic("bad int field tag")
376 case reflect.Uint, reflect.Uint32, reflect.Uint64:
380 val, err = strconv.Atoui64(s)
382 panic("bad uint field tag")
389 if fv.Type().Elem().Kind() != reflect.String {
390 panic("only string slice is supported")
392 ids := strings.Split(s, " ")
393 val = make([]string, len(ids))
395 val[j], err = parseId(ids[j])
401 panic("bad slice field tag")
403 fv.Set(reflect.ValueOf(val))
405 panic("bad field type")
414 func getLine(data []byte) (line, rest []byte) {
415 i := bytes.Index(data, []byte{'\n'})
420 } else if i > 0 && data[i-1] == '\r' {
423 return data[:i], data[j:]
426 func trimspace(s []byte) []byte {
427 a := bytes.Split(s, []byte("\n"))
429 a[i] = bytes.TrimRight(a[i], " \t\r")
431 return bytes.Join(a, []byte("\n"))
434 func dashesc(s []byte) []byte {
435 r := bytes.Replace(s, []byte("\n-"), []byte("\n- -"), -1)
436 if len(r) > 0 && r[0] == '-' {
437 r = append([]byte("- "), r...)
442 func dashunesc(s []byte) []byte {
443 r := bytes.Replace(s, []byte("\n- "), []byte("\n"), -1)
444 if len(r) >= 2 && r[0] == '-' && r[1] == ' ' {
450 // RFC 4880 is unclear about multiple Hash header semantics, section 7. says
451 // "One or more "Hash" Armor Headers"
453 // "If more than one message digest is used in the signature, the "Hash"
454 // armor header contains a comma-delimited list of used message digests."
456 // "there is no limit to the length of Armor Headers. Care should
457 // be taken that the Armor Headers are short enough to survive
458 // transport. One way to do this is to repeat an Armor Header key
459 // multiple times with different values for each so that no one line is
461 // we accept a single Hash header with a list of hash algorithms for now
462 // but use the one specified by the signature
464 func split(s []byte) (hash, body, sig []byte) {
465 if !bytes.HasPrefix(s, []byte(ClearSignedHeader)) {
468 s = s[len(ClearSignedHeader):]
469 // only allow a single Hash: header
470 if bytes.HasPrefix(s, []byte("Hash: ")) {
471 s = s[len("Hash: "):]
475 empty, s := getLine(s)
479 i := bytes.Index(s, []byte("\n-----BEGIN"))
483 body, sig = s[:i], s[i+1:]
484 if i > 0 && body[i-1] == '\r' {