-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.
+// Package document implements epoint document parsing and creation.
+//
+// An epoint document is an OpenPGP (RFC 4880) clear signed
+// utf-8 text of key-value pairs.
+// The body contains a content-type MIME header so the document
+// can be used in OpenPGP/MIME (RFC 3156) emails.
+// The format of the key-value pairs are similar to MIME header
+// fields: keys and values are separated by ": ", repeated keys
+// are not allowed, long values can be split before a space.
//
// Example:
//
// -----BEGIN PGP SIGNED MESSAGE-----
// Hash: SHA1
//
-// Content-Type: text/plain.epoint.cert; charset=utf-8
+// Content-Type: text/plain.epoint.type; charset=utf-8
//
// Key: Value1
// Another-Key: Value2
+// Last-Key: Long
+// value that spans
+// multiple lines
// -----BEGIN PGP SIGNATURE-----
+//
// pgp signature
// -----END PGP SIGNATURE-----
+package document
+
+// TODO: error types
+// TODO: optional fields
+// TODO: fields of notice (last notice, serial)
+// TODO: space vs not to space
import (
"bytes"
+ "crypto"
+ "crypto/openpgp"
+ "crypto/openpgp/armor"
+ "crypto/openpgp/packet"
"encoding/hex"
"fmt"
"reflect"
"time"
)
-const ClearSignedHeader = "-----BEGIN PGP SIGNED MESSAGE-----\n"
+const ClearSignedHeader = "-----BEGIN PGP SIGNED MESSAGE-----"
-// (non-standard) MIME type for epoint documents, see RFC 2045 and RFC 2046
+// MIME type for epoint documents, see RFC 4288
var ContentType = map[string]string{
- "cert": "text/plain.epoint.cert; charset=utf-8",
- "draft": "text/plain.epoint.draft; charset=utf-8",
+ "Draft": "text/vnd.epoint.draft; charset=utf-8",
+ "Notice": "text/vnd.epoint.notice; charset=utf-8",
+ "DebitCert": "text/vnd.epoint.debit; charset=utf-8",
+ "CreditCert": "text/vnd.epoint.credit; charset=utf-8",
+ "BounceCert": "text/vnd.epoint.bounce; charset=utf-8",
}
// OpenPGP signed cleartext document representation
-type ClearSigned struct {
+type Signed struct {
+ // Sign and CleanSigned sets Hash for FormatSigned
+ // TODO: CreationDate
Hash string
- // Signed text (no dash escape, no trailing space)
+ // Signed text (no dash escape, no trailing space, \n new lines)
Body []byte
// Armored detached text signature of the Body
- ArmoredSignature []byte
+ Signature []byte
+}
+
+// parsed epoint document
+type Document struct {
+ Type string
+ Fields map[string]string
+ Order []string
}
-type Field struct {
- Key string
- Value string
+var fieldtype = map[string]string{
+ "Amount": "int",
+ "Authorized-By": "id",
+ "Balance": "int",
+ "Beneficiary": "id",
+ "Date": "date",
+ "Debit-Cert": "id",
+ "Denomination": "text",
+ "Difference": "int",
+ "Draft": "id",
+ "Drawer": "id",
+ "Expiry-Date": "date",
+ "Holder": "id",
+ "Issuer": "id",
+ "Last-Cert": "id",
+ "Last-Credit-Serial": "int",
+ "Last-Debit-Serial": "int",
+ "Maturity-Date": "date",
+ "Nonce": "text",
+ "Notes": "text",
+ "References": "ids",
+ "Serial": "int",
+}
+
+var fieldname = map[string]string{
+ "AuthorizedBy": "Authorized-By",
+ "DebitCert": "Debit-Cert",
+ "ExpiryDate": "Expiry-Date",
+ "LastCert": "Last-Cert",
+ "LastCreditSerial": "Last-Credit-Serial",
+ "LastDebitSerial": "Last-Debit-Serial",
+ "MaturityDate": "Maturity-Date",
}
-// Draft document represents an obligation transfer order
type Draft struct {
- Drawer string `marshal:"id"`
- Beneficiary string `marshal:"id"`
+ Drawer string
+ Beneficiary string
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"`
+ Issuer string
+ AuthorizedBy string
+ MaturityDate int64 // optional
+ ExpiryDate int64 // optional
+ Nonce string // optional
+ Notes string // optional
+}
+
+type Notice struct {
+ Date int64
+ AuthorizedBy string
Notes string
- Nonce string
- // useful if more strict date of issue information is needed
- //References []string
+ 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"`
+type DebitCert struct {
+ Holder string
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"`
+ Issuer string
+ Date int64
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"`
+ Draft string
+ AuthorizedBy string
+ Notes string // optional
+ LastDebitSerial int64 // 0 if none
+ LastCreditSerial int64 // 0 if none
+ LastCert string // ? if serial == 1
+ References []string // may be empty
}
-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
+type CreditCert struct {
+ Holder string
+ Serial int64
+ Balance int64
+ Denomination string
+ Issuer string
+ Date int64
+ Difference int64
+ Draft string
+ Drawer string
+ DebitCert string
+ AuthorizedBy string
+ Notes string // optional
+ LastDebitSerial int64 // 0 if none
+ LastCreditSerial int64 // 0 if none
+ LastCert string // ? if serial == 1
+ References []string // may be empty
}
-func FormatClearSigned(c *ClearSigned) (data []byte, err error) {
- s := ClearSignedHeader
- if c.Hash != "" {
- s += "Hash: " + c.Hash + "\n"
+type BounceCert struct {
+ Drawer string
+ Draft string
+ LastCert string // optional
+ Balance int64 // 0 if none
+ Denomination string
+ Issuer string
+ Date int64
+ AuthorizedBy string
+ Notes string // optional
+ References []string // may be empty
+}
+
+// parse an epoint document without checking the signature and format details
+func Parse(s []byte) (iv interface{}, c *Signed, err error) {
+ c, err = ParseSigned(s)
+ if err != nil {
+ return
}
- // 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)
+ doc, err := ParseDocument(c.Body)
+ if err != nil {
+ return
+ }
+ iv, err = ParseStruct(doc)
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:])})
+// format and sign an epoint document
+func Format(iv interface{}, key *openpgp.Entity) (s []byte, err error) {
+ doc, err := FormatStruct(iv)
+ if err != nil {
+ return
}
- return
+ body, err := FormatDocument(doc)
+ if err != nil {
+ return
+ }
+ c, err := Sign(body, key)
+ if err != nil {
+ return
+ }
+ return FormatSigned(c)
}
-func ParseBody(s []byte) (doctype string, fields []Field, err error) {
- // parse content type header first
- fs, s, err := ParseFields(s)
+// verify an epoint document, return the cleaned version as well
+func Verify(c *Signed, key *openpgp.Entity) (cleaned []byte, err error) {
+ err = CleanSigned(c)
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")
+ err = VerifyCleaned(c, key)
+ if err != nil {
+ return
}
- ctype := fs[0].Value
- for k, v := range ContentType {
- if ctype == v {
- doctype = k
- break
+ return FormatSigned(c)
+}
+
+// verify signature of body with given key
+func VerifyCleaned(c *Signed, key *openpgp.Entity) (err error) {
+ kr := openpgp.EntityList{key}
+ msg := bytes.NewBuffer(c.Body)
+ sig := bytes.NewBuffer(c.Signature)
+ _, err = openpgp.CheckArmoredDetachedSignature(kr, msg, sig)
+ return
+}
+
+// sign body with given secret key
+func Sign(body []byte, key *openpgp.Entity) (c *Signed, err error) {
+ c = new(Signed)
+ c.Hash = "SHA256"
+ c.Body = body
+ w := new(bytes.Buffer)
+ err = openpgp.ArmoredDetachSignText(w, key, bytes.NewBuffer(c.Body))
+ c.Signature = w.Bytes()
+ return
+}
+
+// split a clear signed document into body and armored signature
+func ParseSigned(s []byte) (c *Signed, err error) {
+ // look for clear signed header
+ for !bytes.HasPrefix(s, []byte(ClearSignedHeader)) {
+ _, s = getLine(s)
+ if len(s) == 0 {
+ err = fmt.Errorf("ParseSigned: clear signed header is missing")
+ return
}
}
- if doctype == "" {
- return "", nil, fmt.Errorf("ParseBody: unknown Content-Type: %s", ctype)
+ s = s[len(ClearSignedHeader):]
+ // end of line after the header
+ empty, s := getLine(s)
+ if len(empty) != 0 {
+ err = fmt.Errorf("ParseSigned: bad clear signed header")
+ return
}
- fields, s, err = ParseFields(s)
- if err == nil && len(s) > 0 {
- err = fmt.Errorf("ParseBody: extra data after fields: %q", s)
+ // skip all hash headers, section 7.
+ for bytes.HasPrefix(s, []byte("Hash: ")) {
+ _, s = getLine(s)
+ }
+ // skip empty line
+ empty, s = getLine(s)
+ if len(empty) != 0 {
+ err = fmt.Errorf("ParseSigned: expected an empty line after armor headers")
+ return
}
+ lines := [][]byte{}
+ for !bytes.HasPrefix(s, []byte("-----BEGIN")) {
+ var line []byte
+ line, s = getLine(s)
+ // dash unescape, section 7.1.
+ if bytes.HasPrefix(line, []byte("- ")) {
+ line = line[2:]
+ }
+ // empty values are not supported: "Key: \n"
+ lines = append(lines, bytes.TrimRight(line, " \t"))
+ }
+ c = new(Signed)
+ // last line is not closed by \n
+ c.Body = bytes.Join(lines, []byte("\n"))
+ // signature is just the rest of the input data
+ c.Signature = s
return
}
-func parse(s []byte, doctype string) (v interface{}, err error) {
- t, fields, err := ParseBody(s)
+// clean up, check and reencode signature
+// used on drafts before calculating the signed document hash
+func CleanSigned(c *Signed) (err error) {
+ b, err := armor.Decode(bytes.NewBuffer(c.Signature))
if err != nil {
return
}
- if doctype != t && doctype != "" {
- err = fmt.Errorf("parse: expected doctype %s; got %s", doctype, t)
+ if b.Type != openpgp.SignatureType {
+ err = fmt.Errorf("CleanSigned: invalid armored signature type")
return
}
- switch t {
- case "draft":
- v = new(Draft)
- case "cert":
- v = new(Cert)
- default:
- err = fmt.Errorf("parse: unkown doc type: %s", t)
+ p, err := packet.Read(b.Body)
+ if err != nil {
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"
+ sig, ok := p.(*packet.Signature)
+ if !ok {
+ err = fmt.Errorf("CleanSigned: invalid signature packet")
+ return
+ }
+ // section 5.2.3
+ if sig.SigType != packet.SigTypeText {
+ err = fmt.Errorf("CleanSigned: expected text signature")
+ return
+ }
+ switch sig.Hash {
+ case crypto.SHA1:
+ c.Hash = "SHA1"
+ case crypto.SHA256:
+ c.Hash = "SHA256"
default:
- panic("reder: unknown type")
+ err = fmt.Errorf("CleanSigned: expected SHA1 or SHA256 signature hash")
+ return
}
- s := "Content-Type: " + ContentType[doctype] + "\n\n"
- fields := marshal(v)
- for _, f := range fields {
- s += f.Key + ": " + f.Value + "\n"
+ // TODO: check CreationTime and other subpackets
+ if sig.SigLifetimeSecs != nil && *sig.SigLifetimeSecs != 0 {
+ err = fmt.Errorf("CleanSigned: signature must not expire")
+ return
}
- return []byte(s), nil
-}
-
-func ParseDraft(s []byte) (draft *Draft, err error) {
- v, err := parse(s, "draft")
+ out := new(bytes.Buffer)
+ w, err := armor.Encode(out, openpgp.SignatureType, nil)
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")
+ err = sig.Serialize(w)
if err != nil {
- return nil, err
+ return
}
- 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))
+ err = w.Close()
+ if err != nil {
+ return
}
- dst := make([]byte, len(s)/2)
- _, err := hex.Decode(dst, []byte(s))
- return s, err
-}
-
-func formatId(s string) string {
- return s
+ c.Signature = out.Bytes()
+ return
}
-func parseString(s string) (string, error) {
- if len(s) > 140 {
- return "", fmt.Errorf("parseString: 140 chars limit is exceeded")
+// create clear signed document
+func FormatSigned(c *Signed) (data []byte, err error) {
+ s := ClearSignedHeader + "\n"
+ if c.Hash != "" {
+ s += "Hash: " + c.Hash + "\n"
}
- return s, nil
-}
-
-func formatString(s string) string {
- return s
+ s += "\n"
+ s += string(c.Body)
+ s += "\n"
+ s += string(c.Signature)
+ data = []byte(s)
+ return
}
-func parseDate(s string) (int64, error) {
- t, err := time.Parse(time.RFC3339, s)
+// parse type and fields of a document body
+func ParseDocument(body []byte) (doc *Document, err error) {
+ // parse content type header first
+ fields, s, err := ParseFields(body)
if err != nil {
- return 0, err
+ return
}
- return t.Seconds(), nil
+ ctype, ok := fields["Content-Type"]
+ if len(fields) != 1 || !ok {
+ return nil, fmt.Errorf("ParseBody: expected a single Content-Type header field")
+ }
+ doc = new(Document)
+ for k, v := range ContentType {
+ if ctype == v {
+ doc.Type = k
+ break
+ }
+ }
+ if doc.Type == "" {
+ return nil, fmt.Errorf("ParseBody: unknown Content-Type: %s", ctype)
+ }
+ // TODO: doc.Order
+ doc.Fields, s, err = ParseFields(s)
+ if err == nil && len(s) > 0 {
+ err = fmt.Errorf("ParseBody: extra data after fields: %q", s)
+ }
+ return
}
-func formatDate(i int64) string {
- return time.SecondsToUTC(i).Format(time.RFC3339)
+// create document body
+func FormatDocument(doc *Document) (body []byte, err error) {
+ ctype, ok := ContentType[doc.Type]
+ if !ok {
+ err = fmt.Errorf("FormatDocument: unknown document type: %s", doc.Type)
+ return
+ }
+ s := "Content-Type: " + ctype + "\n\n"
+ for _, k := range doc.Order {
+ s += k + ": " + doc.Fields[k] + "\n"
+ }
+ return []byte(s), nil
}
-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")
+// parse doc fields into a struct according to the document type
+func ParseStruct(doc *Document) (iv interface{}, err error) {
+ switch doc.Type {
+ case "Draft":
+ iv = new(Draft)
+ case "Notice":
+ iv = new(Notice)
+ case "DebitCert":
+ iv = new(DebitCert)
+ case "CreditCert":
+ iv = new(CreditCert)
+ case "BounceCert":
+ iv = new(BounceCert)
+ default:
+ err = fmt.Errorf("ParseStruct: unkown doc type: %s", doc.Type)
+ return
}
- v = v.Elem()
+ seen := make(map[string]bool)
+ v := reflect.ValueOf(iv).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
+ key := fieldname[ft.Name]
+ if key == "" {
+ key = ft.Name
}
- if fields[i].Key != k {
- return fmt.Errorf("unmarshal: field %d of %s (%s) is missing\n", i+1, t.Name(), k)
+ seen[key] = true
+ s, ok := doc.Fields[key]
+ if !ok {
+ return nil, fmt.Errorf("ParseStruct: field %s of %s is missing\n", key, t.Name())
}
- s := fields[i].Value
- var err error
- switch fv.Kind() {
- case reflect.String:
+ switch fieldtype[key] {
+ case "id":
var val string
- switch m {
- case "id":
- val, err = parseId(s)
- case "":
- val, err = parseString(s)
- default:
- panic("bad string field tag")
- }
+ val, err = parseId(s)
fv.SetString(val)
- case reflect.Int, reflect.Int32, reflect.Int64:
+ case "text":
+ var val string
+ val, err = parseString(s)
+ fv.SetString(val)
+ case "int":
var val int64
- switch m {
- case "date":
- val, err = parseDate(s)
- case "":
- val, err = strconv.Atoi64(s)
- default:
- panic("bad int field tag")
- }
+ val, err = strconv.Atoi64(s)
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
- }
+ case "date":
+ var val int64
+ val, err = parseDate(s)
+ fv.SetInt(val)
+ case "ids":
+ ids := strings.Split(s, " ")
+ val := make([]string, len(ids))
+ for j, id := range ids {
+ val[j], err = parseId(id)
+ if err != nil {
+ return
}
- default:
- panic("bad slice field tag")
}
fv.Set(reflect.ValueOf(val))
default:
- panic("bad field type")
+ panic("bad field type " + key + " " + fieldtype[key])
}
if err != nil {
- return err
+ return
+ }
+ }
+ if len(doc.Fields) != n {
+ for k := range doc.Fields {
+ if !seen[k] {
+ err = fmt.Errorf("ParseStruct: unknown field %s in %s", k, t.Name())
+ return
+ }
}
}
- return nil
+ return
}
-func marshal(iv interface{}) []Field {
+// turn a struct into a document
+func FormatStruct(iv interface{}) (doc *Document, err 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")
+ panic("input is not a pointer to struct")
}
v = v.Elem()
t := v.Type()
n := v.NumField()
- fields := []Field{}
+ doc = new(Document)
+ doc.Type = t.Name()
+ doc.Fields = make(map[string]string)
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
+ key := fieldname[ft.Name]
+ if key == "" {
+ key = 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")
+ switch fieldtype[key] {
+ case "id":
+ val = formatId(fv.String())
+ case "text":
+ val = formatString(fv.String())
+ case "int":
+ val = strconv.Itoa64(fv.Int())
+ case "date":
+ val = formatDate(fv.Int())
+ case "ids":
+ k := fv.Len()
+ for j := 0; j < k; j++ {
+ if j > 0 {
+ val += "\n "
}
- 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")
+ val += formatId(fv.Index(j).String())
}
default:
- panic("bad field type")
+ panic("bad field type " + key + " " + fieldtype[key])
}
- fields = append(fields, Field{k, val})
+ doc.Fields[key] = val
+ doc.Order = append(doc.Order, key)
}
- return fields
+ return
}
-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--
+func ParseFields(s []byte) (fields map[string]string, rest []byte, err error) {
+ rest = s
+ fields = make(map[string]string)
+ lastkey := ""
+ for len(rest) > 0 {
+ var line []byte
+ line, rest = getLine(rest)
+ // empty line after the parsed fields is consumed
+ if len(line) == 0 {
+ break
+ }
+ // TODO: empty line: " \n"
+ if line[0] == ' ' {
+ if lastkey == "" {
+ err = fmt.Errorf("ParseFields: expected a field, not ' '")
+ return
+ }
+ fields[lastkey] += string(line)
+ continue
+ }
+ // TODO: empty value: "Key: \n"
+ i := bytes.Index(line, []byte(": "))
+ if i < 0 {
+ err = fmt.Errorf("ParseFields: missing ': '")
+ break
+ }
+ lastkey = string(line[:i])
+ if _, ok := fields[lastkey]; ok {
+ err = fmt.Errorf("ParseFields: repeated fields are not allowed")
+ return
+ }
+ fields[lastkey] = string(line[i+2:])
}
- return data[:i], data[j:]
+ return
}
-func trimspace(s []byte) []byte {
- a := bytes.Split(s, []byte("\n"))
- for i := range a {
- a[i] = bytes.TrimRight(a[i], " \t\r")
+// TODO: limit errors
+
+func parseId(s string) (string, error) {
+ if len(s) != 40 {
+ return "", fmt.Errorf("parseId: expected 40 characters; got %d", len(s))
}
- return bytes.Join(a, []byte("\n"))
+ dst := make([]byte, len(s)/2)
+ _, err := hex.Decode(dst, []byte(s))
+ return s, err
}
-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 formatId(s string) string {
+ return s
}
-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:]
+func parseString(s string) (string, error) {
+ if len(s) > 140 {
+ return "", fmt.Errorf("parseString: 140 chars limit is exceeded")
}
- return r
+ return s, nil
}
-// 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
+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
}
- i := bytes.Index(s, []byte("\n-----BEGIN"))
+ return t.Seconds(), nil
+}
+
+func formatDate(i int64) string {
+ return time.SecondsToUTC(i).Format(time.RFC3339)
+}
+
+func getLine(data []byte) (line, rest []byte) {
+ i := bytes.Index(data, []byte{'\n'})
+ j := i + 1
if i < 0 {
- return
- }
- body, sig = s[:i], s[i+1:]
- if i > 0 && body[i-1] == '\r' {
- body = body[:i-1]
+ i = len(data)
+ j = i
+ } else if i > 0 && data[i-1] == '\r' {
+ i--
}
- return
+ return data[:i], data[j:]
}
"testing"
)
-const D1 = `-----BEGIN PGP SIGNED MESSAGE-----
+var signedData = []struct {
+ text string
+ ok bool
+ hash string
+ body string
+ sig string
+}{
+ {
+ `-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
-Content-Type: text/plain.epoint.cert; charset=utf-8
-
-A: foo
-B: bar
-a: baz
+body
-----BEGIN PGP SIGNATURE-----
-
sig
------END PGP SIGNATURE-----
-`
-const D2 = `-----BEGIN PGP SIGNED MESSAGE-----
+`, true, "SHA1", "body", "-----BEGIN PGP SIGNATURE-----\nsig\n"},
+ {
+ `-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
+Hash: SHA256
-Content-Type: text/plain.epoint.cert; charset=utf-8
-
-A: foo
-B: bar
-a: baz
-
+- body
-----BEGIN PGP SIGNATURE-----
-
-sig
------END PGP SIGNATURE-----
-`
-
-var C1 = &ClearSigned{
- Hash: "SHA1",
- Body: []byte(`Content-Type: text/plain.epoint.cert; charset=utf-8
-
-A: foo
-B: bar
-a: baz`),
- ArmoredSignature: []byte(`-----BEGIN PGP SIGNATURE-----
-
sig
------END PGP SIGNATURE-----
-`)}
-
-var C2 = &ClearSigned{
- Hash: "SHA1",
- Body: []byte(`Content-Type: text/plain.epoint.cert; charset=utf-8
-
-A: foo
-B: bar
-a: baz
-`),
- ArmoredSignature: []byte(`-----BEGIN PGP SIGNATURE-----
-
-sig
------END PGP SIGNATURE-----
-`)}
-
-var F = []Field{
- {"A", "foo"},
- {"B", "bar"},
- {"a", "baz"},
+`, false, "", "body", "-----BEGIN PGP SIGNATURE-----\nsig\n"},
}
-var testData = []struct {
- D []byte
- C *ClearSigned
- T string
- F []Field
-}{
- {[]byte(D1), C1, "cert", F},
- {[]byte(D2), C2, "cert", F},
-}
-
-func eqClearSigned(c1, c2 *ClearSigned) bool {
- return c1.Hash == c2.Hash &&
- string(c1.Body) == string(c2.Body) &&
- string(c1.ArmoredSignature) == string(c2.ArmoredSignature)
-}
-
-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 true
-}
-
-func TestClearSigned(t *testing.T) {
- for _, x := range testData {
- c, err := ParseClearSigned(x.D)
+func TestSigned(t *testing.T) {
+ for _, x := range signedData {
+ c, err := ParseSigned([]byte(x.text))
if err != nil {
- t.Errorf("decoding %#v failed: %s\n", x.D, err)
+ t.Errorf("parsing %q failed: %s\n", x.text, err)
continue
}
- if !eqClearSigned(c, x.C) {
- t.Errorf("expected: %#v, got %#v\n", x.C, c)
+ if string(c.Body) != x.body {
+ t.Errorf("expected: %q, got %q\n", x.body, c.Body)
+ }
+ if string(c.Signature) != x.sig {
+ t.Errorf("expected: %q, got %q\n", x.sig, c.Signature)
}
}
- for _, x := range testData {
- d, err := FormatClearSigned(x.C)
+ for _, x := range signedData {
+ if !x.ok {
+ continue
+ }
+ c := &Signed{x.hash, []byte(x.body), []byte(x.sig)}
+ s, err := FormatSigned(c)
if err != nil {
- t.Errorf("encoding %#v failed: %s\n", x.C, err)
+ t.Errorf("formating %#v failed: %s\n", c, err)
continue
}
- if string(d) != string(x.D) {
- t.Errorf("expected: %#v, got %#v\n", x.D, d)
+ if string(s) != x.text {
+ t.Errorf("expected: %q, got %q\n", x.text, s)
}
}
}
-func TestParse(t *testing.T) {
- for _, x := range testData {
- tt, f, err := ParseBody(x.C.Body)
+var docData = []struct {
+ text string
+ ok bool
+ t string
+ k []string
+ v []string
+}{
+ {
+ `Content-Type: text/vnd.epoint.draft; charset=utf-8
+
+K1: v1
+K2-Foo: v 2
+K3: v 3
+`, true, "Draft", []string{"K1", "K2-Foo", "K3"}, []string{"v1", "v 2", "v 3"}},
+ {
+ `Content-Type: text/vnd.epoint.debit; charset=utf-8
+
+K1: v1
+K2-Foo: v 2
+K3: v 3
+`, true, "DebitCert", []string{"K1", "K2-Foo", "K3"}, []string{" v1", "v 2", "v 3"}},
+ {
+ `Content-Type: text/vnd.epoint.credit; charset=utf-8
+
+K1: v1
+K2-Foo: v 2
+K3: v
+ 3
+`, false, "CreditCert", []string{"K1", "K2-Foo", "K3"}, []string{"v1", "v 2", "v 3"}},
+}
+
+func TestDocument(t *testing.T) {
+ for _, x := range docData {
+ doc, err := ParseDocument([]byte(x.text))
if err != nil {
- t.Errorf("parsing %s failed: %s\n", x.C.Body, err)
+ t.Errorf("parsing %q failed: %s\n", x.text, err)
+ continue
+ }
+ if string(doc.Type) != x.t {
+ t.Errorf("expected: %q, got %q\n", x.t, doc.Type)
+ }
+ if len(doc.Fields) != len(x.k) {
+ t.Errorf("expected: %d fields, got %d\n", len(x.k), len(doc.Fields))
+ }
+ for i, k := range x.k {
+ if doc.Fields[k] != x.v[i] {
+ t.Errorf("value of %s expected to be %s, got %s\n", k, x.v[i], doc.Fields[k])
+ }
+ }
+ }
+ for _, x := range docData {
+ if !x.ok {
continue
}
- if !eqFields(f, x.F) {
- t.Errorf("expected fields %#v, got %#v\n", x.F, f)
+ doc := new(Document)
+ doc.Type = x.t
+ doc.Fields = make(map[string]string)
+ doc.Order = x.k
+ for i, k := range x.k {
+ doc.Fields[k] = x.v[i]
}
- if tt != x.T {
- t.Errorf("expected type %s, got %s\n", x.T, tt)
+ s, err := FormatDocument(doc)
+ if err != nil {
+ t.Errorf("formating %#v failed: %s\n", doc, err)
+ continue
+ }
+ if string(s) != x.text {
+ t.Errorf("expected: %q, got %q\n", x.text, s)
}
}
}
-const draftBody = `Content-Type: text/plain.epoint.draft; charset=utf-8
+const draftBody = `Content-Type: text/vnd.epoint.draft; charset=utf-8
Drawer: 000000000000000000000000000000000000000A
Beneficiary: 000000000000000000000000000000000000000B
Denomination: half euro
Issuer: 000000000000000000000000000000000000000D
Authorized-By: 000000000000000000000000000000000000000C
-Date: 2011-11-13T12:20:35Z
Maturity-Date: 2011-11-13T12:20:35Z
Expiry-Date: 2011-12-27T09:18:46Z
-Notes: some notes
Nonce: 42
+Notes: some notes
`
func TestDraft(t *testing.T) {
- d, err := ParseDraft([]byte(draftBody))
+ doc, err := ParseDocument([]byte(draftBody))
if err != nil {
- t.Errorf("ParseDraft failed: %s\n", err)
+ t.Errorf("ParseDocument failed: %s\n", err)
return
}
- s, err := FormatDraft(d)
+ iv, err := ParseStruct(doc)
if err != nil {
- t.Errorf("render %v draft failed: %s\n", d, err)
+ t.Errorf("ParseStruct %v failed: %s\n", doc, err)
+ return
+ }
+ d, ok := iv.(*Draft)
+ if !ok {
+ t.Errorf("expected *Draft got %#v\n", iv)
+ return
+ }
+ doc, err = FormatStruct(d)
+ if err != nil {
+ t.Errorf("format %v draft failed: %s\n", d, err)
+ return
+ }
+ s, err := FormatDocument(doc)
+ if err != nil {
+ t.Errorf("format %v doc failed: %s\n", doc, err)
+ return
}
if string(s) != draftBody {
t.Errorf("parsed %#v\nexpected: %s\ngot: %s\n", d, draftBody, s)
}
}
-const certBody = `Content-Type: text/plain.epoint.cert; charset=utf-8
+const debitBody = `Content-Type: text/vnd.epoint.debit; charset=utf-8
Holder: 0000000000000000000000000000000000000009
Serial: 13
-Date: 2011-11-01T10:29:38Z
Balance: 23
Denomination: half euro
Issuer: 000000000000000000000000000000000000000B
+Date: 2011-11-13T12:20:35Z
+Difference: 1
+Draft: 000000000000000000000000000000000000000D
Authorized-By: 000000000000000000000000000000000000000A
+Notes: -
Last-Debit-Serial: 0
Last-Credit-Serial: 12
Last-Cert: 000000000000000000000000000000000000000C
-Difference: 1
-Draft: 000000000000000000000000000000000000000D
-Drawer: 000000000000000000000000000000000000000E
-Drawer-Cert: 000000000000000000000000000000000000000F
-Notes: -
-References: 000000000000000000000000000000000000000C 000000000000000000000000000000000000000F
+References: 000000000000000000000000000000000000000C
+ 000000000000000000000000000000000000000F
`
func TestCert(t *testing.T) {
- c, err := ParseCert([]byte(certBody))
+ doc, err := ParseDocument([]byte(debitBody))
if err != nil {
- t.Errorf("ParseCert failed: %s\n", err)
+ t.Errorf("ParseDocument failed: %s\n", err)
return
}
- s, err := FormatCert(c)
+ iv, err := ParseStruct(doc)
if err != nil {
- t.Errorf("render %v cert failed: %s\n", c, err)
+ t.Errorf("ParseStruct %v failed: %s\n", doc, err)
+ return
+ }
+ d, ok := iv.(*DebitCert)
+ if !ok {
+ t.Errorf("expected *DebitCert got %#v\n", iv)
+ return
+ }
+ doc, err = FormatStruct(d)
+ if err != nil {
+ t.Errorf("format %v draft failed: %s\n", d, err)
+ return
+ }
+ s, err := FormatDocument(doc)
+ if err != nil {
+ t.Errorf("format %v doc failed: %s\n", doc, err)
+ return
}
- if string(s) != certBody {
- t.Errorf("parsed %#v\nexpected: %s\ngot: %s\n", c, certBody, s)
+ if string(s) != debitBody {
+ t.Errorf("parsed %#v\nexpected: %s\ngot: %s\n", d, debitBody, s)
}
}