From: nsz Date: Tue, 22 Nov 2011 22:59:10 +0000 (+0100) Subject: document rewrite X-Git-Url: http://nsz.repo.hu/git/?p=epoint;a=commitdiff_plain;h=75502b9220b35cdc4fe15977f8aeb794535bcd35 document rewrite --- diff --git a/document/document.go b/document/document.go index 1c61fcb..c410f61 100644 --- a/document/document.go +++ b/document/document.go @@ -1,26 +1,42 @@ -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" @@ -29,464 +45,567 @@ import ( "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:] } diff --git a/document/document_test.go b/document/document_test.go index 6113fac..024b0bd 100644 --- a/document/document_test.go +++ b/document/document_test.go @@ -4,135 +4,135 @@ import ( "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 @@ -140,59 +140,89 @@ Amount: 1 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) } }