// 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.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 wrapper (so reporting to user or creating bounce cert is simple) // TODO: optional fields: exact semantics ("" vs "-" vs nil) // TODO: trailing space handling in ParseFields // TODO: fields of notice (last notice, serial, failure notice,..) // TODO: limits and cert type specific input validation // TODO: fix Cert mess // TODO: nonce is id, id is even number of hex digits // TODO: denom, issuer from key (key representation: armor?) import ( "bytes" "crypto" "crypto/openpgp" "crypto/openpgp/armor" "crypto/openpgp/packet" "crypto/sha1" "encoding/hex" "fmt" "reflect" "strconv" "strings" "time" ) // limits const ( MaxFields = 20 MaxLineLength = 160 // 1 sha512 + 1 key (without \n) MaxValueLength = 1300 // 20 sha256 space separated (without \n) MaxNonceLength = 20 MaxDenominationLength = 100 ) const ClearSignedHeader = "-----BEGIN PGP SIGNED MESSAGE-----" // MIME type for epoint documents, see RFC 4288 var ContentType = map[string]string{ "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 Signed struct { // Sign and CleanSigned sets Hash for FormatSigned // TODO: CreationDate Hash string // Signed text (no dash escape, no trailing space, \n new lines) Body []byte // Armored detached text signature of the Body Signature []byte } // parsed epoint document type Document struct { Type string Fields map[string]string Order []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": "id", "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", } type Draft struct { Drawer string Beneficiary string Amount int64 Denomination string Issuer string AuthorizedBy string MaturityDate *int64 // optional ExpiryDate *int64 // optional Nonce string Notes *string // optional } type Notice struct { Date int64 AuthorizedBy string Notes *string // optional References []string // may be empty (startup notice) } type Cert struct { IsDebit bool Holder string Serial int64 Balance int64 Denomination string Issuer string Date int64 Difference int64 Draft string Beneficiary *string // only in debit cert Drawer *string // only in credit cert DebitCert *string // only in credit cert AuthorizedBy string Notes *string // optional LastDebitSerial int64 // 0 if none LastCreditSerial int64 // 0 if none LastCert *string // nil if serial == 1 References []string } type DebitCert struct { Holder string Serial int64 Balance int64 Denomination string Issuer string Date int64 Difference int64 Draft string Beneficiary string AuthorizedBy string Notes *string // optional LastDebitSerial int64 // 0 if none LastCreditSerial int64 // 0 if none LastCert *string // nil if serial == 1 References []string } 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 } type BounceCert struct { Drawer string Draft string LastCert *string // optional Balance int64 // 0 if none Date int64 AuthorizedBy string Notes *string // optional References []string } func ToCert(v interface{}) (cert *Cert, err error) { cert = new(Cert) switch x := v.(type) { case *DebitCert: cert.IsDebit = true cert.Beneficiary = &x.Beneficiary cert.Holder = x.Holder cert.Serial = x.Serial cert.Balance = x.Balance cert.Denomination = x.Denomination cert.Issuer = x.Issuer cert.Date = x.Date cert.Difference = x.Difference cert.Draft = x.Draft cert.AuthorizedBy = x.AuthorizedBy cert.Notes = x.Notes cert.LastDebitSerial = x.LastDebitSerial cert.LastCreditSerial = x.LastCreditSerial cert.LastCert = x.LastCert cert.References = x.References case *CreditCert: cert.IsDebit = false cert.Drawer = &x.Drawer cert.DebitCert = &x.DebitCert cert.Holder = x.Holder cert.Serial = x.Serial cert.Balance = x.Balance cert.Denomination = x.Denomination cert.Issuer = x.Issuer cert.Date = x.Date cert.Difference = x.Difference cert.Draft = x.Draft cert.AuthorizedBy = x.AuthorizedBy cert.Notes = x.Notes cert.LastDebitSerial = x.LastDebitSerial cert.LastCreditSerial = x.LastCreditSerial cert.LastCert = x.LastCert cert.References = x.References default: err = fmt.Errorf("ToCert: only debit or credit document can be converted to cert") } return } // sha1 sum of the (cleaned) document body as uppercase hex string func Id(c *Signed) string { h := sha1.New() h.Write(c.Body) return fmt.Sprintf("%040X", h.Sum()) } // 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 } doc, err := ParseDocument(c.Body) if err != nil { return } iv, err = ParseStruct(doc) return } // format and sign an epoint document func Format(iv interface{}, key *openpgp.Entity) (s []byte, c *Signed, err error) { doc, err := FormatStruct(iv) if err != nil { return } body, err := FormatDocument(doc) if err != nil { return } c, err = Sign(body, key) if err != nil { return } s, err = FormatSigned(c) return } // verify an epoint document, return the cleaned version as well func Verify(c *Signed, key openpgp.KeyRing) (err error) { msg := bytes.NewBuffer(c.Body) sig := bytes.NewBuffer(c.Signature) // TODO: verify signature _, _ = msg, sig // _, err = openpgp.CheckArmoredDetachedSignature(key, 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)) if err != nil { return } // close armored document with a \n _, _ = w.Write([]byte{'\n'}) 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 } } 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 } // 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 } // 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 b.Type != openpgp.SignatureType { err = fmt.Errorf("CleanSigned: invalid armored signature type") return } p, err := packet.Read(b.Body) if err != nil { return } 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: err = fmt.Errorf("CleanSigned: expected SHA1 or SHA256 signature hash") return } // TODO: check CreationTime and other subpackets if sig.SigLifetimeSecs != nil && *sig.SigLifetimeSecs != 0 { err = fmt.Errorf("CleanSigned: signature must not expire") return } out := new(bytes.Buffer) w, err := armor.Encode(out, openpgp.SignatureType, nil) if err != nil { return } err = sig.Serialize(w) if err != nil { return } err = w.Close() if err != nil { return } c.Signature = out.Bytes() return } // create clear signed document func FormatSigned(c *Signed) (data []byte, err error) { s := ClearSignedHeader + "\n" if c.Hash != "" { s += "Hash: " + c.Hash + "\n" } s += "\n" s += string(c.Body) s += "\n" s += string(c.Signature) data = []byte(s) return } // 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 } 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 } // 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 } // 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 } seen := make(map[string]bool) v := reflect.ValueOf(iv).Elem() t := v.Type() n := v.NumField() nokey := 0 for i := 0; i < n; i++ { ft := t.Field(i) fv := v.Field(i) key := fieldname[ft.Name] if key == "" { key = ft.Name } seen[key] = true s, ok := doc.Fields[key] if !ok { if fv.Kind() == reflect.Ptr { // missing optional key: leave the pointer as nil nokey++ continue } return nil, fmt.Errorf("ParseStruct: field %s of %s is missing\n", key, t.Name()) } if fv.Kind() == reflect.Ptr { if s == "" || s == "-" { // TODO // empty optional key: same as missing continue } fv.Set(reflect.New(fv.Type().Elem())) fv = fv.Elem() } switch fieldtype[key] { case "id": var val string val, err = parseId(s) fv.SetString(val) case "text": var val string val, err = parseString(s) fv.SetString(val) case "int": var val int64 val, err = strconv.Atoi64(s) fv.SetInt(val) case "date": var val int64 val, err = parseDate(s) fv.SetInt(val) case "ids": // TODO: empty slice? ids := strings.Split(s, " ") val := make([]string, len(ids)) for j, id := range ids { val[j], err = parseId(id) if err != nil { return } } fv.Set(reflect.ValueOf(val)) default: panic("bad field type " + key + " " + fieldtype[key]) } if err != nil { return } } if len(doc.Fields)+nokey != n { for k := range doc.Fields { if !seen[k] { err = fmt.Errorf("ParseStruct: unknown field %s in %s", k, t.Name()) return } } } return } // 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("input is not a pointer to struct") } v = v.Elem() t := v.Type() n := v.NumField() 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) key := fieldname[ft.Name] if key == "" { key = ft.Name } val := "" if fv.Kind() == reflect.Ptr { if fv.IsNil() { // keep empty optional fields but mark them val = "-" goto setval } fv = fv.Elem() } 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 " } val += formatId(fv.Index(j).String()) } default: panic("bad field type " + key + " " + fieldtype[key]) } setval: doc.Fields[key] = val doc.Order = append(doc.Order, key) } return } func ParseFields(s []byte) (fields map[string]string, rest []byte, err error) { rest = s fields = make(map[string]string) key := "" // \n is optional after the last field and an extra \n is allowed as well for len(rest) > 0 { var line []byte line, rest = getLine(rest) // empty line after the last field is consumed if len(line) == 0 { break } if line[0] == ' ' && key != "" { // "Key: v1\n v2\n" is equivalent to "Key: v1 v2\n" fields[key] += string(line) continue } if line[0] < 'A' || line[0] > 'Z' { err = fmt.Errorf("ParseFields: field name must start with an upper-case ascii letter") return } i := bytes.IndexByte(line, ':') if i < 0 { err = fmt.Errorf("ParseFields: missing ':'") return } key = string(line[:i]) if _, ok := fields[key]; ok { err = fmt.Errorf("ParseFields: repeated fields are not allowed") return } fields[key] = string(line[i+1:]) } for key, v := range fields { // either a single space follows ':' or the value is empty // good: "Key:\n", "Key:\n value\n", "Key: value\n", "Key: v1\n v2\n" // bad: "Key:value\n", "Key: \nvalue\n" // bad but not checked here: "Key: \n", "Key: value \n", "Key:\n \n value\n" if len(v) == 0 { continue } if v[0] != ' ' { err = fmt.Errorf("ParseFields: ':' is not followed by ' '") return } fields[key] = v[1:] } return } // TODO: limit errors func parseId(s string) (string, error) { // check if hex decodable // TODO: length check 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) > MaxValueLength { return "", fmt.Errorf("parseString: length limit is exceeded") } return s, nil } func formatString(s string) string { return s } func parseDate(s string) (int64, error) { // TODO: fractional seconds? 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 getLine(data []byte) (line, rest []byte) { i := bytes.IndexByte(data, '\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:] }