1 // Package document implements epoint document parsing and creation.
3 // An epoint document is an OpenPGP (RFC 4880) clear signed
4 // utf-8 text of key-value pairs.
5 // The body contains a content-type MIME header so the document
6 // can be used in OpenPGP/MIME (RFC 3156) emails.
7 // The format of the key-value pairs are similar to MIME header
8 // fields: keys and values are separated by ": ", repeated keys
9 // are not allowed, long values can be split before a space.
13 // -----BEGIN PGP SIGNED MESSAGE-----
16 // Content-Type: text/plain.epoint.type; charset=utf-8
19 // Another-Key: Value2
23 // -----BEGIN PGP SIGNATURE-----
26 // -----END PGP SIGNATURE-----
29 // TODO: error wrapper (so reporting to user or creating bounce cert is simple)
30 // TODO: optional fields: exact semantics ("" vs "-" vs nil)
31 // TODO: trailing space handling in ParseFields
32 // TODO: fields of notice (last notice, serial, failure notice,..)
33 // TODO: limits and cert type specific input validation
34 // TODO: fix Cert mess
35 // TODO: nonce is id, id is even number of hex digits (require only drawer.nonce to be uniq)
36 // TODO: denom, issuer from key (key representation: armor?)
42 "crypto/openpgp/armor"
43 "crypto/openpgp/packet"
56 MaxLineLength = 160 // 1 sha512 + 1 key (without \n)
57 MaxValueLength = 1300 // 20 sha256 space separated (without \n)
59 MaxDenominationLength = 100
62 const ClearSignedHeader = "-----BEGIN PGP SIGNED MESSAGE-----"
64 // MIME type for epoint documents, see RFC 4288
65 var ContentType = map[string]string{
66 "Draft": "text/vnd.epoint.draft; charset=utf-8",
67 "Notice": "text/vnd.epoint.notice; charset=utf-8",
68 "DebitCert": "text/vnd.epoint.debit; charset=utf-8",
69 "CreditCert": "text/vnd.epoint.credit; charset=utf-8",
70 "BounceCert": "text/vnd.epoint.bounce; charset=utf-8",
73 // OpenPGP signed cleartext document representation
75 // Sign and CleanSigned sets Hash for FormatSigned
78 // Signed text (no dash escape, no trailing space, \n new lines)
80 // Armored detached text signature of the Body
84 // parsed epoint document
85 type Document struct {
87 Fields map[string]string
91 var fieldtype = map[string]string{
93 "Authorized-By": "id",
98 "Denomination": "text",
102 "Expiry-Date": "date",
106 "Last-Credit-Serial": "int",
107 "Last-Debit-Serial": "int",
108 "Maturity-Date": "date",
115 var fieldname = map[string]string{
116 "AuthorizedBy": "Authorized-By",
117 "DebitCert": "Debit-Cert",
118 "ExpiryDate": "Expiry-Date",
119 "LastCert": "Last-Cert",
120 "LastCreditSerial": "Last-Credit-Serial",
121 "LastDebitSerial": "Last-Debit-Serial",
122 "MaturityDate": "Maturity-Date",
132 MaturityDate *int64 // optional
133 ExpiryDate *int64 // optional
135 Notes *string // optional
141 Notes *string // optional
142 References []string // may be empty (startup notice)
153 Notes *string // optional
154 LastDebitSerial int64 // 0 if none
155 LastCreditSerial int64 // 0 if none
156 LastCert *string // nil if serial == 1
162 type DebitCert struct {
167 type CreditCert struct {
173 type BounceCert struct {
176 LastCert *string // optional
177 Balance int64 // 0 if none
180 Notes *string // optional
184 func ToCert(v interface{}) (cert *Cert, err error) {
186 switch x := v.(type) {
192 err = fmt.Errorf("ToCert: only debit or credit document can be converted to cert")
197 func cleanBody(s []byte) []byte {
199 a := bytes.Split(s, nl)
201 a[i] = bytes.TrimRight(a[i], " \t")
203 return bytes.Join(a, nl)
206 // sha1 sum of the (cleaned) document body as uppercase hex string
207 func Id(c *Signed) string {
210 return fmt.Sprintf("%040X", h.Sum())
213 // parse an epoint document without checking the signature and format details
214 func Parse(s []byte) (iv interface{}, c *Signed, err error) {
215 c, err = ParseSigned(s)
219 doc, err := ParseDocument(c.Body)
223 iv, err = ParseStruct(doc)
227 // format and sign an epoint document
228 func Format(iv interface{}, key *openpgp.Entity) (s []byte, c *Signed, err error) {
229 doc, err := FormatStruct(iv)
233 body, err := FormatDocument(doc)
237 c, err = Sign(body, key)
241 s, err = FormatSigned(c)
245 // verify an epoint document, return the cleaned version as well
246 func Verify(c *Signed, key openpgp.KeyRing) (err error) {
247 msg := bytes.NewBuffer(c.Body)
248 sig := bytes.NewBuffer(c.Signature)
249 // TODO: verify signature
251 // _, err = openpgp.CheckArmoredDetachedSignature(key, msg, sig)
255 // sign body with given secret key
256 func Sign(body []byte, key *openpgp.Entity) (c *Signed, err error) {
259 c.Body = cleanBody(body)
260 w := new(bytes.Buffer)
261 err = openpgp.ArmoredDetachSignText(w, key, bytes.NewBuffer(c.Body))
265 // close armored document with a \n
266 _, _ = w.Write([]byte{'\n'})
267 c.Signature = w.Bytes()
271 // split a clear signed document into body and armored signature
272 func ParseSigned(s []byte) (c *Signed, err error) {
273 // look for clear signed header
274 for !bytes.HasPrefix(s, []byte(ClearSignedHeader)) {
277 err = fmt.Errorf("ParseSigned: clear signed header is missing")
281 s = s[len(ClearSignedHeader):]
282 // end of line after the header
283 empty, s := getLine(s)
285 err = fmt.Errorf("ParseSigned: bad clear signed header")
288 // skip all hash headers, section 7.
289 for bytes.HasPrefix(s, []byte("Hash: ")) {
293 empty, s = getLine(s)
295 err = fmt.Errorf("ParseSigned: expected an empty line after armor headers")
299 for !bytes.HasPrefix(s, []byte("-----BEGIN")) {
302 // dash unescape, section 7.1.
303 if bytes.HasPrefix(line, []byte("- ")) {
306 // empty values are not supported: "Key: \n"
307 lines = append(lines, bytes.TrimRight(line, " \t"))
310 // last line is not closed by \n
311 c.Body = bytes.Join(lines, []byte("\n"))
312 // signature is just the rest of the input data
317 // clean up, check and reencode signature
318 // used on drafts before calculating the signed document hash
319 func CleanSigned(c *Signed) (err error) {
320 b, err := armor.Decode(bytes.NewBuffer(c.Signature))
324 if b.Type != openpgp.SignatureType {
325 err = fmt.Errorf("CleanSigned: invalid armored signature type")
328 p, err := packet.Read(b.Body)
332 sig, ok := p.(*packet.Signature)
334 err = fmt.Errorf("CleanSigned: invalid signature packet")
338 if sig.SigType != packet.SigTypeText {
339 err = fmt.Errorf("CleanSigned: expected text signature")
348 err = fmt.Errorf("CleanSigned: expected SHA1 or SHA256 signature hash")
351 // TODO: check CreationTime and other subpackets
352 if sig.SigLifetimeSecs != nil && *sig.SigLifetimeSecs != 0 {
353 err = fmt.Errorf("CleanSigned: signature must not expire")
356 out := new(bytes.Buffer)
357 w, err := armor.Encode(out, openpgp.SignatureType, nil)
361 err = sig.Serialize(w)
369 c.Signature = out.Bytes()
373 // create clear signed document
374 func FormatSigned(c *Signed) (data []byte, err error) {
375 s := ClearSignedHeader + "\n"
377 s += "Hash: " + c.Hash + "\n"
382 s += string(c.Signature)
387 // parse type and fields of a document body
388 func ParseDocument(body []byte) (doc *Document, err error) {
389 // parse content type header first
390 fields, s, err := ParseFields(body)
394 ctype, ok := fields["Content-Type"]
395 if len(fields) != 1 || !ok {
396 return nil, fmt.Errorf("ParseBody: expected a single Content-Type header field")
399 for k, v := range ContentType {
406 return nil, fmt.Errorf("ParseBody: unknown Content-Type: %s", ctype)
409 doc.Fields, s, err = ParseFields(s)
410 if err == nil && len(s) > 0 {
411 err = fmt.Errorf("ParseBody: extra data after fields: %q", s)
416 // create document body
417 func FormatDocument(doc *Document) (body []byte, err error) {
418 ctype, ok := ContentType[doc.Type]
420 err = fmt.Errorf("FormatDocument: unknown document type: %s", doc.Type)
423 s := "Content-Type: " + ctype + "\n\n"
424 for _, k := range doc.Order {
425 s += k + ": " + doc.Fields[k] + "\n"
427 return []byte(s), nil
430 // parse doc fields into a struct according to the document type
431 func parseStruct(v reflect.Value, fields map[string]string, seen map[string]bool) (err error) {
434 for i := 0; i < n && err == nil; i++ {
437 if ft.Anonymous && fv.Kind() == reflect.Struct {
438 err = parseStruct(fv, fields, seen)
441 key := fieldname[ft.Name]
447 if fv.Kind() == reflect.Ptr {
448 // missing optional key: leave the pointer as nil
451 return fmt.Errorf("ParseStruct: field %s of %s is missing\n", key, t.Name())
454 if fv.Kind() == reflect.Ptr {
455 if s == "" || s == "-" {
457 // empty optional key: same as missing
460 fv.Set(reflect.New(fv.Type().Elem()))
463 switch fieldtype[key] {
466 val, err = parseId(s)
470 val, err = parseString(s)
474 val, err = strconv.Atoi64(s)
478 val, err = parseDate(s)
481 // TODO: empty slice?
482 ids := strings.Split(s, " ")
483 val := make([]string, len(ids))
484 for j, id := range ids {
485 val[j], err = parseId(id)
490 fv.Set(reflect.ValueOf(val))
492 panic("bad field type " + key + " " + fieldtype[key])
498 func ParseStruct(doc *Document) (iv interface{}, err error) {
511 err = fmt.Errorf("ParseStruct: unkown doc type: %s", doc.Type)
514 seen := make(map[string]bool)
515 err = parseStruct(reflect.ValueOf(iv).Elem(), doc.Fields, seen)
519 if len(doc.Fields) != len(seen) {
520 for f := range doc.Fields {
522 err = fmt.Errorf("ParseStruct: unknown field %s in %s", f, doc.Type)
530 // turn a struct into a document
531 func formatStruct(v reflect.Value, doc *Document) (err error) {
534 for i := 0; i < n; i++ {
537 if ft.Anonymous && fv.Kind() == reflect.Struct {
538 err = formatStruct(fv, doc)
544 key := fieldname[ft.Name]
549 if fv.Kind() == reflect.Ptr {
551 // keep empty optional fields but mark them
557 switch fieldtype[key] {
559 val = formatId(fv.String())
561 val = formatString(fv.String())
563 val = strconv.Itoa64(fv.Int())
565 val = formatDate(fv.Int())
568 for j := 0; j < k; j++ {
572 val += formatId(fv.Index(j).String())
575 panic("bad field type " + key + " " + fieldtype[key])
578 doc.Fields[key] = val
579 doc.Order = append(doc.Order, key)
584 // turn a struct into a document
585 func FormatStruct(iv interface{}) (doc *Document, err error) {
586 v := reflect.ValueOf(iv)
587 if v.Kind() != reflect.Ptr || v.IsNil() || v.Elem().Kind() != reflect.Struct {
588 panic("input is not a pointer to struct")
591 doc.Type = v.Elem().Type().Name()
592 doc.Fields = make(map[string]string)
593 err = formatStruct(v.Elem(), doc)
597 func ParseFields(s []byte) (fields map[string]string, rest []byte, err error) {
599 fields = make(map[string]string)
601 // \n is optional after the last field and an extra \n is allowed as well
604 line, rest = getLine(rest)
605 // empty line after the last field is consumed
609 if line[0] == ' ' && key != "" {
610 // "Key: v1\n v2\n" is equivalent to "Key: v1 v2\n"
611 fields[key] += string(line)
614 if line[0] < 'A' || line[0] > 'Z' {
615 err = fmt.Errorf("ParseFields: field name must start with an upper-case ascii letter")
618 i := bytes.IndexByte(line, ':')
620 err = fmt.Errorf("ParseFields: missing ':'")
623 key = string(line[:i])
624 if _, ok := fields[key]; ok {
625 err = fmt.Errorf("ParseFields: repeated fields are not allowed")
628 fields[key] = string(line[i+1:])
630 for key, v := range fields {
631 // either a single space follows ':' or the value is empty
632 // good: "Key:\n", "Key:\n value\n", "Key: value\n", "Key: v1\n v2\n"
633 // bad: "Key:value\n", "Key: \nvalue\n"
634 // bad but not checked here: "Key: \n", "Key: value \n", "Key:\n \n value\n"
639 err = fmt.Errorf("ParseFields: ':' is not followed by ' '")
647 // TODO: limit errors
649 func parseId(s string) (string, error) {
650 // check if hex decodable
651 // TODO: length check
652 dst := make([]byte, len(s)/2)
653 _, err := hex.Decode(dst, []byte(s))
657 func formatId(s string) string {
661 func parseString(s string) (string, error) {
662 if len(s) > MaxValueLength {
663 return "", fmt.Errorf("parseString: length limit is exceeded")
668 func formatString(s string) string {
672 func parseDate(s string) (int64, error) {
673 // TODO: fractional seconds?
674 t, err := time.Parse(time.RFC3339, s)
678 return t.Seconds(), nil
681 func formatDate(i int64) string {
682 return time.SecondsToUTC(i).Format(time.RFC3339)
685 func getLine(data []byte) (line, rest []byte) {
686 i := bytes.IndexByte(data, '\n')
691 } else if i > 0 && data[i-1] == '\r' {
694 return data[:i], data[j:]