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 type 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"`
57 Beneficiary string `marshal:"id"`
60 Issuer string `marshal:"id"`
61 AuthorizedBy string `marshal:"id" key:"Authorized-By"`
62 Date int64 `marshal:"date"`
63 MaturityDate int64 `marshal:"date" key:"Maturity-Date"`
64 ExpiryDate int64 `marshal:"date" key:"Expiry-Date"`
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"`
77 Date int64 `marshal:"date"`
80 Issuer string `marshal:"id"`
81 AuthorizedBy string `marshal:"id" key:"Authorized-By"`
82 LastDebitSerial int64 `key:"Last-Debit-Serial"`
83 LastCreditSerial int64 `key:"Last-Credit-Serial"`
84 LastCert string `marshal:"id" key:"Last-Cert"`
86 Draft string `marshal:"id"`
87 Drawer string `marshal:"id"`
88 DrawerCert string `marshal:"id" key:"Drawer-Cert"`
89 Notes string // TODO: server or drawer?
90 References []string `marshal:"idlist"`
93 func ParseClearSigned(s []byte) (c *ClearSigned, err error) {
94 hash, body, sig := split(s)
96 err = fmt.Errorf("ParseClearSigned could parse the signed document")
99 c = &ClearSigned{string(hash), trimspace(dashunesc(body)), sig}
103 func FormatClearSigned(c *ClearSigned) (data []byte, err error) {
104 s := ClearSignedHeader
106 s += "Hash: " + c.Hash + "\n"
108 // TODO: check if space was trimmed from body before signature
110 s += string(dashesc(c.Body))
112 s += string(c.ArmoredSignature)
117 func ParseFields(s []byte) (fields []Field, rest []byte, err error) {
121 line, rest = getLine(rest)
122 // empty line after the parsed fields (consumed)
126 i := bytes.Index(line, []byte(": "))
128 err = fmt.Errorf("ParseFields: missing ': '\n")
131 fields = append(fields, Field{string(line[:i]), string(line[i+2:])})
136 func ParseBody(s []byte) (doctype string, fields []Field, err error) {
137 // parse content type header first
138 fs, s, err := ParseFields(s)
142 if len(fs) != 1 || fs[0].Key != "Content-Type" {
143 return "", nil, fmt.Errorf("ParseBody: single Content-Type header was expected\n")
146 for k, v := range ContentType {
153 return "", nil, fmt.Errorf("ParseBody: unknown Content-Type: %s", ctype)
155 fields, s, err = ParseFields(s)
156 if err == nil && len(s) > 0 {
157 err = fmt.Errorf("ParseBody: extra data after fields: %q", s)
162 func parse(s []byte, doctype string) (v interface{}, err error) {
163 t, fields, err := ParseBody(s)
167 if doctype != t && doctype != "" {
168 err = fmt.Errorf("parse: expected doctype %s; got %s", doctype, t)
177 err = fmt.Errorf("parse: unkown doc type: %s", t)
180 err = unmarshal(fields, v)
184 // TODO: limit errors
185 func format(v interface{}) ([]byte, error) {
193 panic("reder: unknown type")
195 s := "Content-Type: " + ContentType[doctype] + "\n\n"
197 for _, f := range fields {
198 s += f.Key + ": " + f.Value + "\n"
200 return []byte(s), nil
203 func ParseDraft(s []byte) (draft *Draft, err error) {
204 v, err := parse(s, "draft")
212 func FormatDraft(draft *Draft) ([]byte, error) {
216 func ParseCert(s []byte) (cert *Cert, err error) {
217 v, err := parse(s, "cert")
225 func FormatCert(cert *Cert) ([]byte, error) {
229 func parseId(s string) (string, error) {
231 return "", fmt.Errorf("parseId: expected 40 characters; got %d", len(s))
233 dst := make([]byte, len(s)/2)
234 _, err := hex.Decode(dst, []byte(s))
238 func formatId(s string) string {
242 func parseString(s string) (string, error) {
244 return "", fmt.Errorf("parseString: 140 chars limit is exceeded")
249 func formatString(s string) string {
253 func parseDate(s string) (int64, error) {
254 t, err := time.Parse(time.RFC3339, s)
258 return t.Seconds(), nil
261 func formatDate(i int64) string {
262 return time.SecondsToUTC(i).Format(time.RFC3339)
265 func unmarshal(fields []Field, iv interface{}) error {
266 v := reflect.ValueOf(iv)
267 if v.Kind() != reflect.Ptr || v.IsNil() || v.Elem().Kind() != reflect.Struct {
268 panic("unmarshal: input is not a pointer to struct")
273 if len(fields) != n {
274 return fmt.Errorf("unmarshal: %s has %d fields, got %d\n", t.Name(), n, len(fields))
276 for i := 0; i < n; i++ {
279 m := ft.Tag.Get("marshal")
280 k := ft.Tag.Get("key")
284 if fields[i].Key != k {
285 return fmt.Errorf("unmarshal: field %d of %s (%s) is missing\n", i+1, t.Name(), k)
294 val, err = parseId(s)
296 val, err = parseString(s)
298 panic("bad string field tag")
301 case reflect.Int, reflect.Int32, reflect.Int64:
305 val, err = parseDate(s)
307 val, err = strconv.Atoi64(s)
309 panic("bad int field tag")
312 case reflect.Uint, reflect.Uint32, reflect.Uint64:
316 val, err = strconv.Atoui64(s)
318 panic("bad uint field tag")
325 if fv.Type().Elem().Kind() != reflect.String {
326 panic("only string slice is supported")
328 ids := strings.Split(s, " ")
329 val = make([]string, len(ids))
331 val[j], err = parseId(ids[j])
337 panic("bad slice field tag")
339 fv.Set(reflect.ValueOf(val))
341 panic("bad field type")
350 func marshal(iv interface{}) []Field {
351 v := reflect.ValueOf(iv)
352 if v.Kind() != reflect.Ptr || v.IsNil() || v.Elem().Kind() != reflect.Struct {
353 panic("unmarshal: input is not a pointer to struct")
359 for i := 0; i < n; i++ {
362 m := ft.Tag.Get("marshal")
363 k := ft.Tag.Get("key")
372 val = formatId(fv.String())
374 val = formatString(fv.String())
376 panic("bad string field tag")
378 case reflect.Int, reflect.Int32, reflect.Int64:
381 val = formatDate(fv.Int())
383 val = strconv.Itoa64(fv.Int())
385 panic("bad int field tag")
387 case reflect.Uint, reflect.Uint32, reflect.Uint64:
390 val = strconv.Uitoa64(fv.Uint())
392 panic("bad uint field tag")
397 if fv.Type().Elem().Kind() != reflect.String {
398 panic("only string slice is supported")
401 for j := 0; j < k; j++ {
405 val += formatId(fv.Index(j).String())
408 panic("bad slice field tag")
411 panic("bad field type")
413 fields = append(fields, Field{k, val})
418 func getLine(data []byte) (line, rest []byte) {
419 i := bytes.Index(data, []byte{'\n'})
424 } else if i > 0 && data[i-1] == '\r' {
427 return data[:i], data[j:]
430 func trimspace(s []byte) []byte {
431 a := bytes.Split(s, []byte("\n"))
433 a[i] = bytes.TrimRight(a[i], " \t\r")
435 return bytes.Join(a, []byte("\n"))
438 func dashesc(s []byte) []byte {
439 r := bytes.Replace(s, []byte("\n-"), []byte("\n- -"), -1)
440 if len(r) > 0 && r[0] == '-' {
441 r = append([]byte("- "), r...)
446 func dashunesc(s []byte) []byte {
447 r := bytes.Replace(s, []byte("\n- "), []byte("\n"), -1)
448 if len(r) >= 2 && r[0] == '-' && r[1] == ' ' {
454 // RFC 4880 is unclear about multiple Hash header semantics, section 7. says
455 // "One or more "Hash" Armor Headers"
457 // "If more than one message digest is used in the signature, the "Hash"
458 // armor header contains a comma-delimited list of used message digests."
460 // "there is no limit to the length of Armor Headers. Care should
461 // be taken that the Armor Headers are short enough to survive
462 // transport. One way to do this is to repeat an Armor Header key
463 // multiple times with different values for each so that no one line is
465 // we accept a single Hash header with a list of hash algorithms for now
466 // but use the one specified by the signature
468 func split(s []byte) (hash, body, sig []byte) {
469 if !bytes.HasPrefix(s, []byte(ClearSignedHeader)) {
472 s = s[len(ClearSignedHeader):]
473 // only allow a single Hash: header
474 if bytes.HasPrefix(s, []byte("Hash: ")) {
475 s = s[len("Hash: "):]
479 empty, s := getLine(s)
483 i := bytes.Index(s, []byte("\n-----BEGIN"))
487 body, sig = s[:i], s[i+1:]
488 if i > 0 && body[i-1] == '\r' {