code reorganization
authornsz <nsz@port70.net>
Thu, 8 Dec 2011 12:27:10 +0000 (13:27 +0100)
committernsz <nsz@port70.net>
Thu, 8 Dec 2011 12:27:10 +0000 (13:27 +0100)
31 files changed:
Makefile [new file with mode: 0644]
cmd/epoint-client/Makefile [new file with mode: 0644]
cmd/epoint-client/epoint-client.go [new file with mode: 0644]
cmd/epoint-server/Makefile [new file with mode: 0644]
cmd/epoint-server/epoint-server.go [new file with mode: 0644]
cmd/genkey/genkey.go
document/Makefile [deleted file]
document/document.go [deleted file]
document/document_test.go [deleted file]
dsakey/Makefile [deleted file]
dsakey/dsakey.go [deleted file]
dsakey/dsakey_test.go [deleted file]
epoint-server.go [deleted file]
logic/Makefile [deleted file]
logic/logic.go [deleted file]
pkg/Makefile [new file with mode: 0644]
pkg/deps.sh [new file with mode: 0755]
pkg/document/Makefile [new file with mode: 0644]
pkg/document/document.go [new file with mode: 0644]
pkg/document/document_test.go [new file with mode: 0644]
pkg/key/Makefile [new file with mode: 0644]
pkg/key/key.go [new file with mode: 0644]
pkg/key/key_test.go [new file with mode: 0644]
pkg/server/Makefile [new file with mode: 0644]
pkg/server/server.go [new file with mode: 0644]
pkg/store/Makefile [new file with mode: 0644]
pkg/store/store.go [new file with mode: 0644]
pkg/store/store_test.go [new file with mode: 0644]
store/Makefile [deleted file]
store/store.go [deleted file]
store/store_test.go [deleted file]

diff --git a/Makefile b/Makefile
new file mode 100644 (file)
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 (file)
index 0000000..9f4116d
--- /dev/null
@@ -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 (file)
index 0000000..da29a2c
--- /dev/null
@@ -0,0 +1,4 @@
+package main
+
+func main() {
+}
diff --git a/cmd/epoint-server/Makefile b/cmd/epoint-server/Makefile
new file mode 100644 (file)
index 0000000..c14e7c1
--- /dev/null
@@ -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 (file)
index 0000000..10ddcbd
--- /dev/null
@@ -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, `<html><head><title>epoint-server submit form</title></head><body>
+<h2>epoint-server submit form</h2>
+<h3>web form</h3>
+<p>submit one document at once
+<form method="post" action="http://localhost%s/submit">
+<p>key:<br><textarea name="key" rows="5" cols="80"></textarea>
+<p>draft:<br><textarea name="draft" rows="5" cols="80"></textarea>
+<p>debit:<br><textarea name="debit" rows="5" cols="80"></textarea>
+<p><input type="submit">
+</form>
+<h3>command line</h3>
+<pre>
+curl --data-urlencode name@path/to/file.txt host/submit
+</pre>
+where 'name' is 'key', 'draft' or 'debit'.
+</body></html>
+`, 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))
+}
index 3a8a256..7f3c518 100644 (file)
@@ -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 (file)
index 7a52d21..0000000
+++ /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 (file)
index 0c8b0f4..0000000
+++ /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 (file)
index 982f3a9..0000000
+++ /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 (file)
index bd29402..0000000
+++ /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 (file)
index 23ab5fe..0000000
+++ /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 (file)
index 34df359..0000000
+++ /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 (file)
index 425ea45..0000000
+++ /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, `<html><head><title>epoint-server submit form</title></head><body>
-<h2>epoint-server submit form</h2>
-<h3>web form</h3>
-<p>submit one document at once
-<form method="post" action="http://localhost%s/submit">
-<p>key:<br><textarea name="key" rows="5" cols="80"></textarea>
-<p>draft:<br><textarea name="draft" rows="5" cols="80"></textarea>
-<p>debit:<br><textarea name="debit" rows="5" cols="80"></textarea>
-<p><input type="submit">
-</form>
-<h3>command line</h3>
-<pre>
-curl --data-urlencode name@path/to/file.txt host/submit
-</pre>
-where 'name' is 'key', 'draft' or 'debit'.
-</body></html>
-`, 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 (file)
index 89942c5..0000000
+++ /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 (file)
index 602f458..0000000
+++ /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 (file)
index 0000000..4917a98
--- /dev/null
@@ -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 (executable)
index 0000000..123ad3a
--- /dev/null
@@ -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 (file)
index 0000000..91080d9
--- /dev/null
@@ -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 (file)
index 0000000..0c8b0f4
--- /dev/null
@@ -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 (file)
index 0000000..982f3a9
--- /dev/null
@@ -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 (file)
index 0000000..d3d31f5
--- /dev/null
@@ -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 (file)
index 0000000..195b7c5
--- /dev/null
@@ -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 (file)
index 0000000..34df359
--- /dev/null
@@ -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 (file)
index 0000000..6835a0e
--- /dev/null
@@ -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 (file)
index 0000000..5d9af8e
--- /dev/null
@@ -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 (file)
index 0000000..4bc5f57
--- /dev/null
@@ -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 (file)
index 0000000..2a6d2f8
--- /dev/null
@@ -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 (file)
index 0000000..da79564
--- /dev/null
@@ -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 (file)
index 23c7767..0000000
+++ /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 (file)
index 2a6d2f8..0000000
+++ /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 (file)
index da79564..0000000
+++ /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))
-               }
-       }
-}