// -----END PGP SIGNATURE-----
package document
-// TODO: error types
-// TODO: optional fields
-// TODO: fields of notice (last notice, serial)
-// TODO: space vs not to space
+// 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/openpgp"
"crypto/openpgp/armor"
"crypto/openpgp/packet"
+ "crypto/sha1"
"encoding/hex"
"fmt"
"reflect"
"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
"Last-Credit-Serial": "int",
"Last-Debit-Serial": "int",
"Maturity-Date": "date",
- "Nonce": "text",
+ "Nonce": "id",
"Notes": "text",
"References": "ids",
"Serial": "int",
Denomination string
Issuer string
AuthorizedBy string
- MaturityDate int64 // optional
- ExpiryDate int64 // optional
- Nonce string // optional
- Notes string // optional
+ MaturityDate *int64 // optional
+ ExpiryDate *int64 // optional
+ Nonce string
+ Notes *string // optional
}
type Notice struct {
Date int64
AuthorizedBy string
- Notes string
- References []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 {
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 // ? if serial == 1
- References []string // may be empty
+ Notes *string // optional
+ LastDebitSerial int64 // 0 if none
+ LastCreditSerial int64 // 0 if none
+ LastCert *string // nil if serial == 1
+ References []string
}
type CreditCert struct {
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 // may be empty
+ 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
- Denomination string
- Issuer string
+ LastCert *string // optional
+ Balance int64 // 0 if none
Date int64
AuthorizedBy string
- Notes string // optional
- References []string // may be empty
+ 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
}
// format and sign an epoint document
-func Format(iv interface{}, key *openpgp.Entity) (s []byte, err error) {
+func Format(iv interface{}, key *openpgp.Entity) (s []byte, c *Signed, err error) {
doc, err := FormatStruct(iv)
if err != nil {
return
if err != nil {
return
}
- c, err := Sign(body, key)
+ c, err = Sign(body, key)
if err != nil {
return
}
- return FormatSigned(c)
+ s, err = FormatSigned(c)
+ return
}
// verify an epoint document, return the cleaned version as well
-func Verify(c *Signed, key *openpgp.Entity) (cleaned []byte, err error) {
- err = CleanSigned(c)
- if err != nil {
- return
- }
- err = VerifyCleaned(c, key)
- if err != nil {
- return
- }
- return FormatSigned(c)
-}
-
-// verify signature of body with given key
-func VerifyCleaned(c *Signed, key *openpgp.Entity) (err error) {
- kr := openpgp.EntityList{key}
+func Verify(c *Signed, key openpgp.KeyRing) (err error) {
msg := bytes.NewBuffer(c.Body)
sig := bytes.NewBuffer(c.Signature)
- _, err = openpgp.CheckArmoredDetachedSignature(kr, msg, sig)
+ // TODO: verify signature
+ _, _ = msg, sig
+ // _, err = openpgp.CheckArmoredDetachedSignature(key, msg, sig)
return
}
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
}
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)
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 = parseDate(s)
fv.SetInt(val)
case "ids":
+ // TODO: empty slice?
ids := strings.Split(s, " ")
val := make([]string, len(ids))
for j, id := range ids {
return
}
}
- if len(doc.Fields) != n {
+ 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())
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())
default:
panic("bad field type " + key + " " + fieldtype[key])
}
+ setval:
doc.Fields[key] = val
doc.Order = append(doc.Order, key)
}
func ParseFields(s []byte) (fields map[string]string, rest []byte, err error) {
rest = s
fields = make(map[string]string)
- lastkey := ""
+ 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 parsed fields is consumed
+ // empty line after the last field is consumed
if len(line) == 0 {
break
}
- // TODO: empty line: " \n"
- if line[0] == ' ' {
- if lastkey == "" {
- err = fmt.Errorf("ParseFields: expected a field, not ' '")
- return
- }
- fields[lastkey] += string(line)
+ if line[0] == ' ' && key != "" {
+ // "Key: v1\n v2\n" is equivalent to "Key: v1 v2\n"
+ fields[key] += string(line)
continue
}
- // TODO: empty value: "Key: \n"
- i := bytes.Index(line, []byte(": "))
+ 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 ': '")
- break
+ err = fmt.Errorf("ParseFields: missing ':'")
+ return
}
- lastkey = string(line[:i])
- if _, ok := fields[lastkey]; ok {
+ key = string(line[:i])
+ if _, ok := fields[key]; ok {
err = fmt.Errorf("ParseFields: repeated fields are not allowed")
return
}
- fields[lastkey] = string(line[i+2:])
+ 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) {
- if len(s) != 40 {
- return "", fmt.Errorf("parseId: expected 40 characters; got %d", len(s))
- }
+ // check if hex decodable
+ // TODO: length check
dst := make([]byte, len(s)/2)
_, err := hex.Decode(dst, []byte(s))
return s, err
}
func parseString(s string) (string, error) {
- if len(s) > 140 {
- return "", fmt.Errorf("parseString: 140 chars limit is exceeded")
+ if len(s) > MaxValueLength {
+ return "", fmt.Errorf("parseString: length limit is exceeded")
}
return s, nil
}
}
func parseDate(s string) (int64, error) {
+ // TODO: fractional seconds?
t, err := time.Parse(time.RFC3339, s)
if err != nil {
return 0, err
}
func getLine(data []byte) (line, rest []byte) {
- i := bytes.Index(data, []byte{'\n'})
+ i := bytes.IndexByte(data, '\n')
j := i + 1
if i < 0 {
i = len(data)