package document // An epoint document is a clear signed utf-8 text of key-value pairs // according to OpenPGP RFC 4880. The body contains a content-type // MIME header so it can be used in OpenPGP/MIME RFC 3156 emails with // the signature detached. The format of the key-value pairs are // similar to MIME header fields. // // Example: // // -----BEGIN PGP SIGNED MESSAGE----- // Hash: SHA1 // // Content-Type: text/plain.epoint.cert; charset=utf-8 // // Key: Value1 // Another-Key: Value2 // -----BEGIN PGP SIGNATURE----- // pgp signature // -----END PGP SIGNATURE----- import ( "bytes" "encoding/hex" "fmt" "reflect" "strconv" "strings" "time" ) const ClearSignedHeader = "-----BEGIN PGP SIGNED MESSAGE-----\n" // (non-standard) MIME type for epoint documents, see RFC 2045 and RFC 2046 var ContentType = map[string]string{ "cert": "text/plain.epoint.cert; charset=utf-8", "draft": "text/plain.epoint.draft; charset=utf-8", } // OpenPGP signed cleartext document representation type ClearSigned struct { Hash string // Signed text (no dash escape, no trailing space) Body []byte // Armored detached text signature of the Body ArmoredSignature []byte } type Field struct { Key string Value string } // Draft document represents an obligation transfer order type Draft struct { Drawer string `marshal:"id"` Beneficiary string `marshal:"id"` Amount int64 Denomination string Issuer string `marshal:"id"` AuthorizedBy string `marshal:"id" key:"Authorized-By"` Date int64 `marshal:"date"` MaturityDate int64 `marshal:"date" key:"Maturity-Date"` ExpiryDate int64 `marshal:"date" key:"Expiry-Date"` Notes string Nonce string // useful if more strict date of issue information is needed //References []string } // Obligation certificate after a transfer // References previous certificate (if any) // and the transfer related other documents type Cert struct { Holder string `marshal:"id"` Serial int64 Date int64 `marshal:"date"` Balance int64 Denomination string Issuer string `marshal:"id"` AuthorizedBy string `marshal:"id" key:"Authorized-By"` LastDebitSerial int64 `key:"Last-Debit-Serial"` LastCreditSerial int64 `key:"Last-Credit-Serial"` LastCert string `marshal:"id" key:"Last-Cert"` Difference int64 Draft string `marshal:"id"` Drawer string `marshal:"id"` DrawerCert string `marshal:"id" key:"Drawer-Cert"` Notes string // TODO: server or drawer? References []string `marshal:"idlist"` } func ParseClearSigned(s []byte) (c *ClearSigned, err error) { hash, body, sig := split(s) if len(sig) == 0 { err = fmt.Errorf("ParseClearSigned could parse the signed document") return } c = &ClearSigned{string(hash), trimspace(dashunesc(body)), sig} return } func FormatClearSigned(c *ClearSigned) (data []byte, err error) { s := ClearSignedHeader if c.Hash != "" { s += "Hash: " + c.Hash + "\n" } // TODO: check if space was trimmed from body before signature s += "\n" s += string(dashesc(c.Body)) s += "\n" s += string(c.ArmoredSignature) data = []byte(s) return } func ParseFields(s []byte) (fields []Field, rest []byte, err error) { rest = s for len(rest) > 0 { var line []byte line, rest = getLine(rest) // empty line after the parsed fields (consumed) if len(line) == 0 { break } i := bytes.Index(line, []byte(": ")) if i < 0 { err = fmt.Errorf("ParseFields: missing ': '\n") break } fields = append(fields, Field{string(line[:i]), string(line[i+2:])}) } return } func ParseBody(s []byte) (doctype string, fields []Field, err error) { // parse content type header first fs, s, err := ParseFields(s) if err != nil { return } if len(fs) != 1 || fs[0].Key != "Content-Type" { return "", nil, fmt.Errorf("ParseBody: single Content-Type header was expected\n") } ctype := fs[0].Value for k, v := range ContentType { if ctype == v { doctype = k break } } if doctype == "" { return "", nil, fmt.Errorf("ParseBody: unknown Content-Type: %s", ctype) } fields, s, err = ParseFields(s) if err == nil && len(s) > 0 { err = fmt.Errorf("ParseBody: extra data after fields: %q", s) } return } func parse(s []byte, doctype string) (v interface{}, err error) { t, fields, err := ParseBody(s) if err != nil { return } if doctype != t && doctype != "" { err = fmt.Errorf("parse: expected doctype %s; got %s", doctype, t) return } switch t { case "draft": v = new(Draft) case "cert": v = new(Cert) default: err = fmt.Errorf("parse: unkown doc type: %s", t) return } err = unmarshal(fields, v) return } // TODO: limit errors func format(v interface{}) ([]byte, error) { doctype := "" switch v.(type) { case *Draft: doctype = "draft" case *Cert: doctype = "cert" default: panic("reder: unknown type") } s := "Content-Type: " + ContentType[doctype] + "\n\n" fields := marshal(v) for _, f := range fields { s += f.Key + ": " + f.Value + "\n" } return []byte(s), nil } func ParseDraft(s []byte) (draft *Draft, err error) { v, err := parse(s, "draft") if err != nil { return } draft = v.(*Draft) return } func FormatDraft(draft *Draft) ([]byte, error) { return format(draft) } func ParseCert(s []byte) (cert *Cert, err error) { v, err := parse(s, "cert") if err != nil { return nil, err } cert = v.(*Cert) return } func FormatCert(cert *Cert) ([]byte, error) { return format(cert) } func parseId(s string) (string, error) { if len(s) != 40 { return "", fmt.Errorf("parseId: expected 40 characters; got %d", len(s)) } dst := make([]byte, len(s)/2) _, err := hex.Decode(dst, []byte(s)) return s, err } func formatId(s string) string { return s } func parseString(s string) (string, error) { if len(s) > 140 { return "", fmt.Errorf("parseString: 140 chars limit is exceeded") } return s, nil } func formatString(s string) string { return s } func parseDate(s string) (int64, error) { t, err := time.Parse(time.RFC3339, s) if err != nil { return 0, err } return t.Seconds(), nil } func formatDate(i int64) string { return time.SecondsToUTC(i).Format(time.RFC3339) } func unmarshal(fields []Field, iv interface{}) error { v := reflect.ValueOf(iv) if v.Kind() != reflect.Ptr || v.IsNil() || v.Elem().Kind() != reflect.Struct { panic("unmarshal: input is not a pointer to struct") } v = v.Elem() t := v.Type() n := v.NumField() if len(fields) != n { return fmt.Errorf("unmarshal: %s has %d fields, got %d\n", t.Name(), n, len(fields)) } for i := 0; i < n; i++ { ft := t.Field(i) fv := v.Field(i) m := ft.Tag.Get("marshal") k := ft.Tag.Get("key") if k == "" { k = ft.Name } if fields[i].Key != k { return fmt.Errorf("unmarshal: field %d of %s (%s) is missing\n", i+1, t.Name(), k) } s := fields[i].Value var err error switch fv.Kind() { case reflect.String: var val string switch m { case "id": val, err = parseId(s) case "": val, err = parseString(s) default: panic("bad string field tag") } fv.SetString(val) case reflect.Int, reflect.Int32, reflect.Int64: var val int64 switch m { case "date": val, err = parseDate(s) case "": val, err = strconv.Atoi64(s) default: panic("bad int field tag") } fv.SetInt(val) case reflect.Uint, reflect.Uint32, reflect.Uint64: var val uint64 switch m { case "": val, err = strconv.Atoui64(s) default: panic("bad uint field tag") } fv.SetUint(val) case reflect.Slice: var val []string switch m { case "idlist": if fv.Type().Elem().Kind() != reflect.String { panic("only string slice is supported") } ids := strings.Split(s, " ") val = make([]string, len(ids)) for j := range val { val[j], err = parseId(ids[j]) if err != nil { return err } } default: panic("bad slice field tag") } fv.Set(reflect.ValueOf(val)) default: panic("bad field type") } if err != nil { return err } } return nil } func marshal(iv interface{}) []Field { v := reflect.ValueOf(iv) if v.Kind() != reflect.Ptr || v.IsNil() || v.Elem().Kind() != reflect.Struct { panic("unmarshal: input is not a pointer to struct") } v = v.Elem() t := v.Type() n := v.NumField() fields := []Field{} for i := 0; i < n; i++ { ft := t.Field(i) fv := v.Field(i) m := ft.Tag.Get("marshal") k := ft.Tag.Get("key") if k == "" { k = ft.Name } val := "" switch fv.Kind() { case reflect.String: switch m { case "id": val = formatId(fv.String()) case "": val = formatString(fv.String()) default: panic("bad string field tag") } case reflect.Int, reflect.Int32, reflect.Int64: switch m { case "date": val = formatDate(fv.Int()) case "": val = strconv.Itoa64(fv.Int()) default: panic("bad int field tag") } case reflect.Uint, reflect.Uint32, reflect.Uint64: switch m { case "": val = strconv.Uitoa64(fv.Uint()) default: panic("bad uint field tag") } case reflect.Slice: switch m { case "idlist": if fv.Type().Elem().Kind() != reflect.String { panic("only string slice is supported") } k := fv.Len() for j := 0; j < k; j++ { if j > 0 { val += " " } val += formatId(fv.Index(j).String()) } default: panic("bad slice field tag") } default: panic("bad field type") } fields = append(fields, Field{k, val}) } return fields } func getLine(data []byte) (line, rest []byte) { i := bytes.Index(data, []byte{'\n'}) j := i + 1 if i < 0 { i = len(data) j = i } else if i > 0 && data[i-1] == '\r' { i-- } return data[:i], data[j:] } func trimspace(s []byte) []byte { a := bytes.Split(s, []byte("\n")) for i := range a { a[i] = bytes.TrimRight(a[i], " \t\r") } return bytes.Join(a, []byte("\n")) } func dashesc(s []byte) []byte { r := bytes.Replace(s, []byte("\n-"), []byte("\n- -"), -1) if len(r) > 0 && r[0] == '-' { r = append([]byte("- "), r...) } return r } func dashunesc(s []byte) []byte { r := bytes.Replace(s, []byte("\n- "), []byte("\n"), -1) if len(r) >= 2 && r[0] == '-' && r[1] == ' ' { r = r[2:] } return r } // RFC 4880 is unclear about multiple Hash header semantics, section 7. says // "One or more "Hash" Armor Headers" // then // "If more than one message digest is used in the signature, the "Hash" // armor header contains a comma-delimited list of used message digests." // in section 6.2. // "there is no limit to the length of Armor Headers. Care should // be taken that the Armor Headers are short enough to survive // transport. One way to do this is to repeat an Armor Header key // multiple times with different values for each so that no one line is // overly long." // we accept a single Hash header with a list of hash algorithms for now // but use the one specified by the signature func split(s []byte) (hash, body, sig []byte) { if !bytes.HasPrefix(s, []byte(ClearSignedHeader)) { return } s = s[len(ClearSignedHeader):] // only allow a single Hash: header if bytes.HasPrefix(s, []byte("Hash: ")) { s = s[len("Hash: "):] hash, s = getLine(s) } // skip empty line empty, s := getLine(s) if len(empty) != 0 { return } i := bytes.Index(s, []byte("\n-----BEGIN")) if i < 0 { return } body, sig = s[:i], s[i+1:] if i > 0 && body[i-1] == '\r' { body = body[:i-1] } return }