--- /dev/null
+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
--- /dev/null
+include $(GOROOT)/src/Make.inc
+
+TARG=epoint-client
+GOFILES=\
+ epoint-client.go
+
+include $(GOROOT)/src/Make.cmd
--- /dev/null
+package main
+
+func main() {
+}
--- /dev/null
+include $(GOROOT)/src/Make.inc
+
+TARG=epoint-server
+GOFILES=\
+ epoint-server.go
+
+include $(GOROOT)/src/Make.cmd
--- /dev/null
+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))
+}
import (
"crypto/openpgp"
- "epoint/dsakey"
+ "epoint/key"
"fmt"
"log"
"os"
}
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)
+++ /dev/null
-include $(GOROOT)/src/Make.inc
-
-TARG=epoint/document
-GOFILES=document.go
-
-include $(GOROOT)/src/Make.pkg
+++ /dev/null
-// 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:]
-}
+++ /dev/null
-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)
- }
-}
+++ /dev/null
-include $(GOROOT)/src/Make.inc
-
-TARG=epoint/dsakey
-GOFILES=dsakey.go
-
-include $(GOROOT)/src/Make.pkg
+++ /dev/null
-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
-}
+++ /dev/null
-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)
- }
-}
+++ /dev/null
-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))
-}
+++ /dev/null
-include $(GOROOT)/src/Make.inc
-
-TARG=epoint/logic
-GOFILES=logic.go
-
-include $(GOROOT)/src/Make.pkg
+++ /dev/null
-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
-}
--- /dev/null
+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
--- /dev/null
+#!/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
--- /dev/null
+include $(GOROOT)/src/Make.inc
+
+TARG=epoint/document
+GOFILES=\
+ document.go
+
+include $(GOROOT)/src/Make.pkg
--- /dev/null
+// 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:]
+}
--- /dev/null
+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)
+ }
+}
--- /dev/null
+include $(GOROOT)/src/Make.inc
+
+TARG=epoint/key
+GOFILES=\
+ key.go
+
+include $(GOROOT)/src/Make.pkg
--- /dev/null
+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
+}
--- /dev/null
+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)
+ }
+}
--- /dev/null
+include $(GOROOT)/src/Make.inc
+
+TARG=epoint/server
+GOFILES=\
+ server.go
+
+include $(GOROOT)/src/Make.pkg
--- /dev/null
+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
+}
--- /dev/null
+include $(GOROOT)/src/Make.inc
+
+TARG=epoint/store
+GOFILES=\
+ store.go
+
+include $(GOROOT)/src/Make.pkg
--- /dev/null
+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
+}
--- /dev/null
+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))
+ }
+ }
+}
+++ /dev/null
-include $(GOROOT)/src/Make.inc
-
-TARG=epoint/store
-GOFILES=store.go
-
-include $(GOROOT)/src/Make.pkg
+++ /dev/null
-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
-}
+++ /dev/null
-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))
- }
- }
-}