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 is 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/vnd.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: hex nonce, uniq nonce vs uniq drawer.nonce
35 // TODO: denom, issuer from key (key representation: armor?)
41 "crypto/openpgp/armor"
42 "crypto/openpgp/packet"
55 MaxLineLength = 160 // 1 sha512 + 1 key (without \n)
56 MaxValueLength = 1300 // 20 sha256 space separated (without \n)
58 MaxDenominationLength = 100
61 const ClearSignedHeader = "-----BEGIN PGP SIGNED MESSAGE-----"
63 // MIME type for epoint documents, see RFC 4288
64 var ContentType = map[string]string{
65 "Draft": "text/vnd.epoint.draft; charset=utf-8",
66 "Notice": "text/vnd.epoint.notice; charset=utf-8",
67 "DebitCert": "text/vnd.epoint.debit; charset=utf-8",
68 "CreditCert": "text/vnd.epoint.credit; charset=utf-8",
69 "BounceCert": "text/vnd.epoint.bounce; charset=utf-8",
72 // OpenPGP signed cleartext document representation
74 // Sign and CleanSigned sets Hash for FormatSigned
77 // Signed text (no dash escape, no trailing space, \n new lines)
79 // Armored detached text signature of the Body
83 // parsed epoint document
84 type Document struct {
86 Fields map[string]string
90 var fieldtype = map[string]string{
92 "Authorized-By": "id",
97 "Denomination": "text",
101 "Expiry-Date": "date",
105 "Last-Credit-Serial": "int",
106 "Last-Debit-Serial": "int",
107 "Maturity-Date": "date",
114 var fieldname = map[string]string{
115 "AuthorizedBy": "Authorized-By",
116 "DebitCert": "Debit-Cert",
117 "ExpiryDate": "Expiry-Date",
118 "LastCert": "Last-Cert",
119 "LastCreditSerial": "Last-Credit-Serial",
120 "LastDebitSerial": "Last-Debit-Serial",
121 "MaturityDate": "Maturity-Date",
131 MaturityDate *int64 // optional
132 ExpiryDate *int64 // optional
134 Notes *string // optional
140 Notes *string // optional
141 References []string // may be empty (startup notice)
152 Notes *string // optional
153 LastDebitSerial int64 // 0 if none
154 LastCreditSerial int64 // 0 if none
155 LastCert *string // nil if serial == 1
161 type DebitCert struct {
166 type CreditCert struct {
172 type BounceCert struct {
175 LastCert *string // optional
176 Balance int64 // 0 if none
179 Notes *string // optional
183 // Common cert part of a debit or credit cert
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(nil))
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, keys openpgp.KeyRing) (err error) {
247 msg := bytes.NewBuffer(c.Body)
248 sig := bytes.NewBuffer(c.Signature)
249 _, err = openpgp.CheckArmoredDetachedSignature(keys, msg, sig)
253 // Sign body with given secret key
254 func Sign(body []byte, key *openpgp.Entity) (c *Signed, err error) {
257 c.Body = cleanBody(body)
258 w := new(bytes.Buffer)
259 err = openpgp.ArmoredDetachSignText(w, key, bytes.NewBuffer(c.Body))
263 // close armored document with a \n
264 _, _ = w.Write([]byte{'\n'})
265 c.Signature = w.Bytes()
269 // split a clear signed document into body and armored signature
270 func ParseSigned(s []byte) (c *Signed, err error) {
271 // look for clear signed header
272 for !bytes.HasPrefix(s, []byte(ClearSignedHeader)) {
275 err = fmt.Errorf("ParseSigned: clear signed header is missing")
279 s = s[len(ClearSignedHeader):]
280 // end of line after the header
281 empty, s := getLine(s)
283 err = fmt.Errorf("ParseSigned: bad clear signed header")
286 // skip all hash headers, section 7.
287 for bytes.HasPrefix(s, []byte("Hash: ")) {
291 empty, s = getLine(s)
293 err = fmt.Errorf("ParseSigned: expected an empty line after armor headers")
297 for !bytes.HasPrefix(s, []byte("-----BEGIN")) {
300 // dash unescape, section 7.1.
301 if bytes.HasPrefix(line, []byte("- ")) {
304 // empty values are not supported: "Key: \n"
305 lines = append(lines, bytes.TrimRight(line, " \t"))
308 // last line is not closed by \n
309 c.Body = bytes.Join(lines, []byte("\n"))
310 // signature is just the rest of the input data
315 // clean up, check and reencode signature
316 // used on drafts before calculating the signed document hash
317 func CleanSigned(c *Signed) (err error) {
318 b, err := armor.Decode(bytes.NewBuffer(c.Signature))
322 if b.Type != openpgp.SignatureType {
323 err = fmt.Errorf("CleanSigned: invalid armored signature type")
326 p, err := packet.Read(b.Body)
330 sig, ok := p.(*packet.Signature)
332 err = fmt.Errorf("CleanSigned: invalid signature packet")
336 if sig.SigType != packet.SigTypeText {
337 err = fmt.Errorf("CleanSigned: expected text signature")
346 err = fmt.Errorf("CleanSigned: expected SHA1 or SHA256 signature hash")
349 // TODO: check CreationTime and other subpackets
350 if sig.SigLifetimeSecs != nil && *sig.SigLifetimeSecs != 0 {
351 err = fmt.Errorf("CleanSigned: signature must not expire")
354 out := new(bytes.Buffer)
355 w, err := armor.Encode(out, openpgp.SignatureType, nil)
359 err = sig.Serialize(w)
367 c.Signature = out.Bytes()
371 // create clear signed document
372 func FormatSigned(c *Signed) (data []byte, err error) {
373 s := ClearSignedHeader + "\n"
375 s += "Hash: " + c.Hash + "\n"
380 s += string(c.Signature)
385 // parse type and fields of a document body
386 func ParseDocument(body []byte) (doc *Document, err error) {
387 // parse content type header first
388 fields, s, err := ParseFields(body)
392 ctype, ok := fields["Content-Type"]
393 if len(fields) != 1 || !ok {
394 return nil, fmt.Errorf("ParseBody: expected a single Content-Type header field")
397 for k, v := range ContentType {
404 return nil, fmt.Errorf("ParseBody: unknown Content-Type: %s", ctype)
407 doc.Fields, s, err = ParseFields(s)
408 if err == nil && len(s) > 0 {
409 err = fmt.Errorf("ParseBody: extra data after fields: %q", s)
414 // create document body
415 func FormatDocument(doc *Document) (body []byte, err error) {
416 ctype, ok := ContentType[doc.Type]
418 err = fmt.Errorf("FormatDocument: unknown document type: %s", doc.Type)
421 s := "Content-Type: " + ctype + "\n\n"
422 for _, k := range doc.Order {
423 s += k + ": " + doc.Fields[k] + "\n"
425 return []byte(s), nil
428 // parse doc fields into a struct according to the document type
429 func parseStruct(v reflect.Value, fields map[string]string, seen map[string]bool) (err error) {
432 for i := 0; i < n && err == nil; i++ {
435 if ft.Anonymous && fv.Kind() == reflect.Struct {
436 err = parseStruct(fv, fields, seen)
439 key := fieldname[ft.Name]
445 if fv.Kind() == reflect.Ptr {
446 // missing optional key: leave the pointer as nil
449 return fmt.Errorf("ParseStruct: field %s of %s is missing\n", key, t.Name())
452 if fv.Kind() == reflect.Ptr {
453 if s == "" || s == "-" {
455 // empty optional key: same as missing
458 fv.Set(reflect.New(fv.Type().Elem()))
461 switch fieldtype[key] {
464 val, err = parseId(s)
468 val, err = parseString(s)
472 val, err = strconv.ParseInt(s, 10, 64)
476 val, err = parseDate(s)
479 // TODO: empty slice?
480 ids := strings.Split(s, " ")
481 val := make([]string, len(ids))
482 for j, id := range ids {
483 val[j], err = parseId(id)
488 fv.Set(reflect.ValueOf(val))
490 panic("bad field type " + key + " " + fieldtype[key])
496 // ParseStruct parses an epoint document and returns a struct representation
497 func ParseStruct(doc *Document) (iv interface{}, err error) {
510 err = fmt.Errorf("ParseStruct: unkown doc type: %s", doc.Type)
513 seen := make(map[string]bool)
514 err = parseStruct(reflect.ValueOf(iv).Elem(), doc.Fields, seen)
518 if len(doc.Fields) != len(seen) {
519 for f := range doc.Fields {
521 err = fmt.Errorf("ParseStruct: unknown field %s in %s", f, doc.Type)
529 // turn a struct into a document
530 func formatStruct(v reflect.Value, doc *Document) (err error) {
533 for i := 0; i < n; i++ {
536 if ft.Anonymous && fv.Kind() == reflect.Struct {
537 err = formatStruct(fv, doc)
543 key := fieldname[ft.Name]
548 if fv.Kind() == reflect.Ptr {
550 // keep empty optional fields but mark them
556 switch fieldtype[key] {
558 val = formatId(fv.String())
560 val = formatString(fv.String())
562 val = strconv.FormatInt(fv.Int(), 10)
564 val = formatDate(fv.Int())
567 for j := 0; j < k; j++ {
571 val += formatId(fv.Index(j).String())
574 panic("bad field type " + key + " " + fieldtype[key])
577 doc.Fields[key] = val
578 doc.Order = append(doc.Order, key)
583 // FormatStruct turns a struct into a document
584 func FormatStruct(iv interface{}) (doc *Document, err error) {
585 v := reflect.ValueOf(iv)
586 if v.Kind() != reflect.Ptr || v.IsNil() || v.Elem().Kind() != reflect.Struct {
587 panic("input is not a pointer to struct")
590 doc.Type = v.Elem().Type().Name()
591 doc.Fields = make(map[string]string)
592 err = formatStruct(v.Elem(), doc)
596 // ParseFields parses a key value sequence into a fields map
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)
681 func formatDate(i int64) string {
682 return time.Unix(i,0).UTC().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:]