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-----
30 // TODO: optional fields
31 // TODO: fields of notice (last notice, serial)
32 // TODO: space vs not to space
38 "crypto/openpgp/armor"
39 "crypto/openpgp/packet"
48 const ClearSignedHeader = "-----BEGIN PGP SIGNED MESSAGE-----"
50 // MIME type for epoint documents, see RFC 4288
51 var ContentType = map[string]string{
52 "Draft": "text/vnd.epoint.draft; charset=utf-8",
53 "Notice": "text/vnd.epoint.notice; charset=utf-8",
54 "DebitCert": "text/vnd.epoint.debit; charset=utf-8",
55 "CreditCert": "text/vnd.epoint.credit; charset=utf-8",
56 "BounceCert": "text/vnd.epoint.bounce; charset=utf-8",
59 // OpenPGP signed cleartext document representation
61 // Sign and CleanSigned sets Hash for FormatSigned
64 // Signed text (no dash escape, no trailing space, \n new lines)
66 // Armored detached text signature of the Body
70 // parsed epoint document
71 type Document struct {
73 Fields map[string]string
77 var fieldtype = map[string]string{
79 "Authorized-By": "id",
84 "Denomination": "text",
88 "Expiry-Date": "date",
92 "Last-Credit-Serial": "int",
93 "Last-Debit-Serial": "int",
94 "Maturity-Date": "date",
101 var fieldname = map[string]string{
102 "AuthorizedBy": "Authorized-By",
103 "DebitCert": "Debit-Cert",
104 "ExpiryDate": "Expiry-Date",
105 "LastCert": "Last-Cert",
106 "LastCreditSerial": "Last-Credit-Serial",
107 "LastDebitSerial": "Last-Debit-Serial",
108 "MaturityDate": "Maturity-Date",
118 MaturityDate int64 // optional
119 ExpiryDate int64 // optional
120 Nonce string // optional
121 Notes string // optional
131 type DebitCert struct {
141 Notes string // optional
142 LastDebitSerial int64 // 0 if none
143 LastCreditSerial int64 // 0 if none
144 LastCert string // ? if serial == 1
145 References []string // may be empty
148 type CreditCert struct {
160 Notes string // optional
161 LastDebitSerial int64 // 0 if none
162 LastCreditSerial int64 // 0 if none
163 LastCert string // ? if serial == 1
164 References []string // may be empty
167 type BounceCert struct {
170 LastCert string // optional
171 Balance int64 // 0 if none
176 Notes string // optional
177 References []string // may be empty
180 // parse an epoint document without checking the signature and format details
181 func Parse(s []byte) (iv interface{}, c *Signed, err error) {
182 c, err = ParseSigned(s)
186 doc, err := ParseDocument(c.Body)
190 iv, err = ParseStruct(doc)
194 // format and sign an epoint document
195 func Format(iv interface{}, key *openpgp.Entity) (s []byte, err error) {
196 doc, err := FormatStruct(iv)
200 body, err := FormatDocument(doc)
204 c, err := Sign(body, key)
208 return FormatSigned(c)
211 // verify an epoint document, return the cleaned version as well
212 func Verify(c *Signed, key *openpgp.Entity) (cleaned []byte, err error) {
217 err = VerifyCleaned(c, key)
221 return FormatSigned(c)
224 // verify signature of body with given key
225 func VerifyCleaned(c *Signed, key *openpgp.Entity) (err error) {
226 kr := openpgp.EntityList{key}
227 msg := bytes.NewBuffer(c.Body)
228 sig := bytes.NewBuffer(c.Signature)
229 _, err = openpgp.CheckArmoredDetachedSignature(kr, msg, sig)
233 // sign body with given secret key
234 func Sign(body []byte, key *openpgp.Entity) (c *Signed, err error) {
238 w := new(bytes.Buffer)
239 err = openpgp.ArmoredDetachSignText(w, key, bytes.NewBuffer(c.Body))
240 c.Signature = w.Bytes()
244 // split a clear signed document into body and armored signature
245 func ParseSigned(s []byte) (c *Signed, err error) {
246 // look for clear signed header
247 for !bytes.HasPrefix(s, []byte(ClearSignedHeader)) {
250 err = fmt.Errorf("ParseSigned: clear signed header is missing")
254 s = s[len(ClearSignedHeader):]
255 // end of line after the header
256 empty, s := getLine(s)
258 err = fmt.Errorf("ParseSigned: bad clear signed header")
261 // skip all hash headers, section 7.
262 for bytes.HasPrefix(s, []byte("Hash: ")) {
266 empty, s = getLine(s)
268 err = fmt.Errorf("ParseSigned: expected an empty line after armor headers")
272 for !bytes.HasPrefix(s, []byte("-----BEGIN")) {
275 // dash unescape, section 7.1.
276 if bytes.HasPrefix(line, []byte("- ")) {
279 // empty values are not supported: "Key: \n"
280 lines = append(lines, bytes.TrimRight(line, " \t"))
283 // last line is not closed by \n
284 c.Body = bytes.Join(lines, []byte("\n"))
285 // signature is just the rest of the input data
290 // clean up, check and reencode signature
291 // used on drafts before calculating the signed document hash
292 func CleanSigned(c *Signed) (err error) {
293 b, err := armor.Decode(bytes.NewBuffer(c.Signature))
297 if b.Type != openpgp.SignatureType {
298 err = fmt.Errorf("CleanSigned: invalid armored signature type")
301 p, err := packet.Read(b.Body)
305 sig, ok := p.(*packet.Signature)
307 err = fmt.Errorf("CleanSigned: invalid signature packet")
311 if sig.SigType != packet.SigTypeText {
312 err = fmt.Errorf("CleanSigned: expected text signature")
321 err = fmt.Errorf("CleanSigned: expected SHA1 or SHA256 signature hash")
324 // TODO: check CreationTime and other subpackets
325 if sig.SigLifetimeSecs != nil && *sig.SigLifetimeSecs != 0 {
326 err = fmt.Errorf("CleanSigned: signature must not expire")
329 out := new(bytes.Buffer)
330 w, err := armor.Encode(out, openpgp.SignatureType, nil)
334 err = sig.Serialize(w)
342 c.Signature = out.Bytes()
346 // create clear signed document
347 func FormatSigned(c *Signed) (data []byte, err error) {
348 s := ClearSignedHeader + "\n"
350 s += "Hash: " + c.Hash + "\n"
355 s += string(c.Signature)
360 // parse type and fields of a document body
361 func ParseDocument(body []byte) (doc *Document, err error) {
362 // parse content type header first
363 fields, s, err := ParseFields(body)
367 ctype, ok := fields["Content-Type"]
368 if len(fields) != 1 || !ok {
369 return nil, fmt.Errorf("ParseBody: expected a single Content-Type header field")
372 for k, v := range ContentType {
379 return nil, fmt.Errorf("ParseBody: unknown Content-Type: %s", ctype)
382 doc.Fields, s, err = ParseFields(s)
383 if err == nil && len(s) > 0 {
384 err = fmt.Errorf("ParseBody: extra data after fields: %q", s)
389 // create document body
390 func FormatDocument(doc *Document) (body []byte, err error) {
391 ctype, ok := ContentType[doc.Type]
393 err = fmt.Errorf("FormatDocument: unknown document type: %s", doc.Type)
396 s := "Content-Type: " + ctype + "\n\n"
397 for _, k := range doc.Order {
398 s += k + ": " + doc.Fields[k] + "\n"
400 return []byte(s), nil
403 // parse doc fields into a struct according to the document type
404 func ParseStruct(doc *Document) (iv interface{}, err error) {
417 err = fmt.Errorf("ParseStruct: unkown doc type: %s", doc.Type)
420 seen := make(map[string]bool)
421 v := reflect.ValueOf(iv).Elem()
424 for i := 0; i < n; i++ {
427 key := fieldname[ft.Name]
432 s, ok := doc.Fields[key]
434 return nil, fmt.Errorf("ParseStruct: field %s of %s is missing\n", key, t.Name())
436 switch fieldtype[key] {
439 val, err = parseId(s)
443 val, err = parseString(s)
447 val, err = strconv.Atoi64(s)
451 val, err = parseDate(s)
454 ids := strings.Split(s, " ")
455 val := make([]string, len(ids))
456 for j, id := range ids {
457 val[j], err = parseId(id)
462 fv.Set(reflect.ValueOf(val))
464 panic("bad field type " + key + " " + fieldtype[key])
470 if len(doc.Fields) != n {
471 for k := range doc.Fields {
473 err = fmt.Errorf("ParseStruct: unknown field %s in %s", k, t.Name())
481 // turn a struct into a document
482 func FormatStruct(iv interface{}) (doc *Document, err error) {
483 v := reflect.ValueOf(iv)
484 if v.Kind() != reflect.Ptr || v.IsNil() || v.Elem().Kind() != reflect.Struct {
485 panic("input is not a pointer to struct")
492 doc.Fields = make(map[string]string)
493 for i := 0; i < n; i++ {
496 key := fieldname[ft.Name]
501 switch fieldtype[key] {
503 val = formatId(fv.String())
505 val = formatString(fv.String())
507 val = strconv.Itoa64(fv.Int())
509 val = formatDate(fv.Int())
512 for j := 0; j < k; j++ {
516 val += formatId(fv.Index(j).String())
519 panic("bad field type " + key + " " + fieldtype[key])
521 doc.Fields[key] = val
522 doc.Order = append(doc.Order, key)
527 func ParseFields(s []byte) (fields map[string]string, rest []byte, err error) {
529 fields = make(map[string]string)
533 line, rest = getLine(rest)
534 // empty line after the parsed fields is consumed
538 // TODO: empty line: " \n"
541 err = fmt.Errorf("ParseFields: expected a field, not ' '")
544 fields[lastkey] += string(line)
547 // TODO: empty value: "Key: \n"
548 i := bytes.Index(line, []byte(": "))
550 err = fmt.Errorf("ParseFields: missing ': '")
553 lastkey = string(line[:i])
554 if _, ok := fields[lastkey]; ok {
555 err = fmt.Errorf("ParseFields: repeated fields are not allowed")
558 fields[lastkey] = string(line[i+2:])
563 // TODO: limit errors
565 func parseId(s string) (string, error) {
567 return "", fmt.Errorf("parseId: expected 40 characters; got %d", len(s))
569 dst := make([]byte, len(s)/2)
570 _, err := hex.Decode(dst, []byte(s))
574 func formatId(s string) string {
578 func parseString(s string) (string, error) {
580 return "", fmt.Errorf("parseString: 140 chars limit is exceeded")
585 func formatString(s string) string {
589 func parseDate(s string) (int64, error) {
590 t, err := time.Parse(time.RFC3339, s)
594 return t.Seconds(), nil
597 func formatDate(i int64) string {
598 return time.SecondsToUTC(i).Format(time.RFC3339)
601 func getLine(data []byte) (line, rest []byte) {
602 i := bytes.Index(data, []byte{'\n'})
607 } else if i > 0 && data[i-1] == '\r' {
610 return data[:i], data[j:]