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 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
94 func DecodeClearSigned(s []byte) (c *ClearSigned, err error) {
95 hash, body, sig := split(s)
97 err = fmt.Errorf("DecodeClearSigned could parse the signed document")
100 c = &ClearSigned{string(hash), trimspace(dashunesc(body)), sig}
104 func EncodeClearSigned(c *ClearSigned) (data []byte, err error) {
105 s := ClearSignedHeader
107 s += "Hash: " + c.Hash + "\n"
109 // TODO: check if space was trimmed from body before signature
111 s += string(dashesc(c.Body))
113 s += string(c.ArmoredSignature)
118 func ParseFields(s []byte) (fields []Field, rest []byte, err error) {
122 line, rest = getLine(rest)
123 // empty line after the parsed fields (consumed)
127 i := bytes.Index(line, []byte(": "))
129 err = fmt.Errorf("ParseFields: missing ': '\n")
132 fields = append(fields, Field{string(line[:i]), string(line[i+2:])})
137 func ParseBody(s []byte) (doctype string, fields []Field, err error) {
138 // parse content type header first
139 fs, s, err := ParseFields(s)
143 if len(fs) != 1 || fs[0].Key != "Content-Type" {
144 return "", nil, fmt.Errorf("ParseBody: single Content-Type header was expected\n")
147 for k, v := range ContentType {
154 return "", nil, fmt.Errorf("ParseBody: unknown Content-Type: %s", ctype)
156 fields, s, err = ParseFields(s)
157 if err == nil && len(s) > 0 {
158 err = fmt.Errorf("ParseBody: extra data after fields: %q", s)
163 func parse(s []byte, doctype string) (v interface{}, err error) {
164 t, fields, err := ParseBody(s)
168 if doctype != t && doctype != "" {
169 err = fmt.Errorf("parse: expected doctype %s; got %s", doctype, t)
178 err = fmt.Errorf("parse: unkown doc type: %s", t)
181 err = unmarshal(fields, v)
185 // TODO: limit errors
186 func render(v interface{}) ([]byte, error) {
194 panic("reder: unknown type")
196 s := "Content-Type: " + ContentType[doctype] + "\n\n"
198 for _, f := range fields {
199 s += f.Key + ": " + f.Value + "\n"
201 return []byte(s), nil
204 func ParseDraft(s []byte) (draft *Draft, err error) {
205 v, err := parse(s, "draft")
213 func RenderDraft(draft *Draft) ([]byte, error) {
217 func ParseCert(s []byte) (cert *Cert, err error) {
218 v, err := parse(s, "cert")
226 func RenderCert(cert *Cert) ([]byte, error) {
230 func formatId(s string) string {
231 return fmt.Sprintf("%040X", s)
234 func parseId(s string) (string, error) {
235 dst := make([]byte, 20)
237 return "", fmt.Errorf("parseId: expected 40 characters; got %d", len(s))
239 _, err := hex.Decode(dst, []byte(s))
240 return string(dst), err
243 func quoteValue(s string) string {
247 func unquoteValue(s string) (string, error) {
251 func formatDate(i int64) string {
252 return time.SecondsToUTC(i).Format(time.RFC3339)
255 func parseDate(s string) (int64, error) {
256 t, err := time.Parse(time.RFC3339, s)
260 return t.Seconds(), nil
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")
272 for i := 0; i < n; i++ {
275 m := ft.Tag.Get("marshal")
276 k := ft.Tag.Get("key")
285 val = formatId(fv.String())
287 val = quoteValue(fv.String())
289 panic("bad string field tag")
291 case reflect.Int, reflect.Int32, reflect.Int64:
294 val = formatDate(fv.Int())
296 val = strconv.Itoa64(fv.Int())
298 panic("bad int field tag")
300 case reflect.Uint, reflect.Uint32, reflect.Uint64:
303 val = strconv.Uitoa64(fv.Uint())
305 panic("bad uint field tag")
310 if fv.Type().Elem().Kind() != reflect.String {
311 panic("only string slice is supported")
314 for j := 0; j < k; j++ {
318 val += formatId(fv.Index(j).String())
321 panic("bad slice field tag")
324 panic("bad field type")
326 fields = append(fields, Field{k, val})
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")
339 if len(fields) != n {
340 return fmt.Errorf("unmarshal: %s has %d fields, got %d\n", t.Name(), n, len(fields))
342 for i := 0; i < n; i++ {
345 m := ft.Tag.Get("marshal")
346 k := ft.Tag.Get("key")
350 if fields[i].Key != k {
351 return fmt.Errorf("unmarshal: field %d of %s (%s) is missing\n", i, t.Name(), k)
360 val, err = parseId(s)
362 val, err = unquoteValue(s)
364 panic("bad string field tag")
367 case reflect.Int, reflect.Int32, reflect.Int64:
371 val, err = parseDate(s)
373 val, err = strconv.Atoi64(s)
375 panic("bad int field tag")
378 case reflect.Uint, reflect.Uint32, reflect.Uint64:
382 val, err = strconv.Atoui64(s)
384 panic("bad uint field tag")
391 if fv.Type().Elem().Kind() != reflect.String {
392 panic("only string slice is supported")
394 ids := strings.Split(s, " ")
395 val = make([]string, len(ids))
397 val[j], err = parseId(ids[j])
403 panic("bad slice field tag")
405 fv.Set(reflect.ValueOf(val))
407 panic("bad field type")
416 func getLine(data []byte) (line, rest []byte) {
417 i := bytes.Index(data, []byte{'\n'})
422 } else if i > 0 && data[i-1] == '\r' {
425 return data[:i], data[j:]
428 func trimspace(s []byte) []byte {
429 a := bytes.Split(s, []byte("\n"))
431 a[i] = bytes.TrimRight(a[i], " \t\r")
433 return bytes.Join(a, []byte("\n"))
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...)
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] == ' ' {
452 // RFC 4880 is unclear about multiple Hash header semantics, section 7. says
453 // "One or more "Hash" Armor Headers"
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."
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
463 // we accept a single Hash header with a list of hash algorithms for now
464 // but use the one specified by the signature
466 func split(s []byte) (hash, body, sig []byte) {
467 if !bytes.HasPrefix(s, []byte(ClearSignedHeader)) {
470 s = s[len(ClearSignedHeader):]
471 // only allow a single Hash: header
472 if bytes.HasPrefix(s, []byte("Hash: ")) {
473 s = s[len("Hash: "):]
477 empty, s := getLine(s)
481 i := bytes.Index(s, []byte("\n-----BEGIN"))
485 body, sig = s[:i], s[i+1:]
486 if i > 0 && body[i-1] == '\r' {