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
40 "crypto/openpgp/armor"
41 "crypto/openpgp/packet"
54 MaxLineLength = 160 // 1 sha512 + 1 key (without \n)
55 MaxValueLength = 1300 // 20 sha256 space separated (without \n)
57 MaxDenominationLength = 100
60 const ClearSignedHeader = "-----BEGIN PGP SIGNED MESSAGE-----"
62 // MIME type for epoint documents, see RFC 4288
63 var ContentType = map[string]string{
64 "Draft": "text/vnd.epoint.draft; charset=utf-8",
65 "Notice": "text/vnd.epoint.notice; charset=utf-8",
66 "DebitCert": "text/vnd.epoint.debit; charset=utf-8",
67 "CreditCert": "text/vnd.epoint.credit; charset=utf-8",
68 "BounceCert": "text/vnd.epoint.bounce; charset=utf-8",
71 // OpenPGP signed cleartext document representation
73 // Sign and CleanSigned sets Hash for FormatSigned
76 // Signed text (no dash escape, no trailing space, \n new lines)
78 // Armored detached text signature of the Body
82 // parsed epoint document
83 type Document struct {
85 Fields map[string]string
89 var fieldtype = map[string]string{
91 "Authorized-By": "id",
96 "Denomination": "text",
100 "Expiry-Date": "date",
104 "Last-Credit-Serial": "int",
105 "Last-Debit-Serial": "int",
106 "Maturity-Date": "date",
113 var fieldname = map[string]string{
114 "AuthorizedBy": "Authorized-By",
115 "DebitCert": "Debit-Cert",
116 "ExpiryDate": "Expiry-Date",
117 "LastCert": "Last-Cert",
118 "LastCreditSerial": "Last-Credit-Serial",
119 "LastDebitSerial": "Last-Debit-Serial",
120 "MaturityDate": "Maturity-Date",
130 MaturityDate *int64 // optional
131 ExpiryDate *int64 // optional
132 Nonce *string // optional
133 Notes *string // optional
139 Notes *string // optional
140 References []string // may be empty (startup notice)
153 Beneficiary *string // only in debit cert
154 Drawer *string // only in credit cert
155 DebitCert *string // only in credit cert
157 Notes *string // optional
158 LastDebitSerial int64 // 0 if none
159 LastCreditSerial int64 // 0 if none
160 LastCert *string // nil if serial == 1
164 type DebitCert struct {
175 Notes *string // optional
176 LastDebitSerial int64 // 0 if none
177 LastCreditSerial int64 // 0 if none
178 LastCert *string // nil if serial == 1
182 type CreditCert struct {
194 Notes *string // optional
195 LastDebitSerial int64 // 0 if none
196 LastCreditSerial int64 // 0 if none
197 LastCert *string // ? if serial == 1
201 type BounceCert struct {
204 LastCert *string // optional
205 Balance int64 // 0 if none
208 Notes *string // optional
212 func ToCert(v interface{}) (cert *Cert, err error) {
214 switch x := v.(type) {
217 cert.Beneficiary = &x.Beneficiary
219 cert.Holder = x.Holder
220 cert.Serial = x.Serial
221 cert.Balance = x.Balance
222 cert.Denomination = x.Denomination
223 cert.Issuer = x.Issuer
225 cert.Difference = x.Difference
227 cert.AuthorizedBy = x.AuthorizedBy
229 cert.LastDebitSerial = x.LastDebitSerial
230 cert.LastCreditSerial = x.LastCreditSerial
231 cert.LastCert = x.LastCert
232 cert.References = x.References
236 cert.Drawer = &x.Drawer
237 cert.DebitCert = &x.DebitCert
239 cert.Holder = x.Holder
240 cert.Serial = x.Serial
241 cert.Balance = x.Balance
242 cert.Denomination = x.Denomination
243 cert.Issuer = x.Issuer
245 cert.Difference = x.Difference
247 cert.AuthorizedBy = x.AuthorizedBy
249 cert.LastDebitSerial = x.LastDebitSerial
250 cert.LastCreditSerial = x.LastCreditSerial
251 cert.LastCert = x.LastCert
252 cert.References = x.References
254 err = fmt.Errorf("ToCert: only debit or credit document can be converted to cert")
259 // sha1 sum of the (cleaned) document body as uppercase hex string
260 func Id(c *Signed) string {
263 return fmt.Sprintf("%040X", h.Sum())
266 // parse an epoint document without checking the signature and format details
267 func Parse(s []byte) (iv interface{}, c *Signed, err error) {
268 c, err = ParseSigned(s)
272 doc, err := ParseDocument(c.Body)
276 iv, err = ParseStruct(doc)
280 // format and sign an epoint document
281 func Format(iv interface{}, key *openpgp.Entity) (s []byte, c *Signed, err error) {
282 doc, err := FormatStruct(iv)
286 body, err := FormatDocument(doc)
290 c, err = Sign(body, key)
294 s, err = FormatSigned(c)
298 // verify an epoint document, return the cleaned version as well
299 func Verify(c *Signed, key openpgp.KeyRing) (err error) {
300 msg := bytes.NewBuffer(c.Body)
301 sig := bytes.NewBuffer(c.Signature)
302 // TODO: verify signature
304 // _, err = openpgp.CheckArmoredDetachedSignature(key, msg, sig)
308 // sign body with given secret key
309 func Sign(body []byte, key *openpgp.Entity) (c *Signed, err error) {
313 w := new(bytes.Buffer)
314 w.Write([]byte("\n-----BEGIN PGP SIGNATURE-----\n\nTODO: signature\n"))
315 // err = openpgp.ArmoredDetachSignText(w, key, bytes.NewBuffer(c.Body))
316 c.Signature = w.Bytes()
320 // split a clear signed document into body and armored signature
321 func ParseSigned(s []byte) (c *Signed, err error) {
322 // look for clear signed header
323 for !bytes.HasPrefix(s, []byte(ClearSignedHeader)) {
326 err = fmt.Errorf("ParseSigned: clear signed header is missing")
330 s = s[len(ClearSignedHeader):]
331 // end of line after the header
332 empty, s := getLine(s)
334 err = fmt.Errorf("ParseSigned: bad clear signed header")
337 // skip all hash headers, section 7.
338 for bytes.HasPrefix(s, []byte("Hash: ")) {
342 empty, s = getLine(s)
344 err = fmt.Errorf("ParseSigned: expected an empty line after armor headers")
348 for !bytes.HasPrefix(s, []byte("-----BEGIN")) {
351 // dash unescape, section 7.1.
352 if bytes.HasPrefix(line, []byte("- ")) {
355 // empty values are not supported: "Key: \n"
356 lines = append(lines, bytes.TrimRight(line, " \t"))
359 // last line is not closed by \n
360 c.Body = bytes.Join(lines, []byte("\n"))
361 // signature is just the rest of the input data
366 // clean up, check and reencode signature
367 // used on drafts before calculating the signed document hash
368 func CleanSigned(c *Signed) (err error) {
369 b, err := armor.Decode(bytes.NewBuffer(c.Signature))
373 if b.Type != openpgp.SignatureType {
374 err = fmt.Errorf("CleanSigned: invalid armored signature type")
377 p, err := packet.Read(b.Body)
381 sig, ok := p.(*packet.Signature)
383 err = fmt.Errorf("CleanSigned: invalid signature packet")
387 if sig.SigType != packet.SigTypeText {
388 err = fmt.Errorf("CleanSigned: expected text signature")
397 err = fmt.Errorf("CleanSigned: expected SHA1 or SHA256 signature hash")
400 // TODO: check CreationTime and other subpackets
401 if sig.SigLifetimeSecs != nil && *sig.SigLifetimeSecs != 0 {
402 err = fmt.Errorf("CleanSigned: signature must not expire")
405 out := new(bytes.Buffer)
406 w, err := armor.Encode(out, openpgp.SignatureType, nil)
410 err = sig.Serialize(w)
418 c.Signature = out.Bytes()
422 // create clear signed document
423 func FormatSigned(c *Signed) (data []byte, err error) {
424 s := ClearSignedHeader + "\n"
426 s += "Hash: " + c.Hash + "\n"
431 s += string(c.Signature)
436 // parse type and fields of a document body
437 func ParseDocument(body []byte) (doc *Document, err error) {
438 // parse content type header first
439 fields, s, err := ParseFields(body)
443 ctype, ok := fields["Content-Type"]
444 if len(fields) != 1 || !ok {
445 return nil, fmt.Errorf("ParseBody: expected a single Content-Type header field")
448 for k, v := range ContentType {
455 return nil, fmt.Errorf("ParseBody: unknown Content-Type: %s", ctype)
458 doc.Fields, s, err = ParseFields(s)
459 if err == nil && len(s) > 0 {
460 err = fmt.Errorf("ParseBody: extra data after fields: %q", s)
465 // create document body
466 func FormatDocument(doc *Document) (body []byte, err error) {
467 ctype, ok := ContentType[doc.Type]
469 err = fmt.Errorf("FormatDocument: unknown document type: %s", doc.Type)
472 s := "Content-Type: " + ctype + "\n\n"
473 for _, k := range doc.Order {
474 s += k + ": " + doc.Fields[k] + "\n"
476 return []byte(s), nil
479 // parse doc fields into a struct according to the document type
480 func ParseStruct(doc *Document) (iv interface{}, err error) {
493 err = fmt.Errorf("ParseStruct: unkown doc type: %s", doc.Type)
496 seen := make(map[string]bool)
497 v := reflect.ValueOf(iv).Elem()
501 for i := 0; i < n; i++ {
504 key := fieldname[ft.Name]
509 s, ok := doc.Fields[key]
511 if fv.Kind() == reflect.Ptr {
512 // missing optional key: leave the pointer as nil
516 return nil, fmt.Errorf("ParseStruct: field %s of %s is missing\n", key, t.Name())
518 if fv.Kind() == reflect.Ptr {
519 if s == "" || s == "-" {
521 // empty optional key: same as missing
524 fv.Set(reflect.New(fv.Type().Elem()))
527 switch fieldtype[key] {
530 val, err = parseId(s)
534 val, err = parseString(s)
538 val, err = strconv.Atoi64(s)
542 val, err = parseDate(s)
545 // TODO: empty slice?
546 ids := strings.Split(s, " ")
547 val := make([]string, len(ids))
548 for j, id := range ids {
549 val[j], err = parseId(id)
554 fv.Set(reflect.ValueOf(val))
556 panic("bad field type " + key + " " + fieldtype[key])
562 if len(doc.Fields)+nokey != n {
563 for k := range doc.Fields {
565 err = fmt.Errorf("ParseStruct: unknown field %s in %s", k, t.Name())
573 // turn a struct into a document
574 func FormatStruct(iv interface{}) (doc *Document, err error) {
575 v := reflect.ValueOf(iv)
576 if v.Kind() != reflect.Ptr || v.IsNil() || v.Elem().Kind() != reflect.Struct {
577 panic("input is not a pointer to struct")
584 doc.Fields = make(map[string]string)
585 for i := 0; i < n; i++ {
588 key := fieldname[ft.Name]
593 if fv.Kind() == reflect.Ptr {
595 // keep empty optional fields but mark them
601 switch fieldtype[key] {
603 val = formatId(fv.String())
605 val = formatString(fv.String())
607 val = strconv.Itoa64(fv.Int())
609 val = formatDate(fv.Int())
612 for j := 0; j < k; j++ {
616 val += formatId(fv.Index(j).String())
619 panic("bad field type " + key + " " + fieldtype[key])
622 doc.Fields[key] = val
623 doc.Order = append(doc.Order, key)
628 func ParseFields(s []byte) (fields map[string]string, rest []byte, err error) {
630 fields = make(map[string]string)
632 // \n is optional after the last field and an extra \n is allowed as well
635 line, rest = getLine(rest)
636 // empty line after the last field is consumed
640 if line[0] == ' ' && key != "" {
641 // "Key: v1\n v2\n" is equivalent to "Key: v1 v2\n"
642 fields[key] += string(line)
645 if line[0] < 'A' || line[0] > 'Z' {
646 err = fmt.Errorf("ParseFields: field name must start with an upper-case ascii letter")
649 i := bytes.IndexByte(line, ':')
651 err = fmt.Errorf("ParseFields: missing ':'")
654 key = string(line[:i])
655 if _, ok := fields[key]; ok {
656 err = fmt.Errorf("ParseFields: repeated fields are not allowed")
659 fields[key] = string(line[i+1:])
661 for key, v := range fields {
662 // either a single space follows ':' or the value is empty
663 // good: "Key:\n", "Key:\n value\n", "Key: value\n", "Key: v1\n v2\n"
664 // bad: "Key:value\n", "Key: \nvalue\n"
665 // bad but not checked here: "Key: \n", "Key: value \n", "Key:\n \n value\n"
670 err = fmt.Errorf("ParseFields: ':' is not followed by ' '")
678 // TODO: limit errors
680 func parseId(s string) (string, error) {
681 // check if hex decodable
682 // TODO: length check
683 dst := make([]byte, len(s)/2)
684 _, err := hex.Decode(dst, []byte(s))
688 func formatId(s string) string {
692 func parseString(s string) (string, error) {
693 if len(s) > MaxValueLength {
694 return "", fmt.Errorf("parseString: length limit is exceeded")
699 func formatString(s string) string {
703 func parseDate(s string) (int64, error) {
704 // TODO: fractional seconds?
705 t, err := time.Parse(time.RFC3339, s)
709 return t.Seconds(), nil
712 func formatDate(i int64) string {
713 return time.SecondsToUTC(i).Format(time.RFC3339)
716 func getLine(data []byte) (line, rest []byte) {
717 i := bytes.IndexByte(data, '\n')
722 } else if i > 0 && data[i-1] == '\r' {
725 return data[:i], data[j:]