From: nsz Date: Thu, 8 Dec 2011 12:27:10 +0000 (+0100) Subject: code reorganization X-Git-Url: http://nsz.repo.hu/git/?p=epoint;a=commitdiff_plain;h=60f039199482cbeffc865a259f31809aa17d9698 code reorganization --- diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5c5c8fa --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +all: install +install: + make -C pkg install +test: + make -C pkg test +clean: + make -C pkg clean +nuke: + make -C pkg nuke +.PHONY: all install test clean nuke diff --git a/cmd/epoint-client/Makefile b/cmd/epoint-client/Makefile new file mode 100644 index 0000000..9f4116d --- /dev/null +++ b/cmd/epoint-client/Makefile @@ -0,0 +1,7 @@ +include $(GOROOT)/src/Make.inc + +TARG=epoint-client +GOFILES=\ + epoint-client.go + +include $(GOROOT)/src/Make.cmd diff --git a/cmd/epoint-client/epoint-client.go b/cmd/epoint-client/epoint-client.go new file mode 100644 index 0000000..da29a2c --- /dev/null +++ b/cmd/epoint-client/epoint-client.go @@ -0,0 +1,4 @@ +package main + +func main() { +} diff --git a/cmd/epoint-server/Makefile b/cmd/epoint-server/Makefile new file mode 100644 index 0000000..c14e7c1 --- /dev/null +++ b/cmd/epoint-server/Makefile @@ -0,0 +1,7 @@ +include $(GOROOT)/src/Make.inc + +TARG=epoint-server +GOFILES=\ + epoint-server.go + +include $(GOROOT)/src/Make.cmd diff --git a/cmd/epoint-server/epoint-server.go b/cmd/epoint-server/epoint-server.go new file mode 100644 index 0000000..10ddcbd --- /dev/null +++ b/cmd/epoint-server/epoint-server.go @@ -0,0 +1,167 @@ +package main + +import ( + "crypto/openpgp" + "epoint/server" + "fmt" + "log" + "net/http" + "os" +) + +const ( + addr = ":8080" + rootdir = "docroot" + seckey = "./key.sec" +) + +var serverkey *openpgp.Entity + +// todo: http header limit: 64K, body limit: 64K + +// Dummy initialization of serverkey +func initkey() (err error) { + f, err := os.Open(seckey) + if err != nil { + return + } + keys, err := openpgp.ReadKeyRing(f) + if err != nil { + f.Close() + return + } + err = f.Close() + if err != nil { + return + } + serverkey = keys[0] + err = os.MkdirAll(rootdir, 0755) + if err != nil { + return + } + f, err = os.Create(rootdir + "/serverkey") + if err != nil { + return + } + err = serverkey.Serialize(f) + if err != nil { + return + } + // TODO: make sure pubkey is replicated and available + err = f.Sync() + if err != nil { + return + } + err = f.Close() + return +} + +func httpError(w http.ResponseWriter, code int, msg string) { + log.Printf("error: %d %s", code, msg) + http.Error(w, fmt.Sprintf("%d %s\n\n%s\n", code, http.StatusText(code), msg), code) +} + +func httpReq(r *http.Request) string { + err := r.ParseForm() + form := "" + if err != nil { + form = err.Error() + } else { + a := []string{} + for k := range r.Form { + a = append(a, k) + } + form = fmt.Sprintf("%v", a) + } + return fmt.Sprintf("%s %s params:%s", r.Method, r.URL.Raw, form) +} + +func defaultHandler(w http.ResponseWriter, r *http.Request) { + log.Printf("%s %s", r.RemoteAddr, httpReq(r)) + fmt.Fprintf(w, "not implemented: %s %s\n", r.Method, r.URL.Raw) +} + +func submitHandler(w http.ResponseWriter, r *http.Request) { + log.Printf("%s %s", r.RemoteAddr, httpReq(r)) + draft := r.FormValue("draft") + debit := r.FormValue("debit") + key := r.FormValue("key") + switch { + case draft != "": + cert, err := server.EvalDraft([]byte(draft), serverkey) + if err != nil { + msg := fmt.Sprintf("eval draft failed: %s", err) + httpError(w, 404, msg) + } else { + w.Write(cert) + } + case debit != "": + cert, err := server.EvalDebitCert([]byte(debit), serverkey) + if err != nil { + msg := fmt.Sprintf("eval debit failed: %s", err) + httpError(w, 404, msg) + } else { + w.Write(cert) + } + case key != "": + err := server.AddKeys([]byte(key)) + if err != nil { + msg := fmt.Sprintf("add keys failed: %s", err) + httpError(w, 404, msg) + } else { + w.Write([]byte("ok\nTODO: create cert 1 here?")) + } + default: + msg := fmt.Sprintf("expected key, draft or debit param, got: %s", httpReq(r)) + httpError(w, 404, msg) + } +} + +func main() { + err := initkey() + if err != nil { + log.Fatal(err) + } + err = server.Init(rootdir) + if err != nil { + log.Fatal(err) + } + err = server.StoreSk(serverkey) + if err != nil { + log.Fatal(err) + } + + // TODO: url from key + f, err := os.Create(rootdir + "/form.html") + if err != nil { + log.Fatal(err) + } + _, _ = fmt.Fprintf(f, `epoint-server submit form +

epoint-server submit form

+

web form

+

submit one document at once +

+

key:
+

draft:
+

debit:
+

+

+

command line

+
+curl --data-urlencode name@path/to/file.txt host/submit
+
+where 'name' is 'key', 'draft' or 'debit'. + +`, addr) + _ = f.Close() + + // queries + http.Handle("/", http.FileServer(http.Dir(rootdir))) + + // actions + // withdraw, draw, deposit, process, clear + http.HandleFunc("/submit", submitHandler) + + log.Printf("start service on %s, server key id: %X\n", addr, serverkey.PrimaryKey.Fingerprint) + log.Fatal(http.ListenAndServe(addr, nil)) +} diff --git a/cmd/genkey/genkey.go b/cmd/genkey/genkey.go index 3a8a256..7f3c518 100644 --- a/cmd/genkey/genkey.go +++ b/cmd/genkey/genkey.go @@ -2,7 +2,7 @@ package main import ( "crypto/openpgp" - "epoint/dsakey" + "epoint/key" "fmt" "log" "os" @@ -86,9 +86,9 @@ func main() { } var e *openpgp.Entity if isIssuer { - e, err = dsakey.NewIssuerEntity(b[:n], denom) + e, err = key.NewIssuerEntity(b[:n], denom) } else { - e, err = dsakey.NewHolderEntity(b[:n], issuer, denom) + e, err = key.NewHolderEntity(b[:n], issuer, denom) } if err != nil { log.Fatal(err) diff --git a/document/Makefile b/document/Makefile deleted file mode 100644 index 7a52d21..0000000 --- a/document/Makefile +++ /dev/null @@ -1,6 +0,0 @@ -include $(GOROOT)/src/Make.inc - -TARG=epoint/document -GOFILES=document.go - -include $(GOROOT)/src/Make.pkg diff --git a/document/document.go b/document/document.go deleted file mode 100644 index 0c8b0f4..0000000 --- a/document/document.go +++ /dev/null @@ -1,695 +0,0 @@ -// Package document implements epoint document parsing and creation. -// -// An epoint document is an OpenPGP (RFC 4880) clear signed -// utf-8 text of key-value pairs. -// The body contains a content-type MIME header so the document -// can be used in OpenPGP/MIME (RFC 3156) emails. -// The format of the key-value pairs are similar to MIME header -// fields: keys and values are separated by ": ", repeated keys -// are not allowed, long values can be split before a space. -// -// Example: -// -// -----BEGIN PGP SIGNED MESSAGE----- -// Hash: SHA1 -// -// Content-Type: text/plain.epoint.type; charset=utf-8 -// -// Key: Value1 -// Another-Key: Value2 -// Last-Key: Long -// value that spans -// multiple lines -// -----BEGIN PGP SIGNATURE----- -// -// pgp signature -// -----END PGP SIGNATURE----- -package document - -// 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 (require only drawer.nonce to be uniq) -// TODO: denom, issuer from key (key representation: armor?) - -import ( - "bytes" - "crypto" - "crypto/openpgp" - "crypto/openpgp/armor" - "crypto/openpgp/packet" - "crypto/sha1" - "encoding/hex" - "fmt" - "reflect" - "strconv" - "strings" - "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 -var ContentType = map[string]string{ - "Draft": "text/vnd.epoint.draft; charset=utf-8", - "Notice": "text/vnd.epoint.notice; charset=utf-8", - "DebitCert": "text/vnd.epoint.debit; charset=utf-8", - "CreditCert": "text/vnd.epoint.credit; charset=utf-8", - "BounceCert": "text/vnd.epoint.bounce; charset=utf-8", -} - -// OpenPGP signed cleartext document representation -type Signed struct { - // Sign and CleanSigned sets Hash for FormatSigned - // TODO: CreationDate - Hash string - // Signed text (no dash escape, no trailing space, \n new lines) - Body []byte - // Armored detached text signature of the Body - Signature []byte -} - -// parsed epoint document -type Document struct { - Type string - Fields map[string]string - Order []string -} - -var fieldtype = map[string]string{ - "Amount": "int", - "Authorized-By": "id", - "Balance": "int", - "Beneficiary": "id", - "Date": "date", - "Debit-Cert": "id", - "Denomination": "text", - "Difference": "int", - "Draft": "id", - "Drawer": "id", - "Expiry-Date": "date", - "Holder": "id", - "Issuer": "id", - "Last-Cert": "id", - "Last-Credit-Serial": "int", - "Last-Debit-Serial": "int", - "Maturity-Date": "date", - "Nonce": "id", - "Notes": "text", - "References": "ids", - "Serial": "int", -} - -var fieldname = map[string]string{ - "AuthorizedBy": "Authorized-By", - "DebitCert": "Debit-Cert", - "ExpiryDate": "Expiry-Date", - "LastCert": "Last-Cert", - "LastCreditSerial": "Last-Credit-Serial", - "LastDebitSerial": "Last-Debit-Serial", - "MaturityDate": "Maturity-Date", -} - -type Draft struct { - Drawer string - Beneficiary string - Amount int64 - Denomination string - Issuer string - AuthorizedBy string - MaturityDate *int64 // optional - ExpiryDate *int64 // optional - Nonce string - Notes *string // optional -} - -type Notice struct { - Date int64 - AuthorizedBy string - Notes *string // optional - References []string // may be empty (startup notice) -} - -type Cert struct { - Holder string - Serial int64 - Balance int64 - Denomination string - Issuer string - Date int64 - AuthorizedBy string - Notes *string // optional - LastDebitSerial int64 // 0 if none - LastCreditSerial int64 // 0 if none - LastCert *string // nil if serial == 1 - References []string - Difference int64 - Draft string -} - -type DebitCert struct { - Cert - Beneficiary string -} - -type CreditCert struct { - Cert - Drawer string - DebitCert string -} - -type BounceCert struct { - Drawer string - Draft string - LastCert *string // optional - Balance int64 // 0 if none - Date int64 - AuthorizedBy string - Notes *string // optional - References []string -} - -func ToCert(v interface{}) (cert *Cert, err error) { - cert = new(Cert) - switch x := v.(type) { - case *DebitCert: - cert = &x.Cert - case *CreditCert: - cert = &x.Cert - default: - err = fmt.Errorf("ToCert: only debit or credit document can be converted to cert") - } - return -} - -func cleanBody(s []byte) []byte { - nl := []byte{'\n'} - a := bytes.Split(s, nl) - for i := range a { - a[i] = bytes.TrimRight(a[i], " \t") - } - return bytes.Join(a, nl) -} - -// 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 -func Parse(s []byte) (iv interface{}, c *Signed, err error) { - c, err = ParseSigned(s) - if err != nil { - return - } - doc, err := ParseDocument(c.Body) - if err != nil { - return - } - iv, err = ParseStruct(doc) - return -} - -// format and sign an epoint document -func Format(iv interface{}, key *openpgp.Entity) (s []byte, c *Signed, err error) { - doc, err := FormatStruct(iv) - if err != nil { - return - } - body, err := FormatDocument(doc) - if err != nil { - return - } - c, err = Sign(body, key) - if err != nil { - return - } - s, err = FormatSigned(c) - return -} - -// verify an epoint document, return the cleaned version as well -func Verify(c *Signed, key openpgp.KeyRing) (err error) { - msg := bytes.NewBuffer(c.Body) - sig := bytes.NewBuffer(c.Signature) - // TODO: verify signature - _, _ = msg, sig - // _, err = openpgp.CheckArmoredDetachedSignature(key, msg, sig) - return -} - -// sign body with given secret key -func Sign(body []byte, key *openpgp.Entity) (c *Signed, err error) { - c = new(Signed) - c.Hash = "SHA256" - c.Body = cleanBody(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 -} - -// split a clear signed document into body and armored signature -func ParseSigned(s []byte) (c *Signed, err error) { - // look for clear signed header - for !bytes.HasPrefix(s, []byte(ClearSignedHeader)) { - _, s = getLine(s) - if len(s) == 0 { - err = fmt.Errorf("ParseSigned: clear signed header is missing") - return - } - } - s = s[len(ClearSignedHeader):] - // end of line after the header - empty, s := getLine(s) - if len(empty) != 0 { - err = fmt.Errorf("ParseSigned: bad clear signed header") - return - } - // skip all hash headers, section 7. - for bytes.HasPrefix(s, []byte("Hash: ")) { - _, s = getLine(s) - } - // skip empty line - empty, s = getLine(s) - if len(empty) != 0 { - err = fmt.Errorf("ParseSigned: expected an empty line after armor headers") - return - } - lines := [][]byte{} - for !bytes.HasPrefix(s, []byte("-----BEGIN")) { - var line []byte - line, s = getLine(s) - // dash unescape, section 7.1. - if bytes.HasPrefix(line, []byte("- ")) { - line = line[2:] - } - // empty values are not supported: "Key: \n" - lines = append(lines, bytes.TrimRight(line, " \t")) - } - c = new(Signed) - // last line is not closed by \n - c.Body = bytes.Join(lines, []byte("\n")) - // signature is just the rest of the input data - c.Signature = s - return -} - -// clean up, check and reencode signature -// used on drafts before calculating the signed document hash -func CleanSigned(c *Signed) (err error) { - b, err := armor.Decode(bytes.NewBuffer(c.Signature)) - if err != nil { - return - } - if b.Type != openpgp.SignatureType { - err = fmt.Errorf("CleanSigned: invalid armored signature type") - return - } - p, err := packet.Read(b.Body) - if err != nil { - return - } - sig, ok := p.(*packet.Signature) - if !ok { - err = fmt.Errorf("CleanSigned: invalid signature packet") - return - } - // section 5.2.3 - if sig.SigType != packet.SigTypeText { - err = fmt.Errorf("CleanSigned: expected text signature") - return - } - switch sig.Hash { - case crypto.SHA1: - c.Hash = "SHA1" - case crypto.SHA256: - c.Hash = "SHA256" - default: - err = fmt.Errorf("CleanSigned: expected SHA1 or SHA256 signature hash") - return - } - // TODO: check CreationTime and other subpackets - if sig.SigLifetimeSecs != nil && *sig.SigLifetimeSecs != 0 { - err = fmt.Errorf("CleanSigned: signature must not expire") - return - } - out := new(bytes.Buffer) - w, err := armor.Encode(out, openpgp.SignatureType, nil) - if err != nil { - return - } - err = sig.Serialize(w) - if err != nil { - return - } - err = w.Close() - if err != nil { - return - } - c.Signature = out.Bytes() - return -} - -// create clear signed document -func FormatSigned(c *Signed) (data []byte, err error) { - s := ClearSignedHeader + "\n" - if c.Hash != "" { - s += "Hash: " + c.Hash + "\n" - } - s += "\n" - s += string(c.Body) - s += "\n" - s += string(c.Signature) - data = []byte(s) - return -} - -// parse type and fields of a document body -func ParseDocument(body []byte) (doc *Document, err error) { - // parse content type header first - fields, s, err := ParseFields(body) - if err != nil { - return - } - ctype, ok := fields["Content-Type"] - if len(fields) != 1 || !ok { - return nil, fmt.Errorf("ParseBody: expected a single Content-Type header field") - } - doc = new(Document) - for k, v := range ContentType { - if ctype == v { - doc.Type = k - break - } - } - if doc.Type == "" { - return nil, fmt.Errorf("ParseBody: unknown Content-Type: %s", ctype) - } - // TODO: doc.Order - doc.Fields, s, err = ParseFields(s) - if err == nil && len(s) > 0 { - err = fmt.Errorf("ParseBody: extra data after fields: %q", s) - } - return -} - -// create document body -func FormatDocument(doc *Document) (body []byte, err error) { - ctype, ok := ContentType[doc.Type] - if !ok { - err = fmt.Errorf("FormatDocument: unknown document type: %s", doc.Type) - return - } - s := "Content-Type: " + ctype + "\n\n" - for _, k := range doc.Order { - s += k + ": " + doc.Fields[k] + "\n" - } - return []byte(s), nil -} - -// parse doc fields into a struct according to the document type -func parseStruct(v reflect.Value, fields map[string]string, seen map[string]bool) (err error) { - t := v.Type() - n := v.NumField() - for i := 0; i < n && err == nil; i++ { - ft := t.Field(i) - fv := v.Field(i) - if ft.Anonymous && fv.Kind() == reflect.Struct { - err = parseStruct(fv, fields, seen) - continue - } - key := fieldname[ft.Name] - if key == "" { - key = ft.Name - } - s, ok := fields[key] - if !ok { - if fv.Kind() == reflect.Ptr { - // missing optional key: leave the pointer as nil - continue - } - return fmt.Errorf("ParseStruct: field %s of %s is missing\n", key, t.Name()) - } - seen[key] = true - 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 = parseId(s) - fv.SetString(val) - case "text": - var val string - val, err = parseString(s) - fv.SetString(val) - case "int": - var val int64 - val, err = strconv.Atoi64(s) - fv.SetInt(val) - case "date": - var val int64 - 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 { - val[j], err = parseId(id) - if err != nil { - return - } - } - fv.Set(reflect.ValueOf(val)) - default: - panic("bad field type " + key + " " + fieldtype[key]) - } - } - return -} - -func ParseStruct(doc *Document) (iv interface{}, err error) { - switch doc.Type { - case "Draft": - iv = new(Draft) - case "Notice": - iv = new(Notice) - case "DebitCert": - iv = new(DebitCert) - case "CreditCert": - iv = new(CreditCert) - case "BounceCert": - iv = new(BounceCert) - default: - err = fmt.Errorf("ParseStruct: unkown doc type: %s", doc.Type) - return - } - seen := make(map[string]bool) - err = parseStruct(reflect.ValueOf(iv).Elem(), doc.Fields, seen) - if err != nil { - return - } - if len(doc.Fields) != len(seen) { - for f := range doc.Fields { - if !seen[f] { - err = fmt.Errorf("ParseStruct: unknown field %s in %s", f, doc.Type) - return - } - } - } - return -} - -// turn a struct into a document -func formatStruct(v reflect.Value, doc *Document) (err error) { - t := v.Type() - n := v.NumField() - for i := 0; i < n; i++ { - ft := t.Field(i) - fv := v.Field(i) - if ft.Anonymous && fv.Kind() == reflect.Struct { - err = formatStruct(fv, doc) - if err != nil { - return - } - continue - } - key := fieldname[ft.Name] - if key == "" { - 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()) - case "text": - val = formatString(fv.String()) - case "int": - val = strconv.Itoa64(fv.Int()) - case "date": - val = formatDate(fv.Int()) - case "ids": - k := fv.Len() - for j := 0; j < k; j++ { - if j > 0 { - val += "\n " - } - val += formatId(fv.Index(j).String()) - } - default: - panic("bad field type " + key + " " + fieldtype[key]) - } - setval: - doc.Fields[key] = val - doc.Order = append(doc.Order, key) - } - return -} - -// turn a struct into a document -func FormatStruct(iv interface{}) (doc *Document, err error) { - v := reflect.ValueOf(iv) - if v.Kind() != reflect.Ptr || v.IsNil() || v.Elem().Kind() != reflect.Struct { - panic("input is not a pointer to struct") - } - doc = new(Document) - doc.Type = v.Elem().Type().Name() - doc.Fields = make(map[string]string) - err = formatStruct(v.Elem(), doc) - return -} - -func ParseFields(s []byte) (fields map[string]string, rest []byte, err error) { - rest = s - fields = make(map[string]string) - 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 last field is consumed - if len(line) == 0 { - break - } - if line[0] == ' ' && key != "" { - // "Key: v1\n v2\n" is equivalent to "Key: v1 v2\n" - fields[key] += string(line) - continue - } - 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 ':'") - return - } - key = string(line[:i]) - if _, ok := fields[key]; ok { - err = fmt.Errorf("ParseFields: repeated fields are not allowed") - return - } - 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) { - // check if hex decodable - // TODO: length check - dst := make([]byte, len(s)/2) - _, err := hex.Decode(dst, []byte(s)) - return s, err -} - -func formatId(s string) string { - return s -} - -func parseString(s string) (string, error) { - if len(s) > MaxValueLength { - return "", fmt.Errorf("parseString: length limit is exceeded") - } - return s, nil -} - -func formatString(s string) string { - return s -} - -func parseDate(s string) (int64, error) { - // TODO: fractional seconds? - t, err := time.Parse(time.RFC3339, s) - if err != nil { - return 0, err - } - return t.Seconds(), nil -} - -func formatDate(i int64) string { - return time.SecondsToUTC(i).Format(time.RFC3339) -} - -func getLine(data []byte) (line, rest []byte) { - i := bytes.IndexByte(data, '\n') - j := i + 1 - if i < 0 { - i = len(data) - j = i - } else if i > 0 && data[i-1] == '\r' { - i-- - } - return data[:i], data[j:] -} diff --git a/document/document_test.go b/document/document_test.go deleted file mode 100644 index 982f3a9..0000000 --- a/document/document_test.go +++ /dev/null @@ -1,229 +0,0 @@ -package document - -import ( - "testing" -) - -var signedData = []struct { - text string - ok bool - hash string - body string - sig string -}{ - { - `-----BEGIN PGP SIGNED MESSAGE----- -Hash: SHA1 - -body ------BEGIN PGP SIGNATURE----- -sig -`, true, "SHA1", "body", "-----BEGIN PGP SIGNATURE-----\nsig\n"}, - { - `-----BEGIN PGP SIGNED MESSAGE----- -Hash: SHA1 -Hash: SHA256 - -- body ------BEGIN PGP SIGNATURE----- -sig -`, false, "", "body", "-----BEGIN PGP SIGNATURE-----\nsig\n"}, -} - -func TestSigned(t *testing.T) { - for _, x := range signedData { - c, err := ParseSigned([]byte(x.text)) - if err != nil { - t.Errorf("parsing %q failed: %s\n", x.text, err) - continue - } - if string(c.Body) != x.body { - t.Errorf("expected: %q, got %q\n", x.body, c.Body) - } - if string(c.Signature) != x.sig { - t.Errorf("expected: %q, got %q\n", x.sig, c.Signature) - } - } - for _, x := range signedData { - if !x.ok { - continue - } - c := &Signed{x.hash, []byte(x.body), []byte(x.sig)} - s, err := FormatSigned(c) - if err != nil { - t.Errorf("formating %#v failed: %s\n", c, err) - continue - } - if string(s) != x.text { - t.Errorf("expected: %q, got %q\n", x.text, s) - } - } -} - -var docData = []struct { - text string - ok bool - t string - k []string - v []string -}{ - { - `Content-Type: text/vnd.epoint.draft; charset=utf-8 - -K1: v1 -K2-Foo: v 2 -K3: v 3 -`, true, "Draft", []string{"K1", "K2-Foo", "K3"}, []string{"v1", "v 2", "v 3"}}, - { - `Content-Type: text/vnd.epoint.debit; charset=utf-8 - -K1: v1 -K2-Foo: v 2 -K3: v 3 -`, true, "DebitCert", []string{"K1", "K2-Foo", "K3"}, []string{" v1", "v 2", "v 3"}}, - { - `Content-Type: text/vnd.epoint.credit; charset=utf-8 - -K1: v1 -K2-Foo: v 2 -K3: v - 3 -`, false, "CreditCert", []string{"K1", "K2-Foo", "K3"}, []string{"v1", "v 2", "v 3"}}, -} - -func TestDocument(t *testing.T) { - for _, x := range docData { - doc, err := ParseDocument([]byte(x.text)) - if err != nil { - t.Errorf("parsing %q failed: %s\n", x.text, err) - continue - } - if string(doc.Type) != x.t { - t.Errorf("expected: %q, got %q\n", x.t, doc.Type) - } - if len(doc.Fields) != len(x.k) { - t.Errorf("expected: %d fields, got %d\n", len(x.k), len(doc.Fields)) - } - for i, k := range x.k { - if doc.Fields[k] != x.v[i] { - t.Errorf("value of %s expected to be %s, got %s\n", k, x.v[i], doc.Fields[k]) - } - } - } - for _, x := range docData { - if !x.ok { - continue - } - doc := new(Document) - doc.Type = x.t - doc.Fields = make(map[string]string) - doc.Order = x.k - for i, k := range x.k { - doc.Fields[k] = x.v[i] - } - s, err := FormatDocument(doc) - if err != nil { - t.Errorf("formating %#v failed: %s\n", doc, err) - continue - } - if string(s) != x.text { - t.Errorf("expected: %q, got %q\n", x.text, s) - } - } -} - -const draftBody = `Content-Type: text/vnd.epoint.draft; charset=utf-8 - -Drawer: 000000000000000000000000000000000000000A -Beneficiary: 000000000000000000000000000000000000000B -Amount: 1 -Denomination: half euro -Issuer: 000000000000000000000000000000000000000D -Authorized-By: 000000000000000000000000000000000000000C -Maturity-Date: 2011-11-13T12:20:35Z -Expiry-Date: 2011-12-27T09:18:46Z -Nonce: 42 -Notes: some notes -` - -func TestDraft(t *testing.T) { - doc, err := ParseDocument([]byte(draftBody)) - if err != nil { - t.Errorf("ParseDocument failed: %s\n", err) - return - } - iv, err := ParseStruct(doc) - if err != nil { - t.Errorf("ParseStruct %v failed: %s\n", doc, err) - return - } - d, ok := iv.(*Draft) - if !ok { - t.Errorf("expected *Draft got %#v\n", iv) - return - } - doc, err = FormatStruct(d) - if err != nil { - t.Errorf("format %v draft failed: %s\n", d, err) - return - } - s, err := FormatDocument(doc) - if err != nil { - t.Errorf("format %v doc failed: %s\n", doc, err) - return - } - if string(s) != draftBody { - t.Errorf("parsed %#v\nexpected: %s\ngot: %s\n", d, draftBody, s) - } -} - -const debitBody = `Content-Type: text/vnd.epoint.debit; charset=utf-8 - -Holder: 0000000000000000000000000000000000000009 -Serial: 13 -Balance: 23 -Denomination: half euro -Issuer: 000000000000000000000000000000000000000B -Date: 2011-11-13T12:20:35Z -Authorized-By: 000000000000000000000000000000000000000A -Notes: - -Last-Debit-Serial: 0 -Last-Credit-Serial: 12 -Last-Cert: 000000000000000000000000000000000000000C -References: 000000000000000000000000000000000000000C - 000000000000000000000000000000000000000F -Difference: 1 -Draft: 000000000000000000000000000000000000000D -Beneficiary: 000000000000000000000000000000000000000E -` - -func TestCert(t *testing.T) { - doc, err := ParseDocument([]byte(debitBody)) - if err != nil { - t.Errorf("ParseDocument failed: %s\n", err) - return - } - iv, err := ParseStruct(doc) - if err != nil { - t.Errorf("ParseStruct %v failed: %s\n", doc, err) - return - } - d, ok := iv.(*DebitCert) - if !ok { - t.Errorf("expected *DebitCert got %#v\n", iv) - return - } - doc, err = FormatStruct(d) - if err != nil { - t.Errorf("format %v draft failed: %s\n", d, err) - return - } - s, err := FormatDocument(doc) - if err != nil { - t.Errorf("format %v doc failed: %s\n", doc, err) - return - } - if string(s) != debitBody { - t.Errorf("parsed %#v\nexpected: %s\ngot: %s\n", d, debitBody, s) - } -} diff --git a/dsakey/Makefile b/dsakey/Makefile deleted file mode 100644 index bd29402..0000000 --- a/dsakey/Makefile +++ /dev/null @@ -1,6 +0,0 @@ -include $(GOROOT)/src/Make.inc - -TARG=epoint/dsakey -GOFILES=dsakey.go - -include $(GOROOT)/src/Make.pkg diff --git a/dsakey/dsakey.go b/dsakey/dsakey.go deleted file mode 100644 index 23ab5fe..0000000 --- a/dsakey/dsakey.go +++ /dev/null @@ -1,139 +0,0 @@ -package dsakey - -import ( - "crypto" - "crypto/dsa" - "crypto/openpgp" - "crypto/openpgp/packet" - "crypto/rand" - "crypto/sha1" - "fmt" - "io" - "math/big" -) - -const P = "C1773C0DEF5C1D75BA556137CBCE0F6EE534034FCE503D7ED1FF7A27E8638EAC3BD627C734E08D1D828B52C39EB602DC63D9544D1734A981AE2408F8037305B548EFE457E2A79EB511CFF11A0C3DB05CF64971A6AF3EF191D3EBA0841AAAC3BECF4B6CF199EDD59C732BA642A0074BAE1DC3CF724F830930C898B1865F597EF7" -const Q = "DCA9E7C9FDC18CB0B8E9A80E487F96438147EF75" -const G = "502FF28CC4D7BC1100123C9227994341C29773BFBD8D7E8FFED6D87A9D82FE573744AC8E4CCAE93E3A017A6388921CA5B0C9349B249EF87AB30AE01B3C9FD723001CB25E560CA6C25EDFC97613B41346D0597C2ECA2BED7BC6C9A032049B3FFF9AED462D09651A5995DB8E5E111384AC7B62CBAD827009269FC79D3E4E6D8AA3" - -func PrivKey(r []byte) *dsa.PrivateKey { - priv := new(dsa.PrivateKey) - priv.Parameters.P, _ = new(big.Int).SetString(P, 16) - priv.Parameters.Q, _ = new(big.Int).SetString(Q, 16) - priv.Parameters.G, _ = new(big.Int).SetString(G, 16) - - // q > 2^159 prime - // x = sha1(r) - // if x == 0 then x = q - 1 - // if x == q then x = q - 2 - // if x > q then x = x mod q - - x := new(big.Int) - h := sha1.New() - h.Write(r) - x.SetBytes(h.Sum()) - if x.Sign() == 0 { - x.Sub(priv.Q, big.NewInt(1)) - } - switch x.Cmp(priv.Q) { - case 0: - x.Sub(priv.Q, big.NewInt(2)) - case 1: - x.Sub(x, priv.Q) - } - priv.X = x - priv.Y = new(big.Int) - priv.Y.Exp(priv.G, x, priv.P) - return priv -} - -func GenKey() (priv *dsa.PrivateKey, err error) { - x := make([]byte, len(Q)/2) - _, err = io.ReadFull(rand.Reader, x) - priv = PrivKey(x) - return -} - -// NewEntity returns an Entity that contains a fresh DSA private key with a -// single identity composed of the given full name, comment and email, any of -// which may be empty but must not contain any of "()<>\x00". -func NewEntity(priv *dsa.PrivateKey, currentTimeSecs int64, name, comment, email string) (e *openpgp.Entity, err error) { - uid := packet.NewUserId(name, comment, email) - if uid == nil { - return nil, fmt.Errorf("NewEntity: invalid argument: user id field contained invalid characters") - } - t := uint32(currentTimeSecs) - e = &openpgp.Entity{ - PrimaryKey: packet.NewDSAPublicKey(t, &priv.PublicKey, false /* not a subkey */ ), - PrivateKey: packet.NewDSAPrivateKey(t, priv, false /* not a subkey */ ), - Identities: make(map[string]*openpgp.Identity), - } - isPrimaryId := true - e.Identities[uid.Id] = &openpgp.Identity{ - Name: uid.Name, - UserId: uid, - SelfSignature: &packet.Signature{ - CreationTime: t, - SigType: packet.SigTypePositiveCert, - PubKeyAlgo: packet.PubKeyAlgoDSA, - Hash: crypto.SHA256, - IsPrimaryId: &isPrimaryId, - FlagsValid: true, - FlagSign: true, - FlagCertify: true, - IssuerKeyId: &e.PrimaryKey.KeyId, - }, - } - /* - e.Subkeys = make([]Subkey, 1) - e.Subkeys[0] = Subkey{ - PublicKey: packet.NewRSAPublicKey(t, &encryptingPriv.PublicKey, true), - PrivateKey: packet.NewRSAPrivateKey(t, encryptingPriv, true), - Sig: &packet.Signature{ - CreationTime: t, - SigType: packet.SigTypeSubkeyBinding, - PubKeyAlgo: packet.PubKeyAlgoRSA, - Hash: crypto.SHA256, - FlagsValid: true, - FlagEncryptStorage: true, - FlagEncryptCommunications: true, - IssuerKeyId: &e.PrimaryKey.KeyId, - }, - } - */ - return -} - -// simple key generation for obligation issuer clients -func NewIssuerEntity(r []byte, denomination string) (e *openpgp.Entity, err error) { - return NewEntity(PrivKey(r), 0, "Issuer", denomination, "") -} -// simple key generation for obligation holder clients -func NewHolderEntity(r []byte, issuer, denomination string) (e *openpgp.Entity, err error) { - return NewEntity(PrivKey(r), 0, "Holder of "+issuer, denomination, "") -} - -// check the issuer and denomination associated with the given pgp key -func CheckEntity(e *openpgp.Entity) (isIssuer bool, issuer, denomination string, err error) { - // TODO: allow non-epoint uids - if len(e.Identities) != 1 { - err = fmt.Errorf("CheckEntity: expected one identity") - return - } - for _, i := range e.Identities { - denomination = i.UserId.Comment - if i.UserId.Name == "Issuer" { - isIssuer = true - issuer = fmt.Sprintf("%X", e.PrimaryKey.Fingerprint) - return - } - prefix := "Holder of " - if i.UserId.Name[:len(prefix)] == prefix { - issuer = i.UserId.Name[len(prefix):] - return - } - break - } - err = fmt.Errorf("CheckENtity: invalid userid") - return -} diff --git a/dsakey/dsakey_test.go b/dsakey/dsakey_test.go deleted file mode 100644 index 34df359..0000000 --- a/dsakey/dsakey_test.go +++ /dev/null @@ -1,118 +0,0 @@ -package dsakey - -import ( - "bytes" - "crypto/openpgp" - "fmt" - "testing" - "time" -) - -func testSignAndVerify(t *testing.T, priv *openpgp.Entity) { - msg := []byte("testing") - w := new(bytes.Buffer) - err := openpgp.DetachSign(w, priv, bytes.NewBuffer(msg)) - if err != nil { - t.Errorf("error signing: %s", err) - return - } - - _, err = openpgp.CheckDetachedSignature(openpgp.EntityList{priv}, bytes.NewBuffer(msg), w) - if err != nil { - t.Errorf("Verify failed: %s", err) - } -} - -func TestKey(t *testing.T) { - key, err := GenKey() - if err != nil { - t.Errorf("gen dsa key failed: %s", err) - return - } - priv, err := NewEntity(key, time.Seconds(), "a", "b", "c") - if err != nil { - t.Errorf("new entity failed: %s", err) - } else { - testSignAndVerify(t, priv) - } -} - -func TestGenIssuer(t *testing.T) { - denomination := "1/100 EUR" - priv, err := NewIssuerEntity([]byte("issuer-rand"), denomination) - if err != nil { - t.Errorf("new entity failed: %s", err) - } else { - testSignAndVerify(t, priv) - } - wpriv := new(bytes.Buffer) - err = priv.SerializePrivate(wpriv) - if err != nil { - t.Errorf("priv key serialization failed: %s", err) - return - } - wpub := new(bytes.Buffer) - err = priv.Serialize(wpub) - if err != nil { - t.Errorf("pub key serialization failed: %s", err) - return - } - es, err := openpgp.ReadKeyRing(wpub) - if err != nil { - t.Errorf("pub key parsing failed: %s", err) - return - } - isIssuer, issuer, denom, err := CheckEntity(es[0]) - if err != nil { - t.Errorf("pub key parsing failed: %s", err) - return - } - if !isIssuer { - t.Errorf("expected issuer key got: %v", es[0].Identities) - } - issuerfpr := fmt.Sprintf("%X", priv.PrimaryKey.Fingerprint) - if issuer != issuerfpr { - t.Errorf("expected issuer %s got %s", issuerfpr, issuer) - } - if denom != denomination { - t.Errorf("expected denomination %q got %q", denomination, denom) - } - - priv, err = NewHolderEntity([]byte("holder-rand"), issuerfpr, denomination) - if err != nil { - t.Errorf("new entity failed: %s", err) - } else { - testSignAndVerify(t, priv) - } - wpriv = new(bytes.Buffer) - err = priv.SerializePrivate(wpriv) - if err != nil { - t.Errorf("priv key serialization failed: %s", err) - return - } - wpub = new(bytes.Buffer) - err = priv.Serialize(wpub) - if err != nil { - t.Errorf("pub key serialization failed: %s", err) - return - } - es, err = openpgp.ReadKeyRing(wpub) - if err != nil { - t.Errorf("pub key parsing failed: %s", err) - return - } - isIssuer, issuer, denom, err = CheckEntity(es[0]) - if err != nil { - t.Errorf("pub key parsing failed: %s", err) - return - } - if isIssuer { - t.Errorf("expected non-issuer key got: %v", es[0].Identities) - } - if issuer != issuerfpr { - t.Errorf("expected issuer %s got %s", issuerfpr, issuer) - } - if denom != denomination { - t.Errorf("expected denomination %q got %q", denomination, denom) - } -} diff --git a/epoint-server.go b/epoint-server.go deleted file mode 100644 index 425ea45..0000000 --- a/epoint-server.go +++ /dev/null @@ -1,167 +0,0 @@ -package main - -import ( - "crypto/openpgp" - "epoint/logic" - "fmt" - "log" - "net/http" - "os" -) - -const ( - addr = ":8080" - rootdir = "docroot" - seckey = "./key.sec" -) - -var serverkey *openpgp.Entity - -// todo: http header limit: 64K, body limit: 64K - -// Dummy initialization of serverkey -func initkey() (err error) { - f, err := os.Open(seckey) - if err != nil { - return - } - keys, err := openpgp.ReadKeyRing(f) - if err != nil { - f.Close() - return - } - err = f.Close() - if err != nil { - return - } - serverkey = keys[0] - err = os.MkdirAll(rootdir, 0755) - if err != nil { - return - } - f, err = os.Create(rootdir + "/serverkey") - if err != nil { - return - } - err = serverkey.Serialize(f) - if err != nil { - return - } - // TODO: make sure pubkey is replicated and available - err = f.Sync() - if err != nil { - return - } - err = f.Close() - return -} - -func httpError(w http.ResponseWriter, code int, msg string) { - log.Printf("error: %d %s", code, msg) - http.Error(w, fmt.Sprintf("%d %s\n\n%s\n", code, http.StatusText(code), msg), code) -} - -func httpReq(r *http.Request) string { - err := r.ParseForm() - form := "" - if err != nil { - form = err.Error() - } else { - a := []string{} - for k := range r.Form { - a = append(a, k) - } - form = fmt.Sprintf("%v", a) - } - return fmt.Sprintf("%s %s params:%s", r.Method, r.URL.Raw, form) -} - -func defaultHandler(w http.ResponseWriter, r *http.Request) { - log.Printf("%s %s", r.RemoteAddr, httpReq(r)) - fmt.Fprintf(w, "not implemented: %s %s\n", r.Method, r.URL.Raw) -} - -func submitHandler(w http.ResponseWriter, r *http.Request) { - log.Printf("%s %s", r.RemoteAddr, httpReq(r)) - draft := r.FormValue("draft") - debit := r.FormValue("debit") - key := r.FormValue("key") - switch { - case draft != "": - cert, err := logic.EvalDraft([]byte(draft), serverkey) - if err != nil { - msg := fmt.Sprintf("eval draft failed: %s", err) - httpError(w, 404, msg) - } else { - w.Write(cert) - } - case debit != "": - cert, err := logic.EvalDebitCert([]byte(debit), serverkey) - if err != nil { - msg := fmt.Sprintf("eval debit failed: %s", err) - httpError(w, 404, msg) - } else { - w.Write(cert) - } - case key != "": - err := logic.AddKeys([]byte(key)) - if err != nil { - msg := fmt.Sprintf("add keys failed: %s", err) - httpError(w, 404, msg) - } else { - w.Write([]byte("ok\nTODO: create cert 1 here?")) - } - default: - msg := fmt.Sprintf("expected key, draft or debit param, got: %s", httpReq(r)) - httpError(w, 404, msg) - } -} - -func main() { - err := initkey() - if err != nil { - log.Fatal(err) - } - err = logic.Init(rootdir) - if err != nil { - log.Fatal(err) - } - err = logic.StoreSk(serverkey) - if err != nil { - log.Fatal(err) - } - - // TODO: url from key - f, err := os.Create(rootdir + "/form.html") - if err != nil { - log.Fatal(err) - } - _, _ = fmt.Fprintf(f, `epoint-server submit form -

epoint-server submit form

-

web form

-

submit one document at once -

-

key:
-

draft:
-

debit:
-

-

-

command line

-
-curl --data-urlencode name@path/to/file.txt host/submit
-
-where 'name' is 'key', 'draft' or 'debit'. - -`, addr) - _ = f.Close() - - // queries - http.Handle("/", http.FileServer(http.Dir(rootdir))) - - // actions - // withdraw, draw, deposit, process, clear - http.HandleFunc("/submit", submitHandler) - - log.Printf("start service on %s, server key id: %X\n", addr, serverkey.PrimaryKey.Fingerprint) - log.Fatal(http.ListenAndServe(addr, nil)) -} diff --git a/logic/Makefile b/logic/Makefile deleted file mode 100644 index 89942c5..0000000 --- a/logic/Makefile +++ /dev/null @@ -1,6 +0,0 @@ -include $(GOROOT)/src/Make.inc - -TARG=epoint/logic -GOFILES=logic.go - -include $(GOROOT)/src/Make.pkg diff --git a/logic/logic.go b/logic/logic.go deleted file mode 100644 index 602f458..0000000 --- a/logic/logic.go +++ /dev/null @@ -1,501 +0,0 @@ -package logic - -// main transfer logic - -import ( - "bytes" - "crypto/openpgp" - "epoint/document" - "epoint/dsakey" - "epoint/store" - "fmt" - "time" -) - -// TODO: do in docs? -const IntLimit = 1e15 - -var db *store.Conn - -func StoreSk(sk *openpgp.Entity) (err error) { - // TODO: initkey should save serverkey in db - b := new(bytes.Buffer) - err = sk.Serialize(b) - if err != nil { - return - } - return db.Set("key", fmt.Sprintf("%X", sk.PrimaryKey.Fingerprint), b.Bytes()) -} - -func GetKeys(fpr string) (es openpgp.EntityList, err error) { - b, err := db.Get("key", fpr) - if err != nil { - return - } - es, err = openpgp.ReadKeyRing(bytes.NewBuffer(b)) - if err != nil { - // internal error: pubkey cannot be parsed - return - } - return -} - -func AddKeys(d []byte) (err error) { - entities, err := openpgp.ReadArmoredKeyRing(bytes.NewBuffer(d)) - if err != nil { - return - } - // TODO: allow multiple key uploads at once? - if len(entities) > 100 { - err = fmt.Errorf("expected at most 100 keys; got %d", len(entities)) - return - } - for _, e := range entities { - // TODO: various checks.. - isIssuer, issuer, denom, err1 := dsakey.CheckEntity(e) - err = err1 - if err != nil { - // TODO.. - continue - } - if !isIssuer { - es, err := GetKeys(issuer) - if err != nil { - // TODO.. - continue - } - ok, _, den, err := dsakey.CheckEntity(es[0]) - if !ok || err != nil || den != denom { - // TODO.. - continue - } - } - b := new(bytes.Buffer) - err = e.Serialize(b) - if err != nil { - return - } - fpr := fmt.Sprintf("%X", e.PrimaryKey.Fingerprint) - err = db.Set("key", fpr, b.Bytes()) - if err != nil { - return - } - err = db.Append("keysby/64", fpr[len(fpr)-16:], []byte(fpr)) - if err != nil { - return - } - err = db.Append("keysby/32", fpr[len(fpr)-8:], []byte(fpr)) - if err != nil { - return - } - } - return -} - -func CertByDraft(draftid string) (d []byte, err error) { - certid, err := db.Get("certby/draft", draftid) - if err != nil { - // TODO: we have the draft but the cert is not ready - return - } - d, err = db.Get("cert", string(certid)) - if err != nil { - // shouldn't happen, cert is not available - return - } - return -} - -func CertByDebitCert(debitid string) (d []byte, err error) { - creditid, err := db.Get("certby/debit", debitid) - if err != nil { - // TODO: we have the debit cert but the credit cert is not ready - return - } - d, err = db.Get("cert", string(creditid)) - if err != nil { - // shouldn't happen, cert is not available - return - } - return -} - -// parse clear signed draft and verify it -func ParseDraft(d []byte) (draft *document.Draft, draftid string, err error) { - iv, signed, err := document.Parse(d) - if err != nil { - return - } - draft, ok := iv.(*document.Draft) - if !ok { - err = fmt.Errorf("ParseDraft: expected a draft docuent") - return - } - draftid = document.Id(signed) - - k, err := db.Get("key", draft.Drawer) - if err != nil { - return - } - kr, err := openpgp.ReadKeyRing(bytes.NewBuffer(k)) - if err != nil { - // internal error: pubkey cannot be parsed - return - } - err = document.Verify(signed, kr) - if err != nil { - return - } - _, issuer, denom, err := dsakey.CheckEntity(kr[0]) - if err != nil { - return - } - k, err = db.Get("key", draft.Beneficiary) - if err != nil { - return - } - kr, err = openpgp.ReadKeyRing(bytes.NewBuffer(k)) - if err != nil { - // internal error: pubkey cannot be parsed - return - } - _, issuer2, denom2, err := dsakey.CheckEntity(kr[0]) - if err != nil { - return - } - if draft.Issuer != issuer || - draft.Issuer != issuer2 || - draft.Denomination != denom || - draft.Denomination != denom2 { - err = fmt.Errorf("Issuer or denomination mismatch") - return - } - - // TODO: do various format checks (AuthorizedBy check etc) - if draft.Amount <= 0 || draft.Amount >= IntLimit { - err = fmt.Errorf("draft amount is invalid: %d", draft.Amount) - return - } - return -} - -func ParseDebitCert(d []byte) (cert *document.DebitCert, certid string, err error) { - iv, signed, err := document.Parse(d) - if err != nil { - return - } - cert, ok := iv.(*document.DebitCert) - if !ok { - err = fmt.Errorf("ParseDebitCert: expected a debit docuent") - return - } - - k, err := db.Get("key", cert.AuthorizedBy) - if err != nil { - return - } - // TODO: keep our key at hand - kr, err := openpgp.ReadKeyRing(bytes.NewBuffer(k)) - if err != nil { - // internal error: pubkey cannot be parsed - return - } - // must clean up to make sure the hash is ok - err = document.Verify(signed, kr) - if err != nil { - return - } - - certid = document.Id(signed) - return -} - -func NewDebitCert(draftid string, draft *document.Draft) (*document.DebitCert, error) { - cert := new(document.DebitCert) - cert.Holder = draft.Drawer - cert.Date = time.Seconds() - cert.Denomination = "epoint" - cert.Issuer = draft.Issuer - cert.AuthorizedBy = draft.AuthorizedBy - cert.Difference = -draft.Amount - cert.Draft = draftid - cert.Beneficiary = draft.Beneficiary - - oid, err := db.Get("certby/key", draft.Drawer) - oldcertid := string(oid) - if err != nil { - // first cert: drawer is issuer - if draft.Drawer != draft.Issuer { - return nil, fmt.Errorf("drawer must be the issuer when drawing an empty account") - } - cert.Serial = 1 - cert.Balance = cert.Difference - cert.LastDebitSerial = 0 - cert.LastCreditSerial = 0 - } else { - d, err := db.Get("cert", oldcertid) - if err != nil { - return nil, err - } - iv, _, err := document.Parse(d) - if err != nil { - // internal error - return nil, err - } - // TODO: this is a hack - oldcert, err := document.ToCert(iv) - if err != nil { - // internal error - return nil, err - } - // TODO: sanity checks? oldcert.Holder == draft.Drawer - cert.Serial = oldcert.Serial + 1 - cert.Balance = oldcert.Balance + cert.Difference - if cert.Balance <= -IntLimit { - return nil, fmt.Errorf("balance limit exceeded: %d", cert.Balance) - } - if oldcert.Balance > 0 && cert.Balance < 0 { - return nil, fmt.Errorf("insufficient funds: %d", oldcert.Balance) - } - cert.LastDebitSerial = oldcert.LastDebitSerial - cert.LastCreditSerial = oldcert.LastCreditSerial - if _,ok := iv.(*document.DebitCert); ok { - cert.LastDebitSerial = oldcert.Serial - } else { - cert.LastCreditSerial = oldcert.Serial - } - cert.LastCert = &oldcertid - } - return cert, nil -} - -func NewCreditCert(draftid string, draft *document.Draft, dcertid string, dcert *document.DebitCert) (*document.CreditCert, error) { - cert := new(document.CreditCert) - // TODO: get from old cert instead? - cert.Holder = dcert.Beneficiary - cert.Date = time.Seconds() - // TODO: get these from the cert holder pubkey - cert.Denomination = "epoint" - cert.Issuer = draft.Issuer - cert.AuthorizedBy = dcert.AuthorizedBy // TODO: draft vs dcert vs serverside decision - cert.Difference = -dcert.Difference - cert.Draft = draftid - cert.Drawer = dcert.Holder - cert.DebitCert = dcertid - - oid, err := db.Get("certby/key", dcert.Beneficiary) - oldcertid := string(oid) - if err != nil { - // this is the first cert - cert.Serial = 1 - cert.Balance = cert.Difference - cert.LastDebitSerial = 0 - cert.LastCreditSerial = 0 - } else { - d, err := db.Get("cert", oldcertid) - if err != nil { - // internal error - return nil, err - } - iv, _, err := document.Parse(d) - if err != nil { - // internal error - return nil, err - } - // TODO: this is a hack - oldcert, err := document.ToCert(iv) - if err != nil { - // internal error - return nil, err - } - cert.Serial = oldcert.Serial + 1 - cert.Balance = oldcert.Balance + cert.Difference - if cert.Balance >= IntLimit { - return nil, fmt.Errorf("balance limit exceeded: %d", cert.Balance) - } - cert.LastDebitSerial = oldcert.LastDebitSerial - cert.LastCreditSerial = oldcert.LastCreditSerial - if _,ok := iv.(*document.DebitCert); ok { - cert.LastDebitSerial = oldcert.Serial - } else { - cert.LastCreditSerial = oldcert.Serial - } - cert.LastCert = &oldcertid - } - return cert, nil -} - -func EvalDraft(d []byte, sk *openpgp.Entity) (r []byte, err error) { - draft, draftid, err := ParseDraft(d) - if err != nil { - return - } - _, err = db.Get("draft", draftid) - if err == nil { - // found - // TODO: certby/draft might not be ready even if draft is there - return CertByDraft(draftid) - } - // if draft is ok we save it - err = db.Set("draft", draftid, d) - if err != nil { - // internal error - return - } - // TODO: db.Insert: fails if key exists - s := fmt.Sprintf("%s.%s", draft.Drawer, draft.Nonce) - _, err = db.Get("draftby/key.nonce", s) - if err == nil { - err = fmt.Errorf("draft nonce is not unique") - return - } - err = db.Set("draftby/key.nonce", s, d) - if err != nil { - // internal error - return - } - - // debit cert - cert, err := NewDebitCert(draftid, draft) - if err != nil { - return - } - r, signed, err := document.Format(cert, sk) - certid := document.Id(signed) - err = db.Set("cert", certid, r) - if err != nil { - // internal error - return - } - err = db.Set("certby/draft", draftid, []byte(certid)) - if err != nil { - // internal error - return - } - err = db.Set("certby/key", cert.Holder, []byte(certid)) - if err != nil { - // internal error - return - } - // TODO: append? - err = db.Set("certby/key.serial", fmt.Sprintf("%s.%09d", cert.Holder, cert.Serial), []byte(certid)) - if err != nil { - // internal error - return - } - return -} - -func EvalDebitCert(d []byte, sk *openpgp.Entity) (r []byte, err error) { - dcert, dcertid, err := ParseDebitCert(d) - if err != nil { - return - } - r, err = CertByDebitCert(dcertid) - if err == nil { - // found - return - } - // TODO: we only need the draft to know the issuer (+beneficiary) - // it should be in the pubkey - d, err = db.Get("draft", dcert.Draft) - if err != nil { - // internal error - return - } - iv, _, err := document.Parse(d) - if err != nil { - // internal error - return - } - draft, ok := iv.(*document.Draft) - if !ok { - // internal error - err = fmt.Errorf("EvalDebitCert: expected draft from internal db") - return - } - - // credit side - // TODO: check pubkey etc - cert, err := NewCreditCert(dcert.Draft, draft, dcertid, dcert) - if err != nil { - // internal error - return - } - r, signed, err := document.Format(cert, sk) - if err != nil { - // internal error - return - } - certid := document.Id(signed) - err = db.Set("cert", certid, r) - if err != nil { - // internal error - return - } - err = db.Set("certby/debit", dcertid, []byte(certid)) - if err != nil { - // internal error - return - } - err = db.Set("certby/key", cert.Holder, []byte(certid)) - if err != nil { - // internal error - return - } - // TODO: append? - err = db.Set("certby/key.serial", fmt.Sprintf("%s.%09d", cert.Holder, cert.Serial), []byte(certid)) - if err != nil { - // internal error - return - } - return -} - -func Init(rootdir string) (err error) { - db, err = store.Open(rootdir) - if err != nil { - return - } - err = db.Ensure("key") - if err != nil { - return - } - err = db.Ensure("cert") - if err != nil { - return - } - err = db.Ensure("draft") - if err != nil { - return - } - err = db.Ensure("certby/draft") - if err != nil { - return - } - err = db.Ensure("certby/debit") - if err != nil { - return - } - err = db.Ensure("certby/key") - if err != nil { - return - } - err = db.Ensure("certby/key.serial") - if err != nil { - return - } - err = db.Ensure("draftby/key.nonce") - if err != nil { - return - } - err = db.Ensure("keysby/64") - if err != nil { - return - } - err = db.Ensure("keysby/32") - if err != nil { - return - } - return -} diff --git a/pkg/Makefile b/pkg/Makefile new file mode 100644 index 0000000..4917a98 --- /dev/null +++ b/pkg/Makefile @@ -0,0 +1,49 @@ +include $(GOROOT)/src/Make.inc + +all: install + +DIRS=\ + document\ + key\ + server\ + store\ + ../cmd/genkey\ + ../cmd/epoint-client\ + ../cmd/epoint-server\ + +clean.dirs: $(addsuffix .clean, $(DIRS)) +install.dirs: $(addsuffix .install, $(DIRS)) +nuke.dirs: $(addsuffix .nuke, $(DIRS)) +test.dirs: $(addsuffix .test, $(DIRS)) +testshort.dirs: $(addsuffix .testshort, $(DIRS)) + +%.clean: + +$(MAKE) -C $* clean + +%.install: + +@echo install $* + +@$(MAKE) -C $* install.clean >$*/build.out 2>&1 || (echo INSTALL FAIL $*; cat $*/build.out; exit 1) + +%.nuke: + +$(MAKE) -C $* nuke + +%.test: + +@echo test $* + +@$(MAKE) -C $* test.clean >$*/test.out 2>&1 || (echo TEST FAIL $*; cat $*/test.out; exit 1) + +clean: clean.dirs + +install: install.dirs + +test: test.dirs + +nuke: nuke.dirs + rm -rf "$(GOROOT)"/pkg/$(GOOS)_$(GOARCH)/epoint + +deps: + ./deps.sh + +echo-dirs: + @echo $(DIRS) + +-include Make.deps diff --git a/pkg/deps.sh b/pkg/deps.sh new file mode 100755 index 0000000..123ad3a --- /dev/null +++ b/pkg/deps.sh @@ -0,0 +1,43 @@ +#!/bin/sh + +OUT="Make.deps" +TMP="Make.deps.tmp" + +if [ -f $OUT ] && ! [ -w $OUT ]; then + echo "$0: $OUT is read-only; aborting." 1>&2 + exit 1 +fi + +# Get list of directories from Makefile +dirs=$(make --no-print-directory echo-dirs) +dirpat=$(echo $dirs C | awk '{ + for(i=1;i<=NF;i++){ + x=$i + gsub("/", "\\/", x) + printf("/^(epoint\\/%s)$/\n", x) + } +}') + +for dir in $dirs; do ( + cd $dir >/dev/null || exit 1 + + sources=$(sed -n 's/^[ ]*\([^ ]*\.go\)[ ]*\\*[ ]*$/\1/p' Makefile) + sources=$(echo $sources | sed 's/\$(GOOS)/'$GOOS'/g') + sources=$(echo $sources | sed 's/\$(GOARCH)/'$GOARCH'/g') + # /dev/null here means we get an empty dependency list if $sources is empty + # instead of listing every file in the directory. + sources=$(ls $sources /dev/null 2> /dev/null) # remove .s, .c, etc. + + deps=$( + sed -n '/^import.*"/p; /^import[ \t]*(/,/^)/p' $sources /dev/null | + cut -d '"' -f2 | + awk "$dirpat" | + grep -v "^$dir\$" | + sed 's/epoint\///;s/$/.install/' | + sort -u + ) + + echo $dir.install: $deps +) done > $TMP + +mv $TMP $OUT diff --git a/pkg/document/Makefile b/pkg/document/Makefile new file mode 100644 index 0000000..91080d9 --- /dev/null +++ b/pkg/document/Makefile @@ -0,0 +1,7 @@ +include $(GOROOT)/src/Make.inc + +TARG=epoint/document +GOFILES=\ + document.go + +include $(GOROOT)/src/Make.pkg diff --git a/pkg/document/document.go b/pkg/document/document.go new file mode 100644 index 0000000..0c8b0f4 --- /dev/null +++ b/pkg/document/document.go @@ -0,0 +1,695 @@ +// Package document implements epoint document parsing and creation. +// +// An epoint document is an OpenPGP (RFC 4880) clear signed +// utf-8 text of key-value pairs. +// The body contains a content-type MIME header so the document +// can be used in OpenPGP/MIME (RFC 3156) emails. +// The format of the key-value pairs are similar to MIME header +// fields: keys and values are separated by ": ", repeated keys +// are not allowed, long values can be split before a space. +// +// Example: +// +// -----BEGIN PGP SIGNED MESSAGE----- +// Hash: SHA1 +// +// Content-Type: text/plain.epoint.type; charset=utf-8 +// +// Key: Value1 +// Another-Key: Value2 +// Last-Key: Long +// value that spans +// multiple lines +// -----BEGIN PGP SIGNATURE----- +// +// pgp signature +// -----END PGP SIGNATURE----- +package document + +// 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 (require only drawer.nonce to be uniq) +// TODO: denom, issuer from key (key representation: armor?) + +import ( + "bytes" + "crypto" + "crypto/openpgp" + "crypto/openpgp/armor" + "crypto/openpgp/packet" + "crypto/sha1" + "encoding/hex" + "fmt" + "reflect" + "strconv" + "strings" + "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 +var ContentType = map[string]string{ + "Draft": "text/vnd.epoint.draft; charset=utf-8", + "Notice": "text/vnd.epoint.notice; charset=utf-8", + "DebitCert": "text/vnd.epoint.debit; charset=utf-8", + "CreditCert": "text/vnd.epoint.credit; charset=utf-8", + "BounceCert": "text/vnd.epoint.bounce; charset=utf-8", +} + +// OpenPGP signed cleartext document representation +type Signed struct { + // Sign and CleanSigned sets Hash for FormatSigned + // TODO: CreationDate + Hash string + // Signed text (no dash escape, no trailing space, \n new lines) + Body []byte + // Armored detached text signature of the Body + Signature []byte +} + +// parsed epoint document +type Document struct { + Type string + Fields map[string]string + Order []string +} + +var fieldtype = map[string]string{ + "Amount": "int", + "Authorized-By": "id", + "Balance": "int", + "Beneficiary": "id", + "Date": "date", + "Debit-Cert": "id", + "Denomination": "text", + "Difference": "int", + "Draft": "id", + "Drawer": "id", + "Expiry-Date": "date", + "Holder": "id", + "Issuer": "id", + "Last-Cert": "id", + "Last-Credit-Serial": "int", + "Last-Debit-Serial": "int", + "Maturity-Date": "date", + "Nonce": "id", + "Notes": "text", + "References": "ids", + "Serial": "int", +} + +var fieldname = map[string]string{ + "AuthorizedBy": "Authorized-By", + "DebitCert": "Debit-Cert", + "ExpiryDate": "Expiry-Date", + "LastCert": "Last-Cert", + "LastCreditSerial": "Last-Credit-Serial", + "LastDebitSerial": "Last-Debit-Serial", + "MaturityDate": "Maturity-Date", +} + +type Draft struct { + Drawer string + Beneficiary string + Amount int64 + Denomination string + Issuer string + AuthorizedBy string + MaturityDate *int64 // optional + ExpiryDate *int64 // optional + Nonce string + Notes *string // optional +} + +type Notice struct { + Date int64 + AuthorizedBy string + Notes *string // optional + References []string // may be empty (startup notice) +} + +type Cert struct { + Holder string + Serial int64 + Balance int64 + Denomination string + Issuer string + Date int64 + AuthorizedBy string + Notes *string // optional + LastDebitSerial int64 // 0 if none + LastCreditSerial int64 // 0 if none + LastCert *string // nil if serial == 1 + References []string + Difference int64 + Draft string +} + +type DebitCert struct { + Cert + Beneficiary string +} + +type CreditCert struct { + Cert + Drawer string + DebitCert string +} + +type BounceCert struct { + Drawer string + Draft string + LastCert *string // optional + Balance int64 // 0 if none + Date int64 + AuthorizedBy string + Notes *string // optional + References []string +} + +func ToCert(v interface{}) (cert *Cert, err error) { + cert = new(Cert) + switch x := v.(type) { + case *DebitCert: + cert = &x.Cert + case *CreditCert: + cert = &x.Cert + default: + err = fmt.Errorf("ToCert: only debit or credit document can be converted to cert") + } + return +} + +func cleanBody(s []byte) []byte { + nl := []byte{'\n'} + a := bytes.Split(s, nl) + for i := range a { + a[i] = bytes.TrimRight(a[i], " \t") + } + return bytes.Join(a, nl) +} + +// 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 +func Parse(s []byte) (iv interface{}, c *Signed, err error) { + c, err = ParseSigned(s) + if err != nil { + return + } + doc, err := ParseDocument(c.Body) + if err != nil { + return + } + iv, err = ParseStruct(doc) + return +} + +// format and sign an epoint document +func Format(iv interface{}, key *openpgp.Entity) (s []byte, c *Signed, err error) { + doc, err := FormatStruct(iv) + if err != nil { + return + } + body, err := FormatDocument(doc) + if err != nil { + return + } + c, err = Sign(body, key) + if err != nil { + return + } + s, err = FormatSigned(c) + return +} + +// verify an epoint document, return the cleaned version as well +func Verify(c *Signed, key openpgp.KeyRing) (err error) { + msg := bytes.NewBuffer(c.Body) + sig := bytes.NewBuffer(c.Signature) + // TODO: verify signature + _, _ = msg, sig + // _, err = openpgp.CheckArmoredDetachedSignature(key, msg, sig) + return +} + +// sign body with given secret key +func Sign(body []byte, key *openpgp.Entity) (c *Signed, err error) { + c = new(Signed) + c.Hash = "SHA256" + c.Body = cleanBody(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 +} + +// split a clear signed document into body and armored signature +func ParseSigned(s []byte) (c *Signed, err error) { + // look for clear signed header + for !bytes.HasPrefix(s, []byte(ClearSignedHeader)) { + _, s = getLine(s) + if len(s) == 0 { + err = fmt.Errorf("ParseSigned: clear signed header is missing") + return + } + } + s = s[len(ClearSignedHeader):] + // end of line after the header + empty, s := getLine(s) + if len(empty) != 0 { + err = fmt.Errorf("ParseSigned: bad clear signed header") + return + } + // skip all hash headers, section 7. + for bytes.HasPrefix(s, []byte("Hash: ")) { + _, s = getLine(s) + } + // skip empty line + empty, s = getLine(s) + if len(empty) != 0 { + err = fmt.Errorf("ParseSigned: expected an empty line after armor headers") + return + } + lines := [][]byte{} + for !bytes.HasPrefix(s, []byte("-----BEGIN")) { + var line []byte + line, s = getLine(s) + // dash unescape, section 7.1. + if bytes.HasPrefix(line, []byte("- ")) { + line = line[2:] + } + // empty values are not supported: "Key: \n" + lines = append(lines, bytes.TrimRight(line, " \t")) + } + c = new(Signed) + // last line is not closed by \n + c.Body = bytes.Join(lines, []byte("\n")) + // signature is just the rest of the input data + c.Signature = s + return +} + +// clean up, check and reencode signature +// used on drafts before calculating the signed document hash +func CleanSigned(c *Signed) (err error) { + b, err := armor.Decode(bytes.NewBuffer(c.Signature)) + if err != nil { + return + } + if b.Type != openpgp.SignatureType { + err = fmt.Errorf("CleanSigned: invalid armored signature type") + return + } + p, err := packet.Read(b.Body) + if err != nil { + return + } + sig, ok := p.(*packet.Signature) + if !ok { + err = fmt.Errorf("CleanSigned: invalid signature packet") + return + } + // section 5.2.3 + if sig.SigType != packet.SigTypeText { + err = fmt.Errorf("CleanSigned: expected text signature") + return + } + switch sig.Hash { + case crypto.SHA1: + c.Hash = "SHA1" + case crypto.SHA256: + c.Hash = "SHA256" + default: + err = fmt.Errorf("CleanSigned: expected SHA1 or SHA256 signature hash") + return + } + // TODO: check CreationTime and other subpackets + if sig.SigLifetimeSecs != nil && *sig.SigLifetimeSecs != 0 { + err = fmt.Errorf("CleanSigned: signature must not expire") + return + } + out := new(bytes.Buffer) + w, err := armor.Encode(out, openpgp.SignatureType, nil) + if err != nil { + return + } + err = sig.Serialize(w) + if err != nil { + return + } + err = w.Close() + if err != nil { + return + } + c.Signature = out.Bytes() + return +} + +// create clear signed document +func FormatSigned(c *Signed) (data []byte, err error) { + s := ClearSignedHeader + "\n" + if c.Hash != "" { + s += "Hash: " + c.Hash + "\n" + } + s += "\n" + s += string(c.Body) + s += "\n" + s += string(c.Signature) + data = []byte(s) + return +} + +// parse type and fields of a document body +func ParseDocument(body []byte) (doc *Document, err error) { + // parse content type header first + fields, s, err := ParseFields(body) + if err != nil { + return + } + ctype, ok := fields["Content-Type"] + if len(fields) != 1 || !ok { + return nil, fmt.Errorf("ParseBody: expected a single Content-Type header field") + } + doc = new(Document) + for k, v := range ContentType { + if ctype == v { + doc.Type = k + break + } + } + if doc.Type == "" { + return nil, fmt.Errorf("ParseBody: unknown Content-Type: %s", ctype) + } + // TODO: doc.Order + doc.Fields, s, err = ParseFields(s) + if err == nil && len(s) > 0 { + err = fmt.Errorf("ParseBody: extra data after fields: %q", s) + } + return +} + +// create document body +func FormatDocument(doc *Document) (body []byte, err error) { + ctype, ok := ContentType[doc.Type] + if !ok { + err = fmt.Errorf("FormatDocument: unknown document type: %s", doc.Type) + return + } + s := "Content-Type: " + ctype + "\n\n" + for _, k := range doc.Order { + s += k + ": " + doc.Fields[k] + "\n" + } + return []byte(s), nil +} + +// parse doc fields into a struct according to the document type +func parseStruct(v reflect.Value, fields map[string]string, seen map[string]bool) (err error) { + t := v.Type() + n := v.NumField() + for i := 0; i < n && err == nil; i++ { + ft := t.Field(i) + fv := v.Field(i) + if ft.Anonymous && fv.Kind() == reflect.Struct { + err = parseStruct(fv, fields, seen) + continue + } + key := fieldname[ft.Name] + if key == "" { + key = ft.Name + } + s, ok := fields[key] + if !ok { + if fv.Kind() == reflect.Ptr { + // missing optional key: leave the pointer as nil + continue + } + return fmt.Errorf("ParseStruct: field %s of %s is missing\n", key, t.Name()) + } + seen[key] = true + 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 = parseId(s) + fv.SetString(val) + case "text": + var val string + val, err = parseString(s) + fv.SetString(val) + case "int": + var val int64 + val, err = strconv.Atoi64(s) + fv.SetInt(val) + case "date": + var val int64 + 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 { + val[j], err = parseId(id) + if err != nil { + return + } + } + fv.Set(reflect.ValueOf(val)) + default: + panic("bad field type " + key + " " + fieldtype[key]) + } + } + return +} + +func ParseStruct(doc *Document) (iv interface{}, err error) { + switch doc.Type { + case "Draft": + iv = new(Draft) + case "Notice": + iv = new(Notice) + case "DebitCert": + iv = new(DebitCert) + case "CreditCert": + iv = new(CreditCert) + case "BounceCert": + iv = new(BounceCert) + default: + err = fmt.Errorf("ParseStruct: unkown doc type: %s", doc.Type) + return + } + seen := make(map[string]bool) + err = parseStruct(reflect.ValueOf(iv).Elem(), doc.Fields, seen) + if err != nil { + return + } + if len(doc.Fields) != len(seen) { + for f := range doc.Fields { + if !seen[f] { + err = fmt.Errorf("ParseStruct: unknown field %s in %s", f, doc.Type) + return + } + } + } + return +} + +// turn a struct into a document +func formatStruct(v reflect.Value, doc *Document) (err error) { + t := v.Type() + n := v.NumField() + for i := 0; i < n; i++ { + ft := t.Field(i) + fv := v.Field(i) + if ft.Anonymous && fv.Kind() == reflect.Struct { + err = formatStruct(fv, doc) + if err != nil { + return + } + continue + } + key := fieldname[ft.Name] + if key == "" { + 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()) + case "text": + val = formatString(fv.String()) + case "int": + val = strconv.Itoa64(fv.Int()) + case "date": + val = formatDate(fv.Int()) + case "ids": + k := fv.Len() + for j := 0; j < k; j++ { + if j > 0 { + val += "\n " + } + val += formatId(fv.Index(j).String()) + } + default: + panic("bad field type " + key + " " + fieldtype[key]) + } + setval: + doc.Fields[key] = val + doc.Order = append(doc.Order, key) + } + return +} + +// turn a struct into a document +func FormatStruct(iv interface{}) (doc *Document, err error) { + v := reflect.ValueOf(iv) + if v.Kind() != reflect.Ptr || v.IsNil() || v.Elem().Kind() != reflect.Struct { + panic("input is not a pointer to struct") + } + doc = new(Document) + doc.Type = v.Elem().Type().Name() + doc.Fields = make(map[string]string) + err = formatStruct(v.Elem(), doc) + return +} + +func ParseFields(s []byte) (fields map[string]string, rest []byte, err error) { + rest = s + fields = make(map[string]string) + 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 last field is consumed + if len(line) == 0 { + break + } + if line[0] == ' ' && key != "" { + // "Key: v1\n v2\n" is equivalent to "Key: v1 v2\n" + fields[key] += string(line) + continue + } + 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 ':'") + return + } + key = string(line[:i]) + if _, ok := fields[key]; ok { + err = fmt.Errorf("ParseFields: repeated fields are not allowed") + return + } + 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) { + // check if hex decodable + // TODO: length check + dst := make([]byte, len(s)/2) + _, err := hex.Decode(dst, []byte(s)) + return s, err +} + +func formatId(s string) string { + return s +} + +func parseString(s string) (string, error) { + if len(s) > MaxValueLength { + return "", fmt.Errorf("parseString: length limit is exceeded") + } + return s, nil +} + +func formatString(s string) string { + return s +} + +func parseDate(s string) (int64, error) { + // TODO: fractional seconds? + t, err := time.Parse(time.RFC3339, s) + if err != nil { + return 0, err + } + return t.Seconds(), nil +} + +func formatDate(i int64) string { + return time.SecondsToUTC(i).Format(time.RFC3339) +} + +func getLine(data []byte) (line, rest []byte) { + i := bytes.IndexByte(data, '\n') + j := i + 1 + if i < 0 { + i = len(data) + j = i + } else if i > 0 && data[i-1] == '\r' { + i-- + } + return data[:i], data[j:] +} diff --git a/pkg/document/document_test.go b/pkg/document/document_test.go new file mode 100644 index 0000000..982f3a9 --- /dev/null +++ b/pkg/document/document_test.go @@ -0,0 +1,229 @@ +package document + +import ( + "testing" +) + +var signedData = []struct { + text string + ok bool + hash string + body string + sig string +}{ + { + `-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA1 + +body +-----BEGIN PGP SIGNATURE----- +sig +`, true, "SHA1", "body", "-----BEGIN PGP SIGNATURE-----\nsig\n"}, + { + `-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA1 +Hash: SHA256 + +- body +-----BEGIN PGP SIGNATURE----- +sig +`, false, "", "body", "-----BEGIN PGP SIGNATURE-----\nsig\n"}, +} + +func TestSigned(t *testing.T) { + for _, x := range signedData { + c, err := ParseSigned([]byte(x.text)) + if err != nil { + t.Errorf("parsing %q failed: %s\n", x.text, err) + continue + } + if string(c.Body) != x.body { + t.Errorf("expected: %q, got %q\n", x.body, c.Body) + } + if string(c.Signature) != x.sig { + t.Errorf("expected: %q, got %q\n", x.sig, c.Signature) + } + } + for _, x := range signedData { + if !x.ok { + continue + } + c := &Signed{x.hash, []byte(x.body), []byte(x.sig)} + s, err := FormatSigned(c) + if err != nil { + t.Errorf("formating %#v failed: %s\n", c, err) + continue + } + if string(s) != x.text { + t.Errorf("expected: %q, got %q\n", x.text, s) + } + } +} + +var docData = []struct { + text string + ok bool + t string + k []string + v []string +}{ + { + `Content-Type: text/vnd.epoint.draft; charset=utf-8 + +K1: v1 +K2-Foo: v 2 +K3: v 3 +`, true, "Draft", []string{"K1", "K2-Foo", "K3"}, []string{"v1", "v 2", "v 3"}}, + { + `Content-Type: text/vnd.epoint.debit; charset=utf-8 + +K1: v1 +K2-Foo: v 2 +K3: v 3 +`, true, "DebitCert", []string{"K1", "K2-Foo", "K3"}, []string{" v1", "v 2", "v 3"}}, + { + `Content-Type: text/vnd.epoint.credit; charset=utf-8 + +K1: v1 +K2-Foo: v 2 +K3: v + 3 +`, false, "CreditCert", []string{"K1", "K2-Foo", "K3"}, []string{"v1", "v 2", "v 3"}}, +} + +func TestDocument(t *testing.T) { + for _, x := range docData { + doc, err := ParseDocument([]byte(x.text)) + if err != nil { + t.Errorf("parsing %q failed: %s\n", x.text, err) + continue + } + if string(doc.Type) != x.t { + t.Errorf("expected: %q, got %q\n", x.t, doc.Type) + } + if len(doc.Fields) != len(x.k) { + t.Errorf("expected: %d fields, got %d\n", len(x.k), len(doc.Fields)) + } + for i, k := range x.k { + if doc.Fields[k] != x.v[i] { + t.Errorf("value of %s expected to be %s, got %s\n", k, x.v[i], doc.Fields[k]) + } + } + } + for _, x := range docData { + if !x.ok { + continue + } + doc := new(Document) + doc.Type = x.t + doc.Fields = make(map[string]string) + doc.Order = x.k + for i, k := range x.k { + doc.Fields[k] = x.v[i] + } + s, err := FormatDocument(doc) + if err != nil { + t.Errorf("formating %#v failed: %s\n", doc, err) + continue + } + if string(s) != x.text { + t.Errorf("expected: %q, got %q\n", x.text, s) + } + } +} + +const draftBody = `Content-Type: text/vnd.epoint.draft; charset=utf-8 + +Drawer: 000000000000000000000000000000000000000A +Beneficiary: 000000000000000000000000000000000000000B +Amount: 1 +Denomination: half euro +Issuer: 000000000000000000000000000000000000000D +Authorized-By: 000000000000000000000000000000000000000C +Maturity-Date: 2011-11-13T12:20:35Z +Expiry-Date: 2011-12-27T09:18:46Z +Nonce: 42 +Notes: some notes +` + +func TestDraft(t *testing.T) { + doc, err := ParseDocument([]byte(draftBody)) + if err != nil { + t.Errorf("ParseDocument failed: %s\n", err) + return + } + iv, err := ParseStruct(doc) + if err != nil { + t.Errorf("ParseStruct %v failed: %s\n", doc, err) + return + } + d, ok := iv.(*Draft) + if !ok { + t.Errorf("expected *Draft got %#v\n", iv) + return + } + doc, err = FormatStruct(d) + if err != nil { + t.Errorf("format %v draft failed: %s\n", d, err) + return + } + s, err := FormatDocument(doc) + if err != nil { + t.Errorf("format %v doc failed: %s\n", doc, err) + return + } + if string(s) != draftBody { + t.Errorf("parsed %#v\nexpected: %s\ngot: %s\n", d, draftBody, s) + } +} + +const debitBody = `Content-Type: text/vnd.epoint.debit; charset=utf-8 + +Holder: 0000000000000000000000000000000000000009 +Serial: 13 +Balance: 23 +Denomination: half euro +Issuer: 000000000000000000000000000000000000000B +Date: 2011-11-13T12:20:35Z +Authorized-By: 000000000000000000000000000000000000000A +Notes: - +Last-Debit-Serial: 0 +Last-Credit-Serial: 12 +Last-Cert: 000000000000000000000000000000000000000C +References: 000000000000000000000000000000000000000C + 000000000000000000000000000000000000000F +Difference: 1 +Draft: 000000000000000000000000000000000000000D +Beneficiary: 000000000000000000000000000000000000000E +` + +func TestCert(t *testing.T) { + doc, err := ParseDocument([]byte(debitBody)) + if err != nil { + t.Errorf("ParseDocument failed: %s\n", err) + return + } + iv, err := ParseStruct(doc) + if err != nil { + t.Errorf("ParseStruct %v failed: %s\n", doc, err) + return + } + d, ok := iv.(*DebitCert) + if !ok { + t.Errorf("expected *DebitCert got %#v\n", iv) + return + } + doc, err = FormatStruct(d) + if err != nil { + t.Errorf("format %v draft failed: %s\n", d, err) + return + } + s, err := FormatDocument(doc) + if err != nil { + t.Errorf("format %v doc failed: %s\n", doc, err) + return + } + if string(s) != debitBody { + t.Errorf("parsed %#v\nexpected: %s\ngot: %s\n", d, debitBody, s) + } +} diff --git a/pkg/key/Makefile b/pkg/key/Makefile new file mode 100644 index 0000000..d3d31f5 --- /dev/null +++ b/pkg/key/Makefile @@ -0,0 +1,7 @@ +include $(GOROOT)/src/Make.inc + +TARG=epoint/key +GOFILES=\ + key.go + +include $(GOROOT)/src/Make.pkg diff --git a/pkg/key/key.go b/pkg/key/key.go new file mode 100644 index 0000000..195b7c5 --- /dev/null +++ b/pkg/key/key.go @@ -0,0 +1,139 @@ +package key + +import ( + "crypto" + "crypto/dsa" + "crypto/openpgp" + "crypto/openpgp/packet" + "crypto/rand" + "crypto/sha1" + "fmt" + "io" + "math/big" +) + +const P = "C1773C0DEF5C1D75BA556137CBCE0F6EE534034FCE503D7ED1FF7A27E8638EAC3BD627C734E08D1D828B52C39EB602DC63D9544D1734A981AE2408F8037305B548EFE457E2A79EB511CFF11A0C3DB05CF64971A6AF3EF191D3EBA0841AAAC3BECF4B6CF199EDD59C732BA642A0074BAE1DC3CF724F830930C898B1865F597EF7" +const Q = "DCA9E7C9FDC18CB0B8E9A80E487F96438147EF75" +const G = "502FF28CC4D7BC1100123C9227994341C29773BFBD8D7E8FFED6D87A9D82FE573744AC8E4CCAE93E3A017A6388921CA5B0C9349B249EF87AB30AE01B3C9FD723001CB25E560CA6C25EDFC97613B41346D0597C2ECA2BED7BC6C9A032049B3FFF9AED462D09651A5995DB8E5E111384AC7B62CBAD827009269FC79D3E4E6D8AA3" + +func PrivKey(r []byte) *dsa.PrivateKey { + priv := new(dsa.PrivateKey) + priv.Parameters.P, _ = new(big.Int).SetString(P, 16) + priv.Parameters.Q, _ = new(big.Int).SetString(Q, 16) + priv.Parameters.G, _ = new(big.Int).SetString(G, 16) + + // q > 2^159 prime + // x = sha1(r) + // if x == 0 then x = q - 1 + // if x == q then x = q - 2 + // if x > q then x = x mod q + + x := new(big.Int) + h := sha1.New() + h.Write(r) + x.SetBytes(h.Sum()) + if x.Sign() == 0 { + x.Sub(priv.Q, big.NewInt(1)) + } + switch x.Cmp(priv.Q) { + case 0: + x.Sub(priv.Q, big.NewInt(2)) + case 1: + x.Sub(x, priv.Q) + } + priv.X = x + priv.Y = new(big.Int) + priv.Y.Exp(priv.G, x, priv.P) + return priv +} + +func GenKey() (priv *dsa.PrivateKey, err error) { + x := make([]byte, len(Q)/2) + _, err = io.ReadFull(rand.Reader, x) + priv = PrivKey(x) + return +} + +// NewEntity returns an Entity that contains a fresh DSA private key with a +// single identity composed of the given full name, comment and email, any of +// which may be empty but must not contain any of "()<>\x00". +func NewEntity(priv *dsa.PrivateKey, currentTimeSecs int64, name, comment, email string) (e *openpgp.Entity, err error) { + uid := packet.NewUserId(name, comment, email) + if uid == nil { + return nil, fmt.Errorf("NewEntity: invalid argument: user id field contained invalid characters") + } + t := uint32(currentTimeSecs) + e = &openpgp.Entity{ + PrimaryKey: packet.NewDSAPublicKey(t, &priv.PublicKey, false /* not a subkey */ ), + PrivateKey: packet.NewDSAPrivateKey(t, priv, false /* not a subkey */ ), + Identities: make(map[string]*openpgp.Identity), + } + isPrimaryId := true + e.Identities[uid.Id] = &openpgp.Identity{ + Name: uid.Name, + UserId: uid, + SelfSignature: &packet.Signature{ + CreationTime: t, + SigType: packet.SigTypePositiveCert, + PubKeyAlgo: packet.PubKeyAlgoDSA, + Hash: crypto.SHA256, + IsPrimaryId: &isPrimaryId, + FlagsValid: true, + FlagSign: true, + FlagCertify: true, + IssuerKeyId: &e.PrimaryKey.KeyId, + }, + } + /* + e.Subkeys = make([]Subkey, 1) + e.Subkeys[0] = Subkey{ + PublicKey: packet.NewRSAPublicKey(t, &encryptingPriv.PublicKey, true), + PrivateKey: packet.NewRSAPrivateKey(t, encryptingPriv, true), + Sig: &packet.Signature{ + CreationTime: t, + SigType: packet.SigTypeSubkeyBinding, + PubKeyAlgo: packet.PubKeyAlgoRSA, + Hash: crypto.SHA256, + FlagsValid: true, + FlagEncryptStorage: true, + FlagEncryptCommunications: true, + IssuerKeyId: &e.PrimaryKey.KeyId, + }, + } + */ + return +} + +// simple key generation for obligation issuer clients +func NewIssuerEntity(r []byte, denomination string) (e *openpgp.Entity, err error) { + return NewEntity(PrivKey(r), 0, "Issuer", denomination, "") +} +// simple key generation for obligation holder clients +func NewHolderEntity(r []byte, issuer, denomination string) (e *openpgp.Entity, err error) { + return NewEntity(PrivKey(r), 0, "Holder of "+issuer, denomination, "") +} + +// check the issuer and denomination associated with the given pgp key +func CheckEntity(e *openpgp.Entity) (isIssuer bool, issuer, denomination string, err error) { + // TODO: allow non-epoint uids + if len(e.Identities) != 1 { + err = fmt.Errorf("CheckEntity: expected one identity") + return + } + for _, i := range e.Identities { + denomination = i.UserId.Comment + if i.UserId.Name == "Issuer" { + isIssuer = true + issuer = fmt.Sprintf("%X", e.PrimaryKey.Fingerprint) + return + } + prefix := "Holder of " + if i.UserId.Name[:len(prefix)] == prefix { + issuer = i.UserId.Name[len(prefix):] + return + } + break + } + err = fmt.Errorf("CheckENtity: invalid userid") + return +} diff --git a/pkg/key/key_test.go b/pkg/key/key_test.go new file mode 100644 index 0000000..34df359 --- /dev/null +++ b/pkg/key/key_test.go @@ -0,0 +1,118 @@ +package dsakey + +import ( + "bytes" + "crypto/openpgp" + "fmt" + "testing" + "time" +) + +func testSignAndVerify(t *testing.T, priv *openpgp.Entity) { + msg := []byte("testing") + w := new(bytes.Buffer) + err := openpgp.DetachSign(w, priv, bytes.NewBuffer(msg)) + if err != nil { + t.Errorf("error signing: %s", err) + return + } + + _, err = openpgp.CheckDetachedSignature(openpgp.EntityList{priv}, bytes.NewBuffer(msg), w) + if err != nil { + t.Errorf("Verify failed: %s", err) + } +} + +func TestKey(t *testing.T) { + key, err := GenKey() + if err != nil { + t.Errorf("gen dsa key failed: %s", err) + return + } + priv, err := NewEntity(key, time.Seconds(), "a", "b", "c") + if err != nil { + t.Errorf("new entity failed: %s", err) + } else { + testSignAndVerify(t, priv) + } +} + +func TestGenIssuer(t *testing.T) { + denomination := "1/100 EUR" + priv, err := NewIssuerEntity([]byte("issuer-rand"), denomination) + if err != nil { + t.Errorf("new entity failed: %s", err) + } else { + testSignAndVerify(t, priv) + } + wpriv := new(bytes.Buffer) + err = priv.SerializePrivate(wpriv) + if err != nil { + t.Errorf("priv key serialization failed: %s", err) + return + } + wpub := new(bytes.Buffer) + err = priv.Serialize(wpub) + if err != nil { + t.Errorf("pub key serialization failed: %s", err) + return + } + es, err := openpgp.ReadKeyRing(wpub) + if err != nil { + t.Errorf("pub key parsing failed: %s", err) + return + } + isIssuer, issuer, denom, err := CheckEntity(es[0]) + if err != nil { + t.Errorf("pub key parsing failed: %s", err) + return + } + if !isIssuer { + t.Errorf("expected issuer key got: %v", es[0].Identities) + } + issuerfpr := fmt.Sprintf("%X", priv.PrimaryKey.Fingerprint) + if issuer != issuerfpr { + t.Errorf("expected issuer %s got %s", issuerfpr, issuer) + } + if denom != denomination { + t.Errorf("expected denomination %q got %q", denomination, denom) + } + + priv, err = NewHolderEntity([]byte("holder-rand"), issuerfpr, denomination) + if err != nil { + t.Errorf("new entity failed: %s", err) + } else { + testSignAndVerify(t, priv) + } + wpriv = new(bytes.Buffer) + err = priv.SerializePrivate(wpriv) + if err != nil { + t.Errorf("priv key serialization failed: %s", err) + return + } + wpub = new(bytes.Buffer) + err = priv.Serialize(wpub) + if err != nil { + t.Errorf("pub key serialization failed: %s", err) + return + } + es, err = openpgp.ReadKeyRing(wpub) + if err != nil { + t.Errorf("pub key parsing failed: %s", err) + return + } + isIssuer, issuer, denom, err = CheckEntity(es[0]) + if err != nil { + t.Errorf("pub key parsing failed: %s", err) + return + } + if isIssuer { + t.Errorf("expected non-issuer key got: %v", es[0].Identities) + } + if issuer != issuerfpr { + t.Errorf("expected issuer %s got %s", issuerfpr, issuer) + } + if denom != denomination { + t.Errorf("expected denomination %q got %q", denomination, denom) + } +} diff --git a/pkg/server/Makefile b/pkg/server/Makefile new file mode 100644 index 0000000..6835a0e --- /dev/null +++ b/pkg/server/Makefile @@ -0,0 +1,7 @@ +include $(GOROOT)/src/Make.inc + +TARG=epoint/server +GOFILES=\ + server.go + +include $(GOROOT)/src/Make.pkg diff --git a/pkg/server/server.go b/pkg/server/server.go new file mode 100644 index 0000000..5d9af8e --- /dev/null +++ b/pkg/server/server.go @@ -0,0 +1,501 @@ +package server + +// main transfer logic + +import ( + "bytes" + "crypto/openpgp" + "epoint/document" + "epoint/key" + "epoint/store" + "fmt" + "time" +) + +// TODO: do in docs? +const IntLimit = 1e15 + +var db *store.Conn + +func StoreSk(sk *openpgp.Entity) (err error) { + // TODO: initkey should save serverkey in db + b := new(bytes.Buffer) + err = sk.Serialize(b) + if err != nil { + return + } + return db.Set("key", fmt.Sprintf("%X", sk.PrimaryKey.Fingerprint), b.Bytes()) +} + +func GetKeys(fpr string) (es openpgp.EntityList, err error) { + b, err := db.Get("key", fpr) + if err != nil { + return + } + es, err = openpgp.ReadKeyRing(bytes.NewBuffer(b)) + if err != nil { + // internal error: pubkey cannot be parsed + return + } + return +} + +func AddKeys(d []byte) (err error) { + entities, err := openpgp.ReadArmoredKeyRing(bytes.NewBuffer(d)) + if err != nil { + return + } + // TODO: allow multiple key uploads at once? + if len(entities) > 100 { + err = fmt.Errorf("expected at most 100 keys; got %d", len(entities)) + return + } + for _, e := range entities { + // TODO: various checks.. + isIssuer, issuer, denom, err1 := key.CheckEntity(e) + err = err1 + if err != nil { + // TODO.. + continue + } + if !isIssuer { + es, err := GetKeys(issuer) + if err != nil { + // TODO.. + continue + } + ok, _, den, err := key.CheckEntity(es[0]) + if !ok || err != nil || den != denom { + // TODO.. + continue + } + } + b := new(bytes.Buffer) + err = e.Serialize(b) + if err != nil { + return + } + fpr := fmt.Sprintf("%X", e.PrimaryKey.Fingerprint) + err = db.Set("key", fpr, b.Bytes()) + if err != nil { + return + } + err = db.Append("keysby/64", fpr[len(fpr)-16:], []byte(fpr)) + if err != nil { + return + } + err = db.Append("keysby/32", fpr[len(fpr)-8:], []byte(fpr)) + if err != nil { + return + } + } + return +} + +func CertByDraft(draftid string) (d []byte, err error) { + certid, err := db.Get("certby/draft", draftid) + if err != nil { + // TODO: we have the draft but the cert is not ready + return + } + d, err = db.Get("cert", string(certid)) + if err != nil { + // shouldn't happen, cert is not available + return + } + return +} + +func CertByDebitCert(debitid string) (d []byte, err error) { + creditid, err := db.Get("certby/debit", debitid) + if err != nil { + // TODO: we have the debit cert but the credit cert is not ready + return + } + d, err = db.Get("cert", string(creditid)) + if err != nil { + // shouldn't happen, cert is not available + return + } + return +} + +// parse clear signed draft and verify it +func ParseDraft(d []byte) (draft *document.Draft, draftid string, err error) { + iv, signed, err := document.Parse(d) + if err != nil { + return + } + draft, ok := iv.(*document.Draft) + if !ok { + err = fmt.Errorf("ParseDraft: expected a draft docuent") + return + } + draftid = document.Id(signed) + + k, err := db.Get("key", draft.Drawer) + if err != nil { + return + } + kr, err := openpgp.ReadKeyRing(bytes.NewBuffer(k)) + if err != nil { + // internal error: pubkey cannot be parsed + return + } + err = document.Verify(signed, kr) + if err != nil { + return + } + _, issuer, denom, err := key.CheckEntity(kr[0]) + if err != nil { + return + } + k, err = db.Get("key", draft.Beneficiary) + if err != nil { + return + } + kr, err = openpgp.ReadKeyRing(bytes.NewBuffer(k)) + if err != nil { + // internal error: pubkey cannot be parsed + return + } + _, issuer2, denom2, err := key.CheckEntity(kr[0]) + if err != nil { + return + } + if draft.Issuer != issuer || + draft.Issuer != issuer2 || + draft.Denomination != denom || + draft.Denomination != denom2 { + err = fmt.Errorf("Issuer or denomination mismatch") + return + } + + // TODO: do various format checks (AuthorizedBy check etc) + if draft.Amount <= 0 || draft.Amount >= IntLimit { + err = fmt.Errorf("draft amount is invalid: %d", draft.Amount) + return + } + return +} + +func ParseDebitCert(d []byte) (cert *document.DebitCert, certid string, err error) { + iv, signed, err := document.Parse(d) + if err != nil { + return + } + cert, ok := iv.(*document.DebitCert) + if !ok { + err = fmt.Errorf("ParseDebitCert: expected a debit docuent") + return + } + + k, err := db.Get("key", cert.AuthorizedBy) + if err != nil { + return + } + // TODO: keep our key at hand + kr, err := openpgp.ReadKeyRing(bytes.NewBuffer(k)) + if err != nil { + // internal error: pubkey cannot be parsed + return + } + // must clean up to make sure the hash is ok + err = document.Verify(signed, kr) + if err != nil { + return + } + + certid = document.Id(signed) + return +} + +func NewDebitCert(draftid string, draft *document.Draft) (*document.DebitCert, error) { + cert := new(document.DebitCert) + cert.Holder = draft.Drawer + cert.Date = time.Seconds() + cert.Denomination = "epoint" + cert.Issuer = draft.Issuer + cert.AuthorizedBy = draft.AuthorizedBy + cert.Difference = -draft.Amount + cert.Draft = draftid + cert.Beneficiary = draft.Beneficiary + + oid, err := db.Get("certby/key", draft.Drawer) + oldcertid := string(oid) + if err != nil { + // first cert: drawer is issuer + if draft.Drawer != draft.Issuer { + return nil, fmt.Errorf("drawer must be the issuer when drawing an empty account") + } + cert.Serial = 1 + cert.Balance = cert.Difference + cert.LastDebitSerial = 0 + cert.LastCreditSerial = 0 + } else { + d, err := db.Get("cert", oldcertid) + if err != nil { + return nil, err + } + iv, _, err := document.Parse(d) + if err != nil { + // internal error + return nil, err + } + // TODO: this is a hack + oldcert, err := document.ToCert(iv) + if err != nil { + // internal error + return nil, err + } + // TODO: sanity checks? oldcert.Holder == draft.Drawer + cert.Serial = oldcert.Serial + 1 + cert.Balance = oldcert.Balance + cert.Difference + if cert.Balance <= -IntLimit { + return nil, fmt.Errorf("balance limit exceeded: %d", cert.Balance) + } + if oldcert.Balance > 0 && cert.Balance < 0 { + return nil, fmt.Errorf("insufficient funds: %d", oldcert.Balance) + } + cert.LastDebitSerial = oldcert.LastDebitSerial + cert.LastCreditSerial = oldcert.LastCreditSerial + if _,ok := iv.(*document.DebitCert); ok { + cert.LastDebitSerial = oldcert.Serial + } else { + cert.LastCreditSerial = oldcert.Serial + } + cert.LastCert = &oldcertid + } + return cert, nil +} + +func NewCreditCert(draftid string, draft *document.Draft, dcertid string, dcert *document.DebitCert) (*document.CreditCert, error) { + cert := new(document.CreditCert) + // TODO: get from old cert instead? + cert.Holder = dcert.Beneficiary + cert.Date = time.Seconds() + // TODO: get these from the cert holder pubkey + cert.Denomination = "epoint" + cert.Issuer = draft.Issuer + cert.AuthorizedBy = dcert.AuthorizedBy // TODO: draft vs dcert vs serverside decision + cert.Difference = -dcert.Difference + cert.Draft = draftid + cert.Drawer = dcert.Holder + cert.DebitCert = dcertid + + oid, err := db.Get("certby/key", dcert.Beneficiary) + oldcertid := string(oid) + if err != nil { + // this is the first cert + cert.Serial = 1 + cert.Balance = cert.Difference + cert.LastDebitSerial = 0 + cert.LastCreditSerial = 0 + } else { + d, err := db.Get("cert", oldcertid) + if err != nil { + // internal error + return nil, err + } + iv, _, err := document.Parse(d) + if err != nil { + // internal error + return nil, err + } + // TODO: this is a hack + oldcert, err := document.ToCert(iv) + if err != nil { + // internal error + return nil, err + } + cert.Serial = oldcert.Serial + 1 + cert.Balance = oldcert.Balance + cert.Difference + if cert.Balance >= IntLimit { + return nil, fmt.Errorf("balance limit exceeded: %d", cert.Balance) + } + cert.LastDebitSerial = oldcert.LastDebitSerial + cert.LastCreditSerial = oldcert.LastCreditSerial + if _,ok := iv.(*document.DebitCert); ok { + cert.LastDebitSerial = oldcert.Serial + } else { + cert.LastCreditSerial = oldcert.Serial + } + cert.LastCert = &oldcertid + } + return cert, nil +} + +func EvalDraft(d []byte, sk *openpgp.Entity) (r []byte, err error) { + draft, draftid, err := ParseDraft(d) + if err != nil { + return + } + _, err = db.Get("draft", draftid) + if err == nil { + // found + // TODO: certby/draft might not be ready even if draft is there + return CertByDraft(draftid) + } + // if draft is ok we save it + err = db.Set("draft", draftid, d) + if err != nil { + // internal error + return + } + // TODO: db.Insert: fails if key exists + s := fmt.Sprintf("%s.%s", draft.Drawer, draft.Nonce) + _, err = db.Get("draftby/key.nonce", s) + if err == nil { + err = fmt.Errorf("draft nonce is not unique") + return + } + err = db.Set("draftby/key.nonce", s, d) + if err != nil { + // internal error + return + } + + // debit cert + cert, err := NewDebitCert(draftid, draft) + if err != nil { + return + } + r, signed, err := document.Format(cert, sk) + certid := document.Id(signed) + err = db.Set("cert", certid, r) + if err != nil { + // internal error + return + } + err = db.Set("certby/draft", draftid, []byte(certid)) + if err != nil { + // internal error + return + } + err = db.Set("certby/key", cert.Holder, []byte(certid)) + if err != nil { + // internal error + return + } + // TODO: append? + err = db.Set("certby/key.serial", fmt.Sprintf("%s.%09d", cert.Holder, cert.Serial), []byte(certid)) + if err != nil { + // internal error + return + } + return +} + +func EvalDebitCert(d []byte, sk *openpgp.Entity) (r []byte, err error) { + dcert, dcertid, err := ParseDebitCert(d) + if err != nil { + return + } + r, err = CertByDebitCert(dcertid) + if err == nil { + // found + return + } + // TODO: we only need the draft to know the issuer (+beneficiary) + // it should be in the pubkey + d, err = db.Get("draft", dcert.Draft) + if err != nil { + // internal error + return + } + iv, _, err := document.Parse(d) + if err != nil { + // internal error + return + } + draft, ok := iv.(*document.Draft) + if !ok { + // internal error + err = fmt.Errorf("EvalDebitCert: expected draft from internal db") + return + } + + // credit side + // TODO: check pubkey etc + cert, err := NewCreditCert(dcert.Draft, draft, dcertid, dcert) + if err != nil { + // internal error + return + } + r, signed, err := document.Format(cert, sk) + if err != nil { + // internal error + return + } + certid := document.Id(signed) + err = db.Set("cert", certid, r) + if err != nil { + // internal error + return + } + err = db.Set("certby/debit", dcertid, []byte(certid)) + if err != nil { + // internal error + return + } + err = db.Set("certby/key", cert.Holder, []byte(certid)) + if err != nil { + // internal error + return + } + // TODO: append? + err = db.Set("certby/key.serial", fmt.Sprintf("%s.%09d", cert.Holder, cert.Serial), []byte(certid)) + if err != nil { + // internal error + return + } + return +} + +func Init(rootdir string) (err error) { + db, err = store.Open(rootdir) + if err != nil { + return + } + err = db.Ensure("key") + if err != nil { + return + } + err = db.Ensure("cert") + if err != nil { + return + } + err = db.Ensure("draft") + if err != nil { + return + } + err = db.Ensure("certby/draft") + if err != nil { + return + } + err = db.Ensure("certby/debit") + if err != nil { + return + } + err = db.Ensure("certby/key") + if err != nil { + return + } + err = db.Ensure("certby/key.serial") + if err != nil { + return + } + err = db.Ensure("draftby/key.nonce") + if err != nil { + return + } + err = db.Ensure("keysby/64") + if err != nil { + return + } + err = db.Ensure("keysby/32") + if err != nil { + return + } + return +} diff --git a/pkg/store/Makefile b/pkg/store/Makefile new file mode 100644 index 0000000..4bc5f57 --- /dev/null +++ b/pkg/store/Makefile @@ -0,0 +1,7 @@ +include $(GOROOT)/src/Make.inc + +TARG=epoint/store +GOFILES=\ + store.go + +include $(GOROOT)/src/Make.pkg diff --git a/pkg/store/store.go b/pkg/store/store.go new file mode 100644 index 0000000..2a6d2f8 --- /dev/null +++ b/pkg/store/store.go @@ -0,0 +1,83 @@ +package store + +// persistent key-value store +// multiple key-value store can be managed by a single db connection +// each store has a name, before usage the name of the store must be +// ensured to exist +// +// TODO: this is a toy implementation + +import ( + "io/ioutil" + "os" + "path/filepath" +) + +type Conn struct { + path string +} + +type NotFoundError struct { + path string +} + +func (e NotFoundError) Error() string { + return "not found: " + e.path +} + +func Open(root string) (c *Conn, err error) { + c = new(Conn) + c.path, err = filepath.Abs(root) + if err != nil { + return + } + err = os.MkdirAll(c.path, 0755) + if err != nil { + return + } + return +} + +func (c *Conn) Get(name, k string) (v []byte, err error) { + v, err = ioutil.ReadFile(filepath.Join(c.path, name, k)) + if err != nil { + if p, ok := err.(*os.PathError); ok && p.Err == os.ENOENT { + err = NotFoundError{name+"/"+k} + } + } + return +} + +func (c *Conn) Ensure(name string) (err error) { + return os.MkdirAll(filepath.Join(c.path, name), 0755) +} + +func (c *Conn) Set(name, k string, v []byte) (err error) { + fn := filepath.Join(c.path, name, k) + f, err := os.OpenFile(fn+".tmp", os.O_CREATE|os.O_TRUNC|os.O_WRONLY|os.O_SYNC, 0666) + if err != nil { + return + } + defer f.Close() + _, err = f.Write(v) + if err != nil { + return + } + err = os.Rename(fn+".tmp", fn) + return +} + +func (c *Conn) Append(name, k string, v []byte) (err error) { + fn := filepath.Join(c.path, name, k) + f, err := os.OpenFile(fn, os.O_CREATE|os.O_APPEND|os.O_WRONLY|os.O_SYNC, 0666) + if err != nil { + return + } + defer f.Close() + _, err = f.Write(v) + return +} + +func (c *Conn) Close() (err error) { + return +} diff --git a/pkg/store/store_test.go b/pkg/store/store_test.go new file mode 100644 index 0000000..da79564 --- /dev/null +++ b/pkg/store/store_test.go @@ -0,0 +1,79 @@ +package store + +import ( + "testing" +) + +var testData = map[string]string{ + "A": "a", + "B": "b", + "C": "c", + "D": "d", + "foo_bar-baz": "a\nb\nc\nd\n", +} + +func TestStore(t *testing.T) { + c, err := Open("teststore") + defer c.Close() + if err != nil { + t.Errorf("open failed: %s", err) + return + } + err = c.Ensure("abc") + if err != nil { + t.Errorf("ensure failed: %s", err) + return + } + for k, v := range testData { + err = c.Set("abc", k, []byte(v)) + if err != nil { + t.Errorf("Set failed: %s", err) + } + } + for k, v := range testData { + d, err := c.Get("abc", k) + if err != nil { + t.Errorf("Get failed: %s", err) + continue + } + if string(d) != v { + t.Errorf("expected %s; got %s", v, string(d)) + } + } +} + +func TestPersist(t *testing.T) { + c, err := Open("teststore") + if err != nil { + t.Errorf("open failed: %s", err) + return + } + err = c.Ensure("abc") + if err != nil { + t.Errorf("ensure failed: %s", err) + return + } + for k, v := range testData { + err = c.Set("abc", k, []byte(v)) + if err != nil { + t.Errorf("Set failed: %s", err) + } + } + c.Close() + + c, err = Open("teststore") + if err != nil { + t.Errorf("open failed: %s", err) + return + } + for k, v := range testData { + d, err := c.Get("abc", k) + if err != nil { + t.Errorf("Get failed: %s", err) + continue + } + if string(d) != v { + t.Errorf("expected %s; got %s", v, string(d)) + } + } +} diff --git a/store/Makefile b/store/Makefile deleted file mode 100644 index 23c7767..0000000 --- a/store/Makefile +++ /dev/null @@ -1,6 +0,0 @@ -include $(GOROOT)/src/Make.inc - -TARG=epoint/store -GOFILES=store.go - -include $(GOROOT)/src/Make.pkg diff --git a/store/store.go b/store/store.go deleted file mode 100644 index 2a6d2f8..0000000 --- a/store/store.go +++ /dev/null @@ -1,83 +0,0 @@ -package store - -// persistent key-value store -// multiple key-value store can be managed by a single db connection -// each store has a name, before usage the name of the store must be -// ensured to exist -// -// TODO: this is a toy implementation - -import ( - "io/ioutil" - "os" - "path/filepath" -) - -type Conn struct { - path string -} - -type NotFoundError struct { - path string -} - -func (e NotFoundError) Error() string { - return "not found: " + e.path -} - -func Open(root string) (c *Conn, err error) { - c = new(Conn) - c.path, err = filepath.Abs(root) - if err != nil { - return - } - err = os.MkdirAll(c.path, 0755) - if err != nil { - return - } - return -} - -func (c *Conn) Get(name, k string) (v []byte, err error) { - v, err = ioutil.ReadFile(filepath.Join(c.path, name, k)) - if err != nil { - if p, ok := err.(*os.PathError); ok && p.Err == os.ENOENT { - err = NotFoundError{name+"/"+k} - } - } - return -} - -func (c *Conn) Ensure(name string) (err error) { - return os.MkdirAll(filepath.Join(c.path, name), 0755) -} - -func (c *Conn) Set(name, k string, v []byte) (err error) { - fn := filepath.Join(c.path, name, k) - f, err := os.OpenFile(fn+".tmp", os.O_CREATE|os.O_TRUNC|os.O_WRONLY|os.O_SYNC, 0666) - if err != nil { - return - } - defer f.Close() - _, err = f.Write(v) - if err != nil { - return - } - err = os.Rename(fn+".tmp", fn) - return -} - -func (c *Conn) Append(name, k string, v []byte) (err error) { - fn := filepath.Join(c.path, name, k) - f, err := os.OpenFile(fn, os.O_CREATE|os.O_APPEND|os.O_WRONLY|os.O_SYNC, 0666) - if err != nil { - return - } - defer f.Close() - _, err = f.Write(v) - return -} - -func (c *Conn) Close() (err error) { - return -} diff --git a/store/store_test.go b/store/store_test.go deleted file mode 100644 index da79564..0000000 --- a/store/store_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package store - -import ( - "testing" -) - -var testData = map[string]string{ - "A": "a", - "B": "b", - "C": "c", - "D": "d", - "foo_bar-baz": "a\nb\nc\nd\n", -} - -func TestStore(t *testing.T) { - c, err := Open("teststore") - defer c.Close() - if err != nil { - t.Errorf("open failed: %s", err) - return - } - err = c.Ensure("abc") - if err != nil { - t.Errorf("ensure failed: %s", err) - return - } - for k, v := range testData { - err = c.Set("abc", k, []byte(v)) - if err != nil { - t.Errorf("Set failed: %s", err) - } - } - for k, v := range testData { - d, err := c.Get("abc", k) - if err != nil { - t.Errorf("Get failed: %s", err) - continue - } - if string(d) != v { - t.Errorf("expected %s; got %s", v, string(d)) - } - } -} - -func TestPersist(t *testing.T) { - c, err := Open("teststore") - if err != nil { - t.Errorf("open failed: %s", err) - return - } - err = c.Ensure("abc") - if err != nil { - t.Errorf("ensure failed: %s", err) - return - } - for k, v := range testData { - err = c.Set("abc", k, []byte(v)) - if err != nil { - t.Errorf("Set failed: %s", err) - } - } - c.Close() - - c, err = Open("teststore") - if err != nil { - t.Errorf("open failed: %s", err) - return - } - for k, v := range testData { - d, err := c.Get("abc", k) - if err != nil { - t.Errorf("Get failed: %s", err) - continue - } - if string(d) != v { - t.Errorf("expected %s; got %s", v, string(d)) - } - } -}