// the signature detached. The format of the key-value pairs are
// similar to MIME header fields.
//
-// TODO: allow visually aligned field values
-// TODO: handling repeated fields
-// TODO: representation of a list (references)
-//
// Example:
//
// -----BEGIN PGP SIGNED MESSAGE-----
import (
"bytes"
+ "encoding/hex"
+ "fmt"
+ "reflect"
"strconv"
+ "strings"
+ "time"
)
const ClearSignedHeader = "-----BEGIN PGP SIGNED MESSAGE-----\n"
-// Non-standard MIME subtype see RFC 2045 and RFC 2046
-// TODO: verify that Content-Transfer-Encoding is not needed
-// TODO: text/epoint.cert would be shorter
+// (non-standard) MIME subtype 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",
ArmoredSignature []byte
}
+type Field struct {
+ Key string
+ Value string
+}
+
// Draft document represents an obligation transfer order
type Draft struct {
- Drawer string // ID of the payer (signer of the document)
- Beneficiary string // ID of the payee
- Amount int64 // amount transfered
- // TODO: issuer keys is enough?
+ Drawer string `marshal:"id"` // ID of the payer (signer of the document)
+ Beneficiary string `marshal:"id"` // ID of the payee
+ Amount int64 // amount transfered
Denomination string
- // TODO: maturity date is enough if the only constraint is <= now
- IssueDate int64
- MaturityDate int64 // Draft is bounced before this date
- // TODO: implement limits
- Notes string // Arbitrary text notes of the drawer
- // TODO: hack to make signed draft unique (not required for DSA)
- Nonce string // unique number
- // TODO: server ID might change, do we need it?
- Server string // ID of the server (drawee?)
- //TODO: naming: drawee vs issuer
- Drawee string // ID of the obligation issuer
- // TODO: reference cert ids in the draft
+ IssueDate int64 `marshal:"date" key:"Issue-Date"`
+ // Draft is bounced before this date
+ MaturityDate int64 `marshal:"date" key:"Maturity-Date"`
+ Notes string // Arbitrary text notes of the drawer
+ Nonce string // unique number
+ Server string `marshal:"id"` // ID of the server
+ Drawee string `marshal:"id"` // ID of the obligation issuer
// useful if more strict date of issue information is needed
//References []string
}
-// TODO: cert references: fpr+serial, fpr+certID
-// Certification of obligation after a transfer
+// Obligation certificate after a transfer
// References previous certificate (if any)
// and the transfer related other documents
type Cert struct {
- Holder string // ID of the creditor
+ Holder string `marshal:"id"` // ID of the creditor
Serial uint32 // serial number, number of certs of the holder
- Date int64 // date of issue
+ Date int64 `marshal:"date"` // date of issue
Balance int64 // current obligation value
Denomination string
- Issuer string // ID of the obligation issuer (drawee?)
- LastDebitSerial uint32 // serial of the last draft cert or 0
- LastCreditSerial uint32 // serial of the last credit cert or 0
- // TODO: move to References?
- LastCert string // ID of the previous cert if any
- // TODO: determine cert type from diff value only?
- // (>0: credit cert, <0: debit cert, ==0: special)
- Difference int64 // difference from previous balance
- // TODO: enough on the debit side
- Draft string // draft ID related to the transfer
- // TODO: credit side, redundant references
- Drawer string // ID of the drawer in the transaction
- DrawerSerial uint32 // serial of the drawer's related debit cert
- DrawerCert string // ID of the drawer's related debit cert
- // TODO: fingerprint?
- References []string // cert IDs for timestamping the system
+ Issuer string `marshal:"id"` // ID of the obligation issuer (drawee?)
+ LastDebitSerial uint32 `key:"Last-Debit-Serial"` // serial of the last draft cert or 0
+ LastCreditSerial uint32 `key:"Last-Credit-Serial"` // serial of the last credit cert or 0
+ LastCert string `marshal:"id" key:"Last-Cert"` // ID of the previous cert if any
+ Difference int64 // difference from previous balance
+ Draft string `marshal:"id"` // draft ID related to the transfer
+ Drawer string `marshal:"id"` // ID of the drawer in the transaction
+ DrawerSerial uint32 `key:"Drawer-Serial"` // serial of the drawer's related debit cert
+ DrawerCert string `marshal:"id" key:"Drawer-Cert"` // ID of the drawer's related debit cert
+ References []string `marshal:"idlist"` // cert IDs for timestamping the system
}
func DecodeClearSigned(s []byte) (c *ClearSigned, err error) {
hash, body, sig := split(s)
if len(sig) == 0 {
- // TODO: split errors
+ err = fmt.Errorf("DecodeClearSigned could parse the signed document")
return
}
c = &ClearSigned{string(hash), trimspace(dashunesc(body)), sig}
return
}
-func ParseFields(s []byte) (fields map[string]string, rest []byte, err error) {
- // TODO: error
- fields = make(map[string]string)
+func ParseFields(s []byte) (fields []Field, rest []byte, err error) {
rest = s
for len(rest) > 0 {
var line []byte
break
}
i := bytes.Index(line, []byte(": "))
- // TODO: long lines can be broken up in MIME
if i < 0 {
- return nil, nil, nil
+ err = fmt.Errorf("ParseFields: missing ': '\n")
+ break
}
- // TODO: repeated fields
- fields[string(line[:i])] = string(line[i+2:])
+ fields = append(fields, Field{string(line[:i]), string(line[i+2:])})
}
return
}
-func ParseBody(s []byte) (t string, fields map[string]string, err error) {
+func ParseBody(s []byte) (doctype string, fields []Field, err error) {
// parse content type header first
- mime, s, err := ParseFields(s)
- if len(mime) != 1 {
+ 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 mime["Content-Type"] == v {
- t = k
- fields, s, err = ParseFields(s)
- if len(s) > 0 {
- fields = nil
- break
- }
- return
+ if ctype == v {
+ doctype = k
+ break
}
}
- // TODO: error
- return
-}
-
-/* rendering with reflect
-func render(d interface{}) (s []byte, err error) {
- a := []string{}
- v := reflect.ValueOf(d)
- t := v.Type()
- n := v.NumField()
- for i := 0; i < n; i++ {
- f := t.Field(i)
- fv := v.Field(i)
- fs := ""
- switch fv.Type() {
- case reflect.String:
- fs = fv.String() // TODO: quote, esc (\n..)
- case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
- fs = strconv.Itoa64(fv.Int())
- case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
- fs = strconv.Uitoa64(fv.Uint())
- default:
- return // TODO: error
- }
- a = append(a, f.Name, ": ", fs, "\n")
+ 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)
}
- s = strings.Join(a, "")
return
}
-*/
-
-func checkID(s string) (string, error) {
- return s, nil
-}
-func ParseDraft(s []byte) (draft *Draft, err error) {
+func parse(s []byte, doctype string) (v interface{}, err error) {
t, fields, err := ParseBody(s)
if err != nil {
return
}
- if t != "draft" {
+ 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
+}
- draftFields := []string{
- "Drawer",
- "Beneficiary",
- "Amount",
- "Denomination",
- "IssueDate",
- "MaturityDate",
- "Notes",
- "Nonce",
- "Server",
- "Drawee",
+// TODO: limit errors
+func render(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"
}
- if len(fields) != len(draftFields) {
+ return []byte(s), nil
+}
+
+func ParseDraft(s []byte) (draft *Draft, err error) {
+ v, err := parse(s, "draft")
+ if err != nil {
return
}
- for _, f := range draftFields {
- _, ok := fields[f]
- if !ok {
- return
- }
+ draft = v.(*Draft)
+ return
+}
+
+func RenderDraft(draft *Draft) ([]byte, error) {
+ return render(draft)
+}
+
+func ParseCert(s []byte) (cert *Cert, err error) {
+ v, err := parse(s, "cert")
+ if err != nil {
+ return nil, err
}
+ cert = v.(*Cert)
+ return
+}
- draft = new(Draft)
- draft.Drawer, _ = checkID(fields["Drawer"])
- draft.Beneficiary, _ = checkID(fields["Beneficiary"])
- draft.Amount, _ = strconv.Atoi64(fields["Amount"])
- draft.Denomination = fields["Denomination"]
- draft.IssueDate, _ = strconv.Atoi64(fields["IssueDate"])
- draft.MaturityDate, _ = strconv.Atoi64(fields["MaturityDate"])
- draft.Notes = fields["Notes"]
- draft.Nonce = fields["Nonce"]
- draft.Server, _ = checkID(fields["Server"])
- draft.Drawee, _ = checkID(fields["Drawee"])
+func RenderCert(cert *Cert) ([]byte, error) {
+ return render(cert)
+}
- // more checks..
+func formatId(s string) string {
+ return fmt.Sprintf("%040X", s)
+}
- return
+func parseId(s string) (string, error) {
+ dst := make([]byte, 20)
+ if len(s) != 40 {
+ return "", fmt.Errorf("parseId: expected 40 characters; got %d", len(s))
+ }
+ _, err := hex.Decode(dst, []byte(s))
+ return string(dst), err
}
-func RenderDraft(draft *Draft) (data []byte, err error) {
- s := "Content-Type: " + ContentType["draft"] + "\n"
- s += "\n"
- s += "Drawer: " + draft.Drawer + "\n"
- s += "Beneficiary: " + draft.Beneficiary + "\n"
- s += "Amount: " + strconv.Itoa64(draft.Amount) + "\n"
- s += "Denomination: " + draft.Denomination + "\n"
- s += "IssueDate: " + strconv.Itoa64(draft.IssueDate) + "\n"
- s += "MaturityDate: " + strconv.Itoa64(draft.MaturityDate) + "\n"
- s += "Notes: " + draft.Notes + "\n"
- s += "Nonce: " + draft.Nonce + "\n"
- s += "Server: " + draft.Server + "\n"
- s += "Drawee: " + draft.Drawee + "\n"
- data = []byte(s)
- return
+func quoteValue(s string) string {
+ return s
}
-func ParseCert(s []byte) (cert Cert, err error) {
- return
+func unquoteValue(s string) (string, error) {
+ return s, nil
}
-func RenderCert(cert Cert) (s []byte, err error) {
- return
+func formatDate(i int64) string {
+ return time.SecondsToUTC(i).Format(time.RFC3339)
}
-func splitline(s []byte) (line, rest []byte) {
- i := bytes.IndexByte(s, '\n')
- if i < 0 {
- line = s
- } else {
- rest = s[i+1:]
- if i > 0 && s[i-1] == '\r' {
- i--
+func parseDate(s string) (int64, error) {
+ t, err := time.Parse(time.RFC3339, s)
+ if err != nil {
+ return 0, err
+ }
+ return t.Seconds(), 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 = quoteValue(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")
}
- line = s[:i]
+ fields = append(fields, Field{k, val})
}
- return
+ return fields
+}
+
+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, 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 = unquoteValue(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 getLine(data []byte) (line, rest []byte) {
-----END PGP SIGNATURE-----
`)}
-var F = map[string]string{
- "A": "foo",
- "B": "bar",
- "a": "baz",
+var F = []Field{
+ {"A", "foo"},
+ {"B", "bar"},
+ {"a", "baz"},
}
var testData = []struct {
D []byte
C *ClearSigned
T string
- F map[string]string
+ F []Field
}{
{[]byte(D1), C1, "cert", F},
{[]byte(D2), C2, "cert", F},
string(c1.ArmoredSignature) == string(c2.ArmoredSignature)
}
-func eqFields(f1, f2 map[string]string) bool {
- for k,v := range f1 {
- if f2[k] != v {
+func eqFields(f1, f2 []Field) bool {
+ if len(f1) != len(f2) {
+ return false
+ }
+ for i, v := range f1 {
+ if f2[i].Key != v.Key && f2[i].Value != v.Value {
return false
}
}
- return len(f1) == len(f2)
+ return true
}
func TestClearSigned(t *testing.T) {
const draftBody = `Content-Type: text/plain.epoint.draft; charset=utf-8
-Drawer: 000000000000000000000000000000000000000a
-Beneficiary: 000000000000000000000000000000000000000b
+Drawer: 000000000000000000000000000000000000000A
+Beneficiary: 000000000000000000000000000000000000000B
Amount: 1
Denomination: half euro
-IssueDate: 2
-MaturityDate: 3
+Issue-Date: 2011-11-13T12:20:35Z
+Maturity-Date: 2011-12-27T09:18:46Z
Notes: some notes
Nonce: 42
-Server: 000000000000000000000000000000000000000c
-Drawee: 000000000000000000000000000000000000000d
+Server: 000000000000000000000000000000000000000C
+Drawee: 000000000000000000000000000000000000000D
`
func TestDraft(t *testing.T) {
d, err := ParseDraft([]byte(draftBody))
if err != nil {
- t.Errorf("parse %q draft failed: %s\n", draftBody, err)
+ t.Errorf("ParseDraft failed: %s\n", err)
+ return
}
s, err := RenderDraft(d)
if err != nil {
t.Errorf("render %v draft failed: %s\n", d, err)
}
if string(s) != draftBody {
- t.Errorf("parsed %#v, expected %#v\n", d, draftBody)
+ t.Errorf("parsed %#v\nexpected: %s\ngot: %s\n", d, draftBody, s)
+ }
+}
+
+const certBody = `Content-Type: text/plain.epoint.cert; charset=utf-8
+
+Holder: 000000000000000000000000000000000000000A
+Serial: 13
+Date: 2011-11-01T10:29:38Z
+Balance: 23
+Denomination: half euro
+Issuer: 000000000000000000000000000000000000000B
+Last-Debit-Serial: 0
+Last-Credit-Serial: 12
+Last-Cert: 000000000000000000000000000000000000000C
+Difference: 1
+Draft: 000000000000000000000000000000000000000D
+Drawer: 000000000000000000000000000000000000000E
+Drawer-Serial: 2
+Drawer-Cert: 000000000000000000000000000000000000000F
+References: 000000000000000000000000000000000000000C 000000000000000000000000000000000000000F
+`
+
+func TestCert(t *testing.T) {
+ c, err := ParseCert([]byte(certBody))
+ if err != nil {
+ t.Errorf("ParseCert failed: %s\n", err)
+ return
+ }
+ s, err := RenderCert(c)
+ if err != nil {
+ t.Errorf("render %v cert failed: %s\n", c, err)
+ }
+ if string(s) != certBody {
+ t.Errorf("parsed %#v\nexpected: %s\ngot: %s\n", c, certBody, s)
}
}