Browse Source

Import keys server from light-client, with changes

pull/1782/head
Ethan Frey 8 years ago
parent
commit
6ec2330eb8
6 changed files with 476 additions and 0 deletions
  1. +65
    -0
      proxy/README.md
  2. +58
    -0
      proxy/helpers.go
  3. +123
    -0
      proxy/keys.go
  4. +190
    -0
      proxy/keys_test.go
  5. +28
    -0
      proxy/types/keys.go
  6. +12
    -0
      proxy/valid.go

+ 65
- 0
proxy/README.md View File

@ -0,0 +1,65 @@
# Proxy Server
This package provides all the functionality for a local proxy http server, and ties together functionality from all the other packages to acheive this aim. Simply configure this server with your application-specific settings via a main script and you are good to go.
This server should run on the client's machine, and can accept a trusted connection from localhost (via tcp or unix socket). It provides a simple json rest API and handles all the binary wrangling and cryptographic proofs under the hood. Thus, you can host a local webapp (via electron?) and connect to this proxy, perform simple queries and posts and behind the scenes take advantage of the *awesome power* of the tendermint blockchain.
If you are writing native code, you can use this as well, or you can look for bindings to embed this functionality directly as a library in your codebase.
**(coming soon)**
## API
The API has various sections based on functionality. The major portions are key management, signing and posting transactions, and querying and proving data.
### Key Management
We expose a number of methods for safely managing your keychain. They are typically bound under `/keys`, but could be placed in another location by the app.
* `POST /keys/` - provide a name and passphrase and create a brand new key
* `GET /keys/` - get a list of all available key names, along with their public key and address
* `GET /keys/{name}` - get public key and address for this named key
Later expose:
* `PUT /keys/{name}` - update the passphrase for the given key. requires you to correctly provide the current passphrase, as well as a new one.
* `DELETE /keys/{name}` - permanently delete this private key. requires you to correctly provide the current passphrase
* export and import functionality
### Transactions
You want to post your transaction. Great. Your application must provide logic to transform json into a `Signable` go struct. Then we handle the rest, signing it with a keypair of your choosing, posting it to tendermint core, and returning you confirmation when it was committed.
* `POST /txs/` - provide name, passphrase and application-specific data to post to tendermint
### Proving Data
We sent some money to our friend, now we want to check his balance. No, not just look at it, but really check it, verify all those cryptographic proofs that some node is not lying and it really, truly is in his account.
Thankfully, we have the technology and can do all the checks in the proxy, it might just take a second or two for us to get all those block headers.
However, this still just leaves us with a bunch of binary blobs from the server, so to make this whole process less painless, you should provide some application-specific logic to parse this binary data from the blockchain, so we can present it as json over the interface.
* `GET /query/{path}/{data}` - will quickly query the data under the given (hex-encoded) key. `path` is `key` to query by merkle key, but your application could provide other prefixes, to differentiate by types (eg. `account`, `votes`, `escrow`). The returned data is parsed into json and displayed.
* `GET /proof/{key}` - will query for a merkle proof of the given key, download block headers, and verify all the signatures of that block. After it is done, it will present you some json and a stamp that it your data is really safe and sound.
## Configuring
When you instantiate a server, make sure to pass in application-specific info in order to properly. Like the following info:
Possibly as command-line flags:
* Where to store the private keys? (or find existing ones)
* Which type of key to generate?
* What is the URL of the tendermint RPC server?
* TODO: support multiple node URLs and round-robin
* What is the chain_id we wish to connect to?
Extra code (plugin) you must write:
* Logic to parse json -> `Signable` transaction
* Logic to parse binary values from merkle tree -> `struct`ured data to render
TODO:
* How to get the trusted validator set?

+ 58
- 0
proxy/helpers.go View File

@ -0,0 +1,58 @@
/*
package proxy provides http handlers to construct a proxy server
for key management, transaction signing, and query validation.
Please read the README and godoc to see how to
configure the server for your application.
*/
package proxy
import (
"encoding/json"
"io/ioutil"
"net/http"
data "github.com/tendermint/go-data"
"github.com/tendermint/go-keys/proxy/types"
"github.com/pkg/errors"
)
func readRequest(r *http.Request, o interface{}) error {
defer r.Body.Close()
data, err := ioutil.ReadAll(r.Body)
if err != nil {
return errors.Wrap(err, "Read Request")
}
err = json.Unmarshal(data, o)
if err != nil {
return errors.Wrap(err, "Parse")
}
return validate(o)
}
// most errors are bad input, so 406... do better....
func writeError(w http.ResponseWriter, err error) {
// fmt.Printf("Error: %+v\n", err)
res := types.ErrorResponse{
Code: 406,
Error: err.Error(),
}
writeCode(w, &res, 406)
}
func writeCode(w http.ResponseWriter, o interface{}, code int) {
// two space indent to make it easier to read
data, err := data.ToJSON(o)
if err != nil {
writeError(w, err)
} else {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
w.Write(data)
}
}
func writeSuccess(w http.ResponseWriter, o interface{}) {
writeCode(w, o, 200)
}

+ 123
- 0
proxy/keys.go View File

@ -0,0 +1,123 @@
package proxy
import (
"errors"
"net/http"
"github.com/gorilla/mux"
keys "github.com/tendermint/go-keys"
"github.com/tendermint/go-keys/proxy/types"
)
type KeyServer struct {
manager keys.Manager
}
func NewKeyServer(manager keys.Manager) KeyServer {
return KeyServer{
manager: manager,
}
}
func (k KeyServer) GenerateKey(w http.ResponseWriter, r *http.Request) {
req := types.CreateKeyRequest{}
err := readRequest(r, &req)
if err != nil {
writeError(w, err)
return
}
key, err := k.manager.Create(req.Name, req.Passphrase, req.Algo)
if err != nil {
writeError(w, err)
return
}
writeSuccess(w, &key)
}
func (k KeyServer) GetKey(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
name := vars["name"]
key, err := k.manager.Get(name)
if err != nil {
writeError(w, err)
return
}
writeSuccess(w, &key)
}
func (k KeyServer) ListKeys(w http.ResponseWriter, r *http.Request) {
keys, err := k.manager.List()
if err != nil {
writeError(w, err)
return
}
writeSuccess(w, keys)
}
func (k KeyServer) UpdateKey(w http.ResponseWriter, r *http.Request) {
req := types.UpdateKeyRequest{}
err := readRequest(r, &req)
if err != nil {
writeError(w, err)
return
}
vars := mux.Vars(r)
name := vars["name"]
if name != req.Name {
writeError(w, errors.New("path and json key names don't match"))
return
}
err = k.manager.Update(req.Name, req.OldPass, req.NewPass)
if err != nil {
writeError(w, err)
return
}
key, err := k.manager.Get(req.Name)
if err != nil {
writeError(w, err)
return
}
writeSuccess(w, &key)
}
func (k KeyServer) DeleteKey(w http.ResponseWriter, r *http.Request) {
req := types.DeleteKeyRequest{}
err := readRequest(r, &req)
if err != nil {
writeError(w, err)
return
}
vars := mux.Vars(r)
name := vars["name"]
if name != req.Name {
writeError(w, errors.New("path and json key names don't match"))
return
}
err = k.manager.Delete(req.Name, req.Passphrase)
if err != nil {
writeError(w, err)
return
}
// not really an error, but something generic
resp := types.ErrorResponse{
Success: true,
}
writeSuccess(w, &resp)
}
func (k KeyServer) Register(r *mux.Router) {
r.HandleFunc("/", k.GenerateKey).Methods("POST")
r.HandleFunc("/", k.ListKeys).Methods("GET")
r.HandleFunc("/{name}", k.GetKey).Methods("GET")
r.HandleFunc("/{name}", k.UpdateKey).Methods("POST", "PUT")
r.HandleFunc("/{name}", k.DeleteKey).Methods("DELETE")
}

+ 190
- 0
proxy/keys_test.go View File

@ -0,0 +1,190 @@
package proxy_test
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
keys "github.com/tendermint/go-keys"
"github.com/tendermint/go-keys/cryptostore"
"github.com/tendermint/go-keys/proxy"
"github.com/tendermint/go-keys/proxy/types"
"github.com/tendermint/go-keys/storage/memstorage"
)
func TestKeyServer(t *testing.T) {
assert, require := assert.New(t), require.New(t)
r := setupServer()
// let's abstract this out a bit....
keys, code, err := listKeys(r)
require.Nil(err)
require.Equal(http.StatusOK, code)
assert.Equal(0, len(keys))
algo := "ed25519"
n1, n2 := "personal", "business"
p0, p1, p2 := "1234", "over10chars...", "really-secure!@#$"
// this fails for validation
_, code, err = createKey(r, n1, p0, algo)
require.Nil(err, "%+v", err)
require.NotEqual(http.StatusOK, code)
// new password better
key, code, err := createKey(r, n1, p1, algo)
require.Nil(err, "%+v", err)
require.Equal(http.StatusOK, code)
require.Equal(key.Name, n1)
// the other one works
key2, code, err := createKey(r, n2, p2, algo)
require.Nil(err, "%+v", err)
require.Equal(http.StatusOK, code)
require.Equal(key2.Name, n2)
// let's abstract this out a bit....
keys, code, err = listKeys(r)
require.Nil(err)
require.Equal(http.StatusOK, code)
if assert.Equal(2, len(keys)) {
// in alphabetical order
assert.Equal(keys[0].Name, n2)
assert.Equal(keys[1].Name, n1)
}
// get works
k, code, err := getKey(r, n1)
require.Nil(err, "%+v", err)
require.Equal(http.StatusOK, code)
assert.Equal(k.Name, n1)
assert.NotNil(k.Address)
assert.Equal(k.Address, key.Address)
// delete with proper key
_, code, err = deleteKey(r, n1, p1)
require.Nil(err, "%+v", err)
require.Equal(http.StatusOK, code)
// after delete, get and list different
_, code, err = getKey(r, n1)
require.Nil(err, "%+v", err)
require.NotEqual(http.StatusOK, code)
keys, code, err = listKeys(r)
require.Nil(err, "%+v", err)
require.Equal(http.StatusOK, code)
if assert.Equal(1, len(keys)) {
assert.Equal(keys[0].Name, n2)
}
}
func setupServer() http.Handler {
// make the storage with reasonable defaults
cstore := cryptostore.New(
cryptostore.SecretBox,
memstorage.New(),
)
// build your http server
ks := proxy.NewKeyServer(cstore)
r := mux.NewRouter()
sk := r.PathPrefix("/keys").Subrouter()
ks.Register(sk)
return r
}
// return data, status code, and error
func listKeys(h http.Handler) (keys.Infos, int, error) {
rr := httptest.NewRecorder()
req, err := http.NewRequest("GET", "/keys/", nil)
if err != nil {
return nil, 0, err
}
h.ServeHTTP(rr, req)
if http.StatusOK != rr.Code {
return nil, rr.Code, nil
}
data := keys.Infos{}
err = json.Unmarshal(rr.Body.Bytes(), &data)
return data, rr.Code, err
}
func getKey(h http.Handler, name string) (*keys.Info, int, error) {
rr := httptest.NewRecorder()
req, err := http.NewRequest("GET", "/keys/"+name, nil)
if err != nil {
return nil, 0, err
}
h.ServeHTTP(rr, req)
if http.StatusOK != rr.Code {
return nil, rr.Code, nil
}
data := keys.Info{}
err = json.Unmarshal(rr.Body.Bytes(), &data)
return &data, rr.Code, err
}
func createKey(h http.Handler, name, passphrase, algo string) (*keys.Info, int, error) {
rr := httptest.NewRecorder()
post := types.CreateKeyRequest{
Name: name,
Passphrase: passphrase,
Algo: algo,
}
var b bytes.Buffer
err := json.NewEncoder(&b).Encode(&post)
if err != nil {
return nil, 0, err
}
req, err := http.NewRequest("POST", "/keys/", &b)
if err != nil {
return nil, 0, err
}
h.ServeHTTP(rr, req)
if http.StatusOK != rr.Code {
return nil, rr.Code, nil
}
data := keys.Info{}
err = json.Unmarshal(rr.Body.Bytes(), &data)
return &data, rr.Code, err
}
func deleteKey(h http.Handler, name, passphrase string) (*types.ErrorResponse, int, error) {
rr := httptest.NewRecorder()
post := types.DeleteKeyRequest{
Name: name,
Passphrase: passphrase,
}
var b bytes.Buffer
err := json.NewEncoder(&b).Encode(&post)
if err != nil {
return nil, 0, err
}
req, err := http.NewRequest("DELETE", "/keys/"+name, &b)
if err != nil {
return nil, 0, err
}
h.ServeHTTP(rr, req)
if http.StatusOK != rr.Code {
return nil, rr.Code, nil
}
data := types.ErrorResponse{}
err = json.Unmarshal(rr.Body.Bytes(), &data)
return &data, rr.Code, err
}

+ 28
- 0
proxy/types/keys.go View File

@ -0,0 +1,28 @@
package types
// CreateKeyRequest is sent to create a new key
type CreateKeyRequest struct {
Name string `json:"name" validate:"required,min=4,printascii"`
Passphrase string `json:"passphrase" validate:"required,min=10"`
Algo string `json:"algo"`
}
// DeleteKeyRequest to destroy a key permanently (careful!)
type DeleteKeyRequest struct {
Name string `json:"name" validate:"required,min=4,printascii"`
Passphrase string `json:"passphrase" validate:"required,min=10"`
}
// UpdateKeyRequest is sent to update the passphrase for an existing key
type UpdateKeyRequest struct {
Name string `json:"name" validate:"required,min=4,printascii"`
OldPass string `json:"passphrase" validate:"required,min=10"`
NewPass string `json:"new_passphrase" validate:"required,min=10"`
}
// ErrorResponse is returned for 4xx and 5xx errors
type ErrorResponse struct {
Success bool `json:"success"`
Error string `json:"error"` // error message if Success is false
Code int `json:"code"` // error code if Success is false
}

+ 12
- 0
proxy/valid.go View File

@ -0,0 +1,12 @@
package proxy
import (
"github.com/pkg/errors"
"gopkg.in/go-playground/validator.v9"
)
var v = validator.New()
func validate(req interface{}) error {
return errors.Wrap(v.Struct(req), "Validate")
}

Loading…
Cancel
Save