signer/storage: implement aes-gcm-backed credential storage

This commit is contained in:
Martin Holst Swende 2018-02-20 14:35:25 +01:00
parent 76a8c7caf0
commit ec83d9d0ab
No known key found for this signature in database
GPG key ID: 683B438C05A5DDF0
16 changed files with 1059 additions and 217 deletions

View file

@ -9,24 +9,38 @@ This setup allows a DApp to connect to a remote Ethereum node and send transacti
help in situations when a DApp is connected to a remote node because a local Ethereum node is not available, not
synchronised with the chain or a particular Ethereum node that has no build in, or limited account management.
In its current form the signer is very limited and designed to work with Mist. It hasn't got a connection to an
Ethereum node. This restriction imposed many limitations such as the lack of ability to keep track of nonces, balances
or fetching additional information that can help the user to make a decision to sign a transaction or data.
The signer can run as a daemon on the same machine, or off a usb-stick like [usb armory](https://inversepath.com/usbarmory),
or a separate VM in a [QubesOS](https://www.qubes-os.org/) type os setup.
## Command line flags
The signer accepts the following command line options:
```
--chainid value chain identifier (default: 1)
COMMANDS:
init Initialize the signer, generate secret storage
attest Attest that a js-file is to be used
addpw Store a credential for a keystore file
help Shows a list of commands or help for one command
GLOBAL OPTIONS:
--loglevel value log level to emit to the screen (default: 4)
--keystore value Directory for the keystore (default: "/home/martin/.ethereum/keystore")
--configdir value Directory for signer configuration (default: "/home/martin/.signer")
--networkid value Network identifier (integer, 1=Frontier, 2=Morden (disused), 3=Ropsten, 4=Rinkeby) (default: 1)
--lightkdf Reduce key-derivation RAM & CPU usage at some expense of KDF strength
--nousb Disables monitoring for and managing USB hardware wallets
--rpcaddr value HTTP-RPC server listening interface (default: "localhost")
--rpcport value HTTP-RPC server listening port (default: 8550)
--signersecret value A file containing the password used to encrypt signer credentials, e.g. keystore credentials and ruleset hash
--4bytedb value File containing 4byte-identifiers (default: "./4byte.json")
--4bytedb-custom value File used for writing new 4byte-identifiers submitted via API (default: "./4byte-custom.json")
--auditlog value File used to emit audit logs. Set to "" to disable (default: "audit.log")
--rules value Enable rule-engine (default: "rules.json")
--stdio-ui Use STDIN/STDOUT as a channel for an external UI. This means that an STDIN/STDOUT is used for RPC-communication with a e.g. a graphical user interface, and can be used when the signer is started by an external process.
--stdio-ui-test Mechanism to test interface between signer and UI. Requires 'stdio-ui'.
--help, -h show help
--version, -v print the version
```
@ -35,6 +49,8 @@ Example:
signer -keystore /my/keystore -chainid 4
```
Check out the [tutorial](tutorial.md) for some concrete examples on how the signer works.
## Security model
The security model of the signer is as follows:
@ -67,29 +83,25 @@ passing on to the UI. The reason it currently does not, is that it would make it
accounts if it immediately returned "unknown account".
* [x] DONE: Similarly, it should be possible to configure the signer to auto-allow listing (certain) accounts, instead of asking every time.
* Upon startup, the signer should spit out some info to the caller (particularly important when executed in `stdio-ui`-mode),
* [x] Done Upon startup, the signer should spit out some info to the caller (particularly important when executed in `stdio-ui`-mode),
invoking methods with the following info:
* Version info about the signer
* Address of API (http/ipc)
* This makes it posible for the UI to use the api for creating transactions
* List of known accounts
* [x] Version info about the signer
* [x] Address of API (http/ipc)
* [ ] List of known accounts
* The signer should pass the `Origin` header as call-info to the UI. As of right now, the way that info about the request is
* Geth todos
- The signer should pass the `Origin` header as call-info to the UI. As of right now, the way that info about the request is
put together is a bit of a hack into the http server. This could probably be greatly improved
* Geth relay
- Geth should be started in `geth --external_signer localhost:8550`.
* Geth checksum
- Relay: Geth should be started in `geth --external_signer localhost:8550`.
- Currently, the Geth API:s use `common.Address` in the arguments to transaction submission (e.g `to` field). This
type is 20 `bytes`, and is incapable of carrying checksum information. The signer uses `common.MixedcaseAddress`, which
retains the original input.
- The Geth api should switch to use the same type, and relay `to`-account verbatim to the external api.
* Wallets / accounts. Add API methods for wallets.
* Storage
* An encrypted key-value storage should be implemented
* [x] Storage
* [x] An encrypted key-value storage should be implemented
* See [rules.md](rules.md) for more info about this.
* Another potential thing to introduce is pairing.
@ -97,6 +109,8 @@ put together is a bit of a hack into the http server. This could probably be gre
* Thus geth/mist/cpp would cryptographically handshake and afterwards the caller would be allowed to make signing requests.
* This feature would make the addition of rules less dangerous.
* Wallets / accounts. Add API methods for wallets.
## Communication
### External API

View file

@ -17,7 +17,6 @@
package core
import (
// "bytes"
"encoding/json"
"fmt"
"io/ioutil"

View file

@ -21,13 +21,12 @@ import (
"strings"
"testing"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
// "reflect"
// "math/big"
"io/ioutil"
"math/big"
"reflect"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
)
func verify(t *testing.T, jsondata, calldata string, exp []interface{}) {

View file

@ -385,7 +385,7 @@ func (api *SignerAPI) SignTransaction(ctx context.Context, args SendTxArgs, meth
//
// https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_sign
func (api *SignerAPI) Sign(ctx context.Context, addr common.MixedcaseAddress, data hexutil.Bytes) (hexutil.Bytes, error) {
sighash, msg := signHash(data)
sighash, msg := SignHash(data)
// We make the request prior to looking up if we actually have the account, to prevent
// account-enumeration via the API
req := &SignDataRequest{Address: addr, Rawdata: data, Message: msg, Hash: sighash, Meta: MetadataFromContext(ctx)}
@ -431,7 +431,7 @@ func (api *SignerAPI) EcRecover(ctx context.Context, data, sig hexutil.Bytes) (c
return common.Address{}, fmt.Errorf("invalid Ethereum signature (V is not 27 or 28)")
}
sig[64] -= 27 // Transform yellow paper V from 27/28 to 0/1
hash, _ := signHash(data)
hash, _ := SignHash(data)
rpk, err := crypto.Ecrecover(hash, sig)
if err != nil {
return common.Address{}, err
@ -441,14 +441,14 @@ func (api *SignerAPI) EcRecover(ctx context.Context, data, sig hexutil.Bytes) (c
return recoveredAddr, nil
}
// signHash is a helper function that calculates a hash for the given message that can be
// SignHash is a helper function that calculates a hash for the given message that can be
// safely used to calculate a signature from.
//
// The hash is calulcated as
// keccak256("\x19Ethereum Signed Message:\n"${message length}${message}).
//
// This gives context to the signed message and prevents signing of transactions.
func signHash(data []byte) ([]byte, string) {
func SignHash(data []byte) ([]byte, string) {
msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data)
return crypto.Keccak256([]byte(msg)), msg
}

View file

@ -20,6 +20,7 @@ import (
"context"
"encoding/json"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
@ -33,9 +34,12 @@ type AuditLogger struct {
}
func (l *AuditLogger) List(ctx context.Context) (Accounts, error) {
l.log.Info("List", "type", "request", "metadata", MetadataFromContext(ctx).String())
res, e := l.api.List(ctx)
l.log.Info("Called list", "interface", "http")
return l.api.List(ctx)
l.log.Info("List", "type", "response", "data", res.String())
return res, e
}
func (l *AuditLogger) New(ctx context.Context) (accounts.Account, error) {

View file

@ -140,7 +140,7 @@ func (ui *CommandlineUI) ApproveSignData(request *SignDataRequest) (SignDataResp
defer ui.mu.Unlock()
fmt.Printf("-------- Sign data request--------------\n")
fmt.Printf("Account: %x\n", request.Address)
fmt.Printf("Account: %s\n", request.Address.String())
fmt.Printf("message: \n%q\n", request.Message)
fmt.Printf("raw data: \n%v\n", request.Rawdata)
fmt.Printf("message hash: %v\n", request.Hash)
@ -238,8 +238,8 @@ func (ui *CommandlineUI) OnApprovedTx(tx ethapi.SignTransactionResult) {
func (ui *CommandlineUI) OnSignerStartup(info StartupInfo) {
fmt.Printf("------- Signer info ------- ")
fmt.Printf("------- Signer info -------\n")
for k, v := range info.Info {
fmt.Printf("* %v : %v", k, v)
fmt.Printf("* %v : %v\n", k, v)
}
}

View file

@ -20,11 +20,12 @@ import (
"encoding/json"
"strings"
"math/big"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"math/big"
)
type Accounts []Account

View file

@ -20,8 +20,9 @@ import (
"bytes"
"errors"
"fmt"
"github.com/ethereum/go-ethereum/common"
"math/big"
"github.com/ethereum/go-ethereum/common"
)
// The validation package contains validation checks for transactions

View file

@ -18,10 +18,11 @@ package core
import (
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"math/big"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
)
func hexAddr(a string) common.Address { return common.BytesToAddress(common.FromHex(a)) }

View file

@ -19,17 +19,28 @@
package main
import (
"bufio"
"context"
"crypto/rand"
"crypto/sha256"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net"
"os"
"os/user"
"path/filepath"
"runtime"
"strings"
"context"
"encoding/json"
"encoding/hex"
"github.com/ethereum/go-ethereum/cmd/signer/core"
"github.com/ethereum/go-ethereum/cmd/signer/rules"
"github.com/ethereum/go-ethereum/cmd/signer/storage"
"github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/rpc"
@ -42,85 +53,271 @@ const EXT_API_VERSION = "2.0.0"
// INT_API_VERSION -- see intapi_changelog.md
const INT_API_VERSION = "1.2.0"
func main() {
const legal_warning = `
WARNING!
app := cli.NewApp()
app.Name = "signer"
app.Usage = "Manage ethereum Account operations"
app.Flags = []cli.Flag{
cli.IntFlag{
The signer is alpha software, and not yet publically released. This software has _not_ been audited, and there
are no guarantees about the workings of this software. It may contain severe flaws. You should not use this software
unless you agree to take full responsibility for doing so, and know what you are doing.
TLDR; THIS IS NOT PRODUCTION-READY SOFTWARE!
`
var (
logLevelFlag = cli.IntFlag{
Name: "loglevel",
Value: 4,
Usage: "log level to emit to the screen",
},
cli.StringFlag{
}
keystoreFlag = cli.StringFlag{
Name: "keystore",
Value: filepath.Join(node.DefaultDataDir(), "keystore"),
Usage: "Directory for the keystore",
},
utils.NetworkIdFlag,
utils.LightKDFFlag,
utils.NoUSBFlag,
utils.RPCListenAddrFlag,
cli.IntFlag{
}
configdirFlag = cli.StringFlag{
Name: "configdir",
Value: DefaultConfigDir(),
Usage: "Directory for signer configuration",
}
rpcPortFlag = cli.IntFlag{
Name: "rpcport",
Usage: "HTTP-RPC server listening port",
Value: node.DefaultHTTPPort + 5,
},
cli.StringFlag{
}
signerSecretFlag = cli.StringFlag{
Name: "signersecret",
Usage: "A file containing the password used to encrypt signer credentials, e.g. keystore credentials and ruleset hash",
}
dBFlag = cli.StringFlag{
Name: "4bytedb",
Usage: "File containing 4byte-identifiers",
Value: "./4byte.json",
},
cli.StringFlag{
}
customDBFlag = cli.StringFlag{
Name: "4bytedb-custom",
Usage: "File used for writing new 4byte-identifiers submitted via API",
Value: "./4byte-custom.json",
},
cli.StringFlag{
}
auditLogFlag = cli.StringFlag{
Name: "auditlog",
Usage: "File used to emit audit logs. Set to \"\" to disable",
Value: "audit.log",
},
cli.StringFlag{
Name: "requestfile",
Usage: "File containing requests to handle",
Value: "",
},
cli.BoolFlag{
}
ruleFlag = cli.StringFlag{
Name: "rules",
Usage: "Enable rule-engine",
Value: "rules.json",
}
stdiouiFlag = cli.BoolFlag{
Name: "stdio-ui",
Usage: "Use STDIN/STDOUT as a channel for an external UI. " +
"This means that an STDIN/STDOUT is used for RPC-communication with a e.g. a graphical user " +
"interface, and can be used when the signer is started by an external process.",
},
cli.BoolFlag{
}
testFlag = cli.BoolFlag{
Name: "stdio-ui-test",
Usage: "Mechanism to test interface between signer and UI. Requires 'stdio-ui'.",
}
app = cli.NewApp()
initCommand = cli.Command{
Action: utils.MigrateFlags(initializeSecrets),
Name: "init",
Usage: "Initialize the signer, generate secret storage",
ArgsUsage: "",
Flags: []cli.Flag{
logLevelFlag,
configdirFlag,
},
Description: `
The init command generates a master seed which the signer can use to store credentials and data needed for
the rule-engine to work.`,
}
attestCommand = cli.Command{
Action: utils.MigrateFlags(attestFile),
Name: "attest",
Usage: "Attest that a js-file is to be used",
ArgsUsage: "<sha256sum>",
Flags: []cli.Flag{
logLevelFlag,
configdirFlag,
signerSecretFlag,
},
Description: `
The attest command stores the sha256 of the rule.js-file that you want to use for automatic processing of
incoming requests.
Whenever you make an edit to the rule file, you need to use attestation to tell
the signer that the file is 'safe' to execute.`,
}
app.Action = func(c *cli.Context) error {
addCredentialCommand = cli.Command{
Action: utils.MigrateFlags(addCredential),
Name: "addpw",
Usage: "Store a credential for a keystore file",
ArgsUsage: "<address> <password>",
Flags: []cli.Flag{
logLevelFlag,
configdirFlag,
signerSecretFlag,
},
Description: `
The addpw command stores a password for a given address (keyfile). If you invoke it with only one parameter, it will
remove any stored credential for that address (keyfile)
`,
}
)
func init() {
app.Name = "signer"
app.Usage = "Manage ethereum Account operations"
app.Flags = []cli.Flag{
logLevelFlag,
keystoreFlag,
configdirFlag,
utils.NetworkIdFlag,
utils.LightKDFFlag,
utils.NoUSBFlag,
utils.RPCListenAddrFlag,
rpcPortFlag,
signerSecretFlag,
dBFlag,
customDBFlag,
auditLogFlag,
ruleFlag,
stdiouiFlag,
testFlag,
}
app.Action = signer
app.Commands = []cli.Command{initCommand, attestCommand, addCredentialCommand}
}
func main() {
if err := app.Run(os.Args); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func initializeSecrets(c *cli.Context) error {
if err := initialize(c); err != nil {
return err
}
configDir := c.String(configdirFlag.Name)
masterSeed := make([]byte, 256)
n, err := io.ReadFull(rand.Reader, masterSeed)
if err != nil {
return err
}
if n != len(masterSeed) {
return fmt.Errorf("Failed to read enough random")
}
err = os.Mkdir(configDir, 0700)
if err != nil && !os.IsExist(err) {
return err
}
location := filepath.Join(configDir, "secrets.dat")
if _, err := os.Stat(location); err == nil {
return fmt.Errorf("file %v already exists, will not overwrite", location)
}
err = ioutil.WriteFile(location, masterSeed, 0700)
if err != nil {
return err
}
fmt.Printf("A master seed has been generated into %s\n", location)
fmt.Printf(`
This is required to be able to store credentials, such as :
* Passwords for keystores (used by rule engine)
* Storage for javascript rules
* Hash of rule-file
You should treat that file with utmost secrecy, and make a backup of it.
NOTE: This file does not contain your accounts. Those need to be backed up separately!
`)
return nil
}
func attestFile(ctx *cli.Context) error {
if len(ctx.Args()) < 1 {
utils.Fatalf("This command requires an argument.")
}
if err := initialize(ctx); err != nil {
return err
}
stretchedKey, err := readMasterKey(ctx)
if err != nil {
utils.Fatalf(err.Error())
}
configDir := ctx.String(configdirFlag.Name)
vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10]))
confKey := crypto.Keccak256([]byte("config"), stretchedKey)
// Initialize the encrypted storages
configStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "config.json"), confKey)
val := ctx.Args().First()
configStorage.Put("ruleset_sha256", val)
log.Info("Ruleset attestation updated", "sha256", val)
return nil
}
func addCredential(ctx *cli.Context) error {
if len(ctx.Args()) < 1 {
utils.Fatalf("This command requires at leaste one argument.")
}
if err := initialize(ctx); err != nil {
return err
}
stretchedKey, err := readMasterKey(ctx)
if err != nil {
utils.Fatalf(err.Error())
}
configDir := ctx.String(configdirFlag.Name)
vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10]))
pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey)
// Initialize the encrypted storages
pw_storage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey)
key := ctx.Args().First()
value := ""
if len(ctx.Args()) > 1 {
value = ctx.Args().Get(1)
}
pw_storage.Put(key, value)
log.Info("Credential store updated", "key", key)
return nil
}
func initialize(c *cli.Context) error {
if !confirm(legal_warning) {
return fmt.Errorf("aborted by user")
}
// Set up the logger to print everything
logOutput := os.Stdout
if c.Bool(stdiouiFlag.Name) {
logOutput = os.Stderr
}
log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(c.Int(logLevelFlag.Name)), log.StreamHandler(logOutput, log.TerminalFormat(true))))
return nil
}
func signer(c *cli.Context) error {
if err := initialize(c); err != nil {
return err
}
var (
ui core.SignerUI
)
// Set up the logger to print everything
logOutput := os.Stdout
if c.Bool("stdio-ui") {
logOutput = os.Stderr
}
log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(c.Int("loglevel")), log.StreamHandler(logOutput, log.TerminalFormat(true))))
if c.Bool("stdio-ui") {
if c.Bool(stdiouiFlag.Name) {
log.Info("Using stdin/stdout as UI-channel")
ui = core.NewStdIOUI()
} else {
log.Info("Using CLI as UI-channel")
ui = core.NewCommandlineUI()
}
if c.Bool("stdio-ui") {
log.Info("Using stdin/stdout as UI-channel")
}
db, err := core.NewAbiDBFromFiles(c.String("4bytedb"), c.String("4bytedb-custom"))
db, err := core.NewAbiDBFromFiles(c.String(dBFlag.Name), c.String(customDBFlag.Name))
if err != nil {
utils.Fatalf(err.Error())
}
@ -132,18 +329,62 @@ func main() {
server = rpc.NewServer()
)
api_impl := core.NewSignerAPI(
configDir := c.String(configdirFlag.Name)
if stretchedKey, err := readMasterKey(c); err != nil {
log.Info("No master seed provided, rules disabled")
} else {
if err != nil {
utils.Fatalf(err.Error())
}
vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10]))
// Generate domain specific keys
pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey)
jskey := crypto.Keccak256([]byte("jsstorage"), stretchedKey)
confkey := crypto.Keccak256([]byte("config"), stretchedKey)
// Initialize the encrypted storages
pw_storage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey)
js_storage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "jsstorage.json"), jskey)
config_storage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "config.json"), confkey)
//Do we have a rule-file?
ruleJS, err := ioutil.ReadFile(c.String(ruleFlag.Name))
if err != nil {
log.Info("Could not load rulefile, rules not enabled", "file", "rulefile")
} else {
hasher := sha256.New()
hasher.Write(ruleJS)
shasum := hasher.Sum(nil)
stored_shasum := config_storage.Get("ruleset_sha256")
if stored_shasum != hex.EncodeToString(shasum) {
log.Info("Could not validate ruleset hash, rules not enabled", "got", hex.EncodeToString(shasum), "expected", stored_shasum)
} else {
// Initialize rules
ruleEngine, err := rules.NewRuleEvaluator(ui, js_storage, pw_storage)
if err != nil {
utils.Fatalf(err.Error())
}
ruleEngine.Init(string(ruleJS))
ui = ruleEngine
log.Info("Rule engine configured", "file", c.String(ruleFlag.Name))
}
}
}
apiImpl := core.NewSignerAPI(
c.Int64(utils.NetworkIdFlag.Name),
c.String("keystore"),
c.String(keystoreFlag.Name),
c.Bool(utils.NoUSBFlag.Name),
ui, db,
c.Bool(utils.LightKDFFlag.Name))
api = api_impl
api = apiImpl
// Audit logging
if logfile := c.String("auditlog"); logfile != "" {
api, err = core.NewAuditLogger(logfile, api_impl)
if logfile := c.String(auditLogFlag.Name); logfile != "" {
api, err = core.NewAuditLogger(logfile, api)
if err != nil {
utils.Fatalf(err.Error())
}
@ -154,14 +395,8 @@ func main() {
utils.Fatalf("Could not register signer API: %v", err)
}
// Import from file
if rfile := c.String("requestfile"); rfile != "" {
//Each line of file represents one request
log.Warn("Import from file not yet implemented")
}
// start http server
endpoint := fmt.Sprintf("%s:%d", c.String(utils.RPCListenAddrFlag.Name), c.Int("rpcport"))
endpoint := fmt.Sprintf("%s:%d", c.String(utils.RPCListenAddrFlag.Name), c.Int(rpcPortFlag.Name))
if listener, err = net.Listen("tcp", endpoint); err != nil {
utils.Fatalf("Could not start http listener: %v", err)
}
@ -169,9 +404,9 @@ func main() {
log.Info("HTTP endpoint opened", "url", extapi_url)
cors := []string{"*"}
if c.Bool("stdio-ui-test") {
if c.Bool(testFlag.Name) {
log.Info("Performing UI test")
go testExternalUI(api_impl)
go testExternalUI(apiImpl)
}
ui.OnSignerStartup(core.StartupInfo{
Info: map[string]interface{}{
@ -186,8 +421,95 @@ func main() {
return nil
}
app.Run(os.Args)
// DefaultConfigDir is the default config directory to use for the vaults and other
// persistence requirements.
func DefaultConfigDir() string {
// Try to place the data folder in the user's home dir
home := homeDir()
if home != "" {
if runtime.GOOS == "darwin" {
return filepath.Join(home, "Library", "Signer")
} else if runtime.GOOS == "windows" {
return filepath.Join(home, "AppData", "Roaming", "Signer")
} else {
return filepath.Join(home, ".signer")
}
}
// As we cannot guess a stable location, return empty and handle later
return ""
}
func homeDir() string {
if home := os.Getenv("HOME"); home != "" {
return home
}
if usr, err := user.Current(); err == nil {
return usr.HomeDir
}
return ""
}
func readMasterKey(ctx *cli.Context) ([]byte, error) {
var (
file string
configDir = ctx.String(configdirFlag.Name)
)
if ctx.IsSet(signerSecretFlag.Name) {
file = ctx.String(signerSecretFlag.Name)
} else {
file = filepath.Join(configDir, "secrets.dat")
}
if err := checkFile(file); err != nil {
return nil, err
}
masterKey, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
if len(masterKey) < 256 {
return nil, fmt.Errorf("Master key of insufficient length, expected >255 bytes, got %d", len(masterKey))
}
// Create vault location
vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), masterKey)[:10]))
err = os.Mkdir(vaultLocation, 0700)
if err != nil && !os.IsExist(err) {
return nil, err
}
//!TODO, use KDF to stretch the master key
// stretched_key := stretch_key(master_key)
return masterKey, nil
}
// checkFile is a convenience function to check if a file
// * exists
// * is mode 0600
func checkFile(filename string) error {
info, err := os.Stat(filename)
if err != nil {
return fmt.Errorf("failed stat on %s: %v", filename, err)
}
// Check the unix permission bits
if info.Mode().Perm()&077 != 0 {
return fmt.Errorf("file (%v) has insecure file permissions (%v)", filename, info.Mode().String())
}
return nil
}
// confirm displays a text and asks for user confirmation
func confirm(text string) bool {
fmt.Printf(text)
fmt.Printf("\nEnter 'ok' to proceed:\n>")
text, err := bufio.NewReader(os.Stdin).ReadString('\n')
if err != nil {
log.Crit("Failed to read user input", "err", err)
}
if text := strings.TrimSpace(text); text == "ok" {
return true
}
return false
}
func testExternalUI(api *core.SignerAPI) {
@ -253,4 +575,9 @@ curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","me
// Not supplied
curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_signTransaction","params":["0x82A2A876D39022B3019932D30Cd9c97ad5616813",{"gas":"0x333","gasPrice":"0x123","nonce":"0x0","to":"0x07a565b7ed7d7a678680a4c162885bedbb695fe0", "value":"0x10", "data":"0x4401a6e40000000000000000000000000000000000000000000000000000000000000012"}],"id":67}' http://localhost:8550/
// Sign data
curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_sign","params":["0x694267f14675d7e1b9494fd8d72fefe1755710fa","bazonk gaz baz"],"id":67}' http://localhost:8550/
**/

View file

@ -19,14 +19,16 @@ package rules
import (
"encoding/json"
"fmt"
"os"
"strings"
"github.com/ethereum/go-ethereum/cmd/signer/core"
"github.com/ethereum/go-ethereum/cmd/signer/rules/deps"
"github.com/ethereum/go-ethereum/cmd/signer/storage"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/internal/ethapi"
"github.com/ethereum/go-ethereum/log"
"github.com/robertkrimen/otto"
"os"
"strings"
)
var (
@ -47,17 +49,17 @@ func consoleOutput(call otto.FunctionCall) otto.Value {
// rulesetUi provides an implementation of SignerUI that evaluates a javascript
// file for each defined UI-method
type rulesetUi struct {
// vm *otto.Otto // The JS vm
next core.SignerUI // The next handler, for manual processing
storage storage.Storage
credentials storage.Storage
jsRules string // The rules to use
}
func NewRuleEvaluator(next core.SignerUI) (*rulesetUi, error) {
func NewRuleEvaluator(next core.SignerUI, jsbackend, credentialsBackend storage.Storage) (*rulesetUi, error) {
c := &rulesetUi{
// vm: otto.New(),
next: next,
storage: storage.NewEphemeralStorage(),
storage: jsbackend,
credentials: credentialsBackend,
jsRules: "",
}
@ -97,7 +99,6 @@ func (r *rulesetUi) execute(jsfunc string, jsarg interface{}) (otto.Value, error
// All calls are objects with the parameters being keys in that object.
// To provide additional insulation between js and go, we serialize it into JSON on the Go-side,
// and deserialize it on the JS side.
//argdata := ""
jsonbytes, err := json.Marshal(jsarg)
if err != nil {
@ -142,24 +143,34 @@ func (r *rulesetUi) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse,
jsonreq, err := json.Marshal(request)
approved, err := r.checkApproval("ApproveTx", jsonreq, err)
if err != nil {
log.Info("Rule-based approval error, going to manual", "error", "err")
log.Info("Rule-based approval error, going to manual", "error", err)
return r.next.ApproveTx(request)
}
if approved {
return core.SignTxResponse{Transaction: request.Transaction, Approved: true, Password: ""}, nil
return core.SignTxResponse{
Transaction: request.Transaction,
Approved: true,
Password: r.lookupPassword(request.Transaction.From.Address()),
},
nil
}
return core.SignTxResponse{Approved: false}, err
}
func (r *rulesetUi) lookupPassword(address common.Address) string {
return r.credentials.Get(strings.ToLower(address.String()))
}
func (r *rulesetUi) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) {
jsonreq, err := json.Marshal(request)
approved, err := r.checkApproval("ApproveSignData", jsonreq, err)
if err != nil {
log.Info("Rule-based approval error, going to manual", "error", "err")
log.Info("Rule-based approval error, going to manual", "error", err)
return r.next.ApproveSignData(request)
}
if approved {
return core.SignDataResponse{Approved: true, Password: ""}, nil
return core.SignDataResponse{Approved: true, Password: r.lookupPassword(request.Address.Address())}, nil
}
return core.SignDataResponse{Approved: false, Password: ""}, err
}
@ -168,7 +179,7 @@ func (r *rulesetUi) ApproveExport(request *core.ExportRequest) (core.ExportRespo
jsonreq, err := json.Marshal(request)
approved, err := r.checkApproval("ApproveExport", jsonreq, err)
if err != nil {
log.Info("Rule-based approval error, going to manual", "error", "err")
log.Info("Rule-based approval error, going to manual", "error", err)
return r.next.ApproveExport(request)
}
if approved {
@ -187,7 +198,7 @@ func (r *rulesetUi) ApproveListing(request *core.ListRequest) (core.ListResponse
jsonreq, err := json.Marshal(request)
approved, err := r.checkApproval("ApproveListing", jsonreq, err)
if err != nil {
log.Info("Rule-based approval error, going to manual", "error", "err")
log.Info("Rule-based approval error, going to manual", "error", err)
return r.next.ApproveListing(request)
}
if approved {
@ -211,6 +222,18 @@ func (r *rulesetUi) ShowInfo(message string) {
log.Info(message)
r.next.ShowInfo(message)
}
func (r *rulesetUi) OnSignerStartup(info core.StartupInfo) {
jsonInfo, err := json.Marshal(info)
if err != nil {
log.Warn("failed marshalling data", "data", info)
return
}
r.next.OnSignerStartup(info)
_, err = r.execute("OnSignerStartup", string(jsonInfo))
if err != nil {
log.Info("error occurred during execution", "error", err)
}
}
func (r *rulesetUi) OnApprovedTx(tx ethapi.SignTransactionResult) {
jsonTx, err := json.Marshal(tx)

View file

@ -2,15 +2,17 @@ package rules
import (
"fmt"
"math/big"
"strings"
"testing"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/cmd/signer/core"
"github.com/ethereum/go-ethereum/cmd/signer/storage"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/internal/ethapi"
"math/big"
"strings"
"testing"
)
const JS = `
@ -97,7 +99,7 @@ func (alwaysDenyUi) OnApprovedTx(tx ethapi.SignTransactionResult) {
}
func initRuleEngine(js string) (*rulesetUi, error) {
r, err := NewRuleEvaluator(&alwaysDenyUi{})
r, err := NewRuleEvaluator(&alwaysDenyUi{}, storage.NewEphemeralStorage(), storage.NewEphemeralStorage())
if err != nil {
return nil, fmt.Errorf("Failed to create js engine: %v", err)
}
@ -234,7 +236,9 @@ func TestForwarding(t *testing.T) {
js := ""
ui := &dummyUi{make([]string, 0)}
r, err := NewRuleEvaluator(ui)
jsbackend := storage.NewEphemeralStorage()
credbackend := storage.NewEphemeralStorage()
r, err := NewRuleEvaluator(ui, jsbackend, credbackend)
if err != nil {
t.Fatalf("Failed to create js engine: %v", err)
}
@ -459,10 +463,7 @@ func TestLimitWindow(t *testing.T) {
t.Errorf("Couldn't create evaluator %v", err)
return
}
if err != nil {
t.Error(err)
return
}
// 0.3 ether: 429D069189E0000 wei
v := big.NewInt(0).SetBytes(common.Hex2Bytes("0429D069189E0000"))
h := hexutil.Big(*v)
@ -562,7 +563,7 @@ func TestContextIsCleared(t *testing.T) {
}
`
ui := &dontCallMe{t}
r, err := NewRuleEvaluator(ui)
r, err := NewRuleEvaluator(ui, storage.NewEphemeralStorage(), storage.NewEphemeralStorage())
if err != nil {
t.Fatalf("Failed to create js engine: %v", err)
}
@ -576,3 +577,44 @@ func TestContextIsCleared(t *testing.T) {
t.Errorf("Expected execution context to be cleared between executions")
}
}
func TestSignData(t *testing.T) {
js := `function ApproveListing(){
return "Approve"
}
function ApproveSignData(r){
if( r.address.toLowerCase() == "0x694267f14675d7e1b9494fd8d72fefe1755710fa")
{
if(r.message.indexOf("bazonk") >= 0){
return "Approve"
}
return "Reject"
}
// Otherwise goes to manual processing
}`
r, err := initRuleEngine(js)
if err != nil {
t.Errorf("Couldn't create evaluator %v", err)
return
}
message := []byte("baz bazonk foo")
hash, msg := core.SignHash(message)
raw := hexutil.Bytes(message)
addr, _ := mixAddr("0x694267f14675d7e1b9494fd8d72fefe1755710fa")
fmt.Printf("address %v %v\n", addr.String(), addr.Original())
resp, err := r.ApproveSignData(&core.SignDataRequest{
Address: *addr,
Message: msg,
Hash: hash,
Meta: core.Metadata{"remoteip", "localip", "inproc"},
Rawdata: raw,
})
if err != nil {
t.Fatalf("Unexpected error", err)
}
if !resp.Approved {
t.Fatalf("Expected approved")
}
}

View file

@ -0,0 +1,148 @@
package storage
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/json"
"io"
"io/ioutil"
"os"
"github.com/ethereum/go-ethereum/log"
)
type storedCredential struct {
// The iv
Iv []byte `json:"iv"`
// The ciphertext
CipherText []byte `json:"c"`
}
// AESEncryptedStorage is a storage type which is backed by a json-faile. The json-file contains
// key-value mappings, where the keys are _not_ encrypted, only the values are.
type AESEncryptedStorage struct {
// File to read/write credentials
filename string
// Key stored in base64
key []byte
}
// NewAESEncryptedStorage creates a new encrypted storage backed by the given file/key
func NewAESEncryptedStorage(filename string, key []byte) *AESEncryptedStorage {
return &AESEncryptedStorage{
filename: filename,
key: key,
}
}
// Put stores a value by key. 0-length keys results in no-op
func (s *AESEncryptedStorage) Put(key, value string) {
if len(key) == 0 {
return
}
data, err := s.readEncryptedStorage()
if err != nil {
log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename)
return
}
ciphertext, iv, err := encrypt(s.key, []byte(value))
if err != nil {
log.Warn("Failed to encrypt entry", "err", err)
return
}
encrypted := storedCredential{Iv: iv, CipherText: ciphertext}
data[key] = encrypted
if err = s.writeEncryptedStorage(data); err != nil {
log.Warn("Failed to write entry", "err", err)
}
}
// Get returns the previously stored value, or the empty string if it does not exist or key is of 0-length
func (s *AESEncryptedStorage) Get(key string) string {
if len(key) == 0 {
return ""
}
data, err := s.readEncryptedStorage()
if err != nil {
log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename)
return ""
}
encrypted, exist := data[key]
if !exist {
log.Warn("Key does not exist", "key", key)
return ""
}
entry, err := decrypt(s.key, encrypted.Iv, encrypted.CipherText)
if err != nil {
log.Warn("Failed to decrypt key", "key", key)
return ""
}
return string(entry)
}
// readEncryptedStorage reads the file with encrypted creds
func (s *AESEncryptedStorage) readEncryptedStorage() (map[string]storedCredential, error) {
creds := make(map[string]storedCredential)
raw, err := ioutil.ReadFile(s.filename)
if err != nil {
if os.IsNotExist(err) {
// Doesn't exist yet
return creds, nil
} else {
log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename)
}
}
if err = json.Unmarshal(raw, &creds); err != nil {
log.Warn("Failed to unmarshal encrypted storage", "err", err, "file", s.filename)
return nil, err
}
return creds, nil
}
// writeEncryptedStorage write the file with encrypted creds
func (s *AESEncryptedStorage) writeEncryptedStorage(creds map[string]storedCredential) error {
raw, err := json.Marshal(creds)
if err != nil {
return err
}
if err = ioutil.WriteFile(s.filename, raw, 0600); err != nil {
return err
}
return nil
}
func encrypt(key []byte, plaintext []byte) ([]byte, []byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, nil, err
}
aesgcm, err := cipher.NewGCM(block)
nonce := make([]byte, aesgcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, nil, err
}
if err != nil {
return nil, nil, err
}
ciphertext := aesgcm.Seal(nil, nonce, plaintext, nil)
return ciphertext, nonce, nil
}
func decrypt(key []byte, nonce []byte, ciphertext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, err
}
return plaintext, nil
}

View file

@ -0,0 +1,99 @@
package storage
import (
"bytes"
"fmt"
"io/ioutil"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/mattn/go-colorable"
)
func TestEncryption(t *testing.T) {
// key := []byte("AES256Key-32Characters1234567890")
// plaintext := []byte(value)
key := []byte("AES256Key-32Characters1234567890")
plaintext := []byte("exampleplaintext")
c, iv, err := encrypt(key, plaintext)
if err != nil {
t.Fatal(err)
}
fmt.Printf("Ciphertext %x, nonce %x\n", c, iv)
p, err := decrypt(key, iv, c)
if err != nil {
t.Fatal(err)
}
fmt.Printf("Plaintext %v\n", string(p))
if !bytes.Equal(plaintext, p) {
t.Errorf("Failed: expected plaintext recovery, got %v expected %v", string(plaintext), string(p))
}
}
func TestFileStorage(t *testing.T) {
a := map[string]storedCredential{
"secret": storedCredential{
Iv: common.Hex2Bytes("cdb30036279601aeee60f16b"),
CipherText: common.Hex2Bytes("f311ac49859d7260c2c464c28ffac122daf6be801d3cfd3edcbde7e00c9ff74f"),
},
"secret2": storedCredential{
Iv: common.Hex2Bytes("afb8a7579bf971db9f8ceeed"),
CipherText: common.Hex2Bytes("2df87baf86b5073ef1f03e3cc738de75b511400f5465bb0ddeacf47ae4dc267d"),
},
}
d, err := ioutil.TempDir("", "eth-encrypted-storage-test")
if err != nil {
t.Fatal(err)
}
stored := &AESEncryptedStorage{
filename: fmt.Sprintf("%v/vault.json", d),
key: []byte("AES256Key-32Characters1234567890"),
}
stored.writeEncryptedStorage(a)
read := &AESEncryptedStorage{
filename: fmt.Sprintf("%v/vault.json", d),
key: []byte("AES256Key-32Characters1234567890"),
}
creds, err := read.readEncryptedStorage()
if err != nil {
t.Fatal(err)
}
for k, v := range a {
if v2, exist := creds[k]; !exist {
t.Errorf("Missing entry %v", k)
} else {
if !bytes.Equal(v.CipherText, v2.CipherText) {
t.Errorf("Wrong ciphertext, expected %x got %x", v.CipherText, v2.CipherText)
}
if !bytes.Equal(v.Iv, v2.Iv) {
t.Errorf("Wrong iv")
}
}
}
}
func TestEnd2End(t *testing.T) {
log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(3), log.StreamHandler(colorable.NewColorableStderr(), log.TerminalFormat(true))))
d, err := ioutil.TempDir("", "eth-encrypted-storage-test")
if err != nil {
t.Fatal(err)
}
s1 := &AESEncryptedStorage{
filename: fmt.Sprintf("%v/vault.json", d),
key: []byte("AES256Key-32Characters1234567890"),
}
s2 := &AESEncryptedStorage{
filename: fmt.Sprintf("%v/vault.json", d),
key: []byte("AES256Key-32Characters1234567890"),
}
s1.Put("bazonk", "foobar")
if v := s2.Get("bazonk"); v != "foobar" {
t.Errorf("Expected bazonk->foobar, got '%v'", v)
}
}

View file

@ -26,8 +26,6 @@ type Storage interface {
Put(key, value string)
// Get returns the previously stored value, or the empty string if it does not exist or key is of 0-length
Get(key string) string
// New creates a new (sub) namespace for the storage
New(namespace string) Storage
}
// EphemeralStorage is an in-memory storage that does
@ -41,17 +39,14 @@ func (s *EphemeralStorage) Put(key, value string) {
if len(key) == 0 {
return
}
key = fmt.Sprintf("%s.%s", s.namespace, key)
fmt.Printf("storage: put %v -> %v\n", key, value)
s.data[key] = value
}
func (s *EphemeralStorage) Get(key string) string {
if len(key) == 0 {
return ""
}
key = fmt.Sprintf("%s.%s", s.namespace, key)
fmt.Printf("storage: get %v\n", key)
if v, exist := s.data[key]; exist {
return v
@ -59,18 +54,9 @@ func (s *EphemeralStorage) Get(key string) string {
return ""
}
func (s *EphemeralStorage) New(namespace string) Storage {
child := &EphemeralStorage{
data: make(map[string]string),
namespace: fmt.Sprintf("%s.%s", s.namespace, namespace),
}
return child
}
func NewEphemeralStorage() Storage {
s := &EphemeralStorage{
data: make(map[string]string),
namespace: "root",
}
return s
}

198
cmd/signer/tutorial.md Normal file
View file

@ -0,0 +1,198 @@
## Initializing the signer
First, initialize the master seed.
```text
#./signer init
WARNING!
The signer is alpha software, and not yet publically released. This software has _not_ been audited, and there
are no guarantees about the workings of this software. It may contain severe flaws. You should not use this software
unless you agree to take full responsibility for doing so, and know what you are doing.
TLDR; THIS IS NOT PRODUCTION-READY SOFTWARE!
Enter 'ok' to proceed:
>ok
A master seed has been generated into /home/martin/.signer/secrets.dat
This is required to be able to store credentials, such as :
* Passwords for keystores (used by rule engine)
* Storage for javascript rules
* Hash of rule-file
You should treat that file with utmost secrecy, and make a backup of it.
NOTE: This file does not contain your accounts. Those need to be backed up separately!
```
(for readability purposes, we'll remove the WARNING printout in the rest of this document)
## Creating rules
Now, you can create a rule-file.
```javascript
function ApproveListing(){
return "Approve"
}
```
Get the `sha256` hash....
```text
#sha256sum rules.js
6c21d1737429d6d4f2e55146da0797782f3c0a0355227f19d702df377c165d72 rules.js
```
...And then `attest` the file:
```text
#./signer attest 6c21d1737429d6d4f2e55146da0797782f3c0a0355227f19d702df377c165d72
INFO [02-21|12:14:38] Ruleset attestation updated sha256=6c21d1737429d6d4f2e55146da0797782f3c0a0355227f19d702df377c165d72
```
At this point, we then start the signer with the rule-file:
```text
#./signer --rules rules.json
INFO [02-21|12:15:18] Using CLI as UI-channel
INFO [02-21|12:15:18] Loaded 4byte db signatures=5509 file=./4byte.json
INFO [02-21|12:15:18] Could not load rulefile, rules not enabled file=rulefile
DEBUG[02-21|12:15:18] FS scan times list=35.335µs set=5.536µs diff=5.073µs
DEBUG[02-21|12:15:18] Ledger support enabled
DEBUG[02-21|12:15:18] Trezor support enabled
INFO [02-21|12:15:18] Audit logs configured file=audit.log
INFO [02-21|12:15:18] HTTP endpoint opened url=http://localhost:8550
------- Signer info -------
* extapi_http : http://localhost:8550
* extapi_ipc : <nil>
* extapi_version : 2.0.0
* intapi_version : 1.2.0
```
Any list-requests will now be auto-approved by our rule-file.
## Under the hood
While doing the operations above, these files have been created:
```text
#ls -laR ~/.signer/
/home/martin/.signer/:
total 16
drwx------ 3 martin martin 4096 feb 21 12:14 .
drwxr-xr-x 71 martin martin 4096 feb 21 12:12 ..
drwx------ 2 martin martin 4096 feb 21 12:14 43f73718397aa54d1b22
-rwx------ 1 martin martin 256 feb 21 12:12 secrets.dat
/home/martin/.signer/43f73718397aa54d1b22:
total 12
drwx------ 2 martin martin 4096 feb 21 12:14 .
drwx------ 3 martin martin 4096 feb 21 12:14 ..
-rw------- 1 martin martin 159 feb 21 12:14 config.json
#cat /home/martin/.signer/43f73718397aa54d1b22/config.json
{"ruleset_sha256":{"iv":"6v4W4tfJxj3zZFbl","c":"6dt5RTDiTq93yh1qDEjpsat/tsKG7cb+vr3sza26IPL2fvsQ6ZoqFx++CPUa8yy6fD9Bbq41L01ehkKHTG3pOAeqTW6zc/+t0wv3AB6xPmU="}}
```
In `~/.signer`, the `secrets.dat` file was created, containing the `master_seed`.
The `master_seed` was then used to derive a few other things:
- `vault_location` : in this case `43f73718397aa54d1b22` .
- Thus, if you use a different `master_seed`, another `vault_location` will be used that does not conflict with each other.
- Example: `signer --signersecret /path/to/afile ...`
- `config.json` which is the encrypted key/value storage for configuration data, containing the key `ruleset_sha256`.
## Adding credentials
In order to make more useful rules; sign transactions, the signer needs access to the passwords needed to unlock keystores.
```text
#./signer addpw 0x694267f14675d7e1b9494fd8d72fefe1755710fa test
INFO [02-21|13:43:21] Credential store updated key=0x694267f14675d7e1b9494fd8d72fefe1755710fa
```
## More advanced rules
Now let's update the rules to make use of credentials
```javascript
function ApproveListing(){
return "Approve"
}
function ApproveSignData(r){
if( r.address.toLowerCase() == "0x694267f14675d7e1b9494fd8d72fefe1755710fa")
{
if(r.message.indexOf("bazonk") >= 0){
return "Approve"
}
return "Reject"
}
// Otherwise goes to manual processing
}
```
In this example,
* any requests to sign data with the account `0x694...` will be
* auto-approved if the message contains with `bazonk`,
* and auto-rejected if it does not.
* Any other signing-requests will be passed along for manual approve/reject.
..attest the new file
```text
#sha256sum rules.js
2a0cb661dacfc804b6e95d935d813fd17c0997a7170e4092ffbc34ca976acd9f rules.js
#./signer attest 2a0cb661dacfc804b6e95d935d813fd17c0997a7170e4092ffbc34ca976acd9f
INFO [02-21|14:36:30] Ruleset attestation updated sha256=2a0cb661dacfc804b6e95d935d813fd17c0997a7170e4092ffbc34ca976acd9f
```
And start the signer:
```
#./signer --rules rules.js
INFO [02-21|14:41:56] Using CLI as UI-channel
INFO [02-21|14:41:56] Loaded 4byte db signatures=5509 file=./4byte.json
INFO [02-21|14:41:56] Rule engine configured file=rules.js
DEBUG[02-21|14:41:56] FS scan times list=34.607µs set=4.509µs diff=4.87µs
DEBUG[02-21|14:41:56] Ledger support enabled
DEBUG[02-21|14:41:56] Trezor support enabled
INFO [02-21|14:41:56] Audit logs configured file=audit.log
INFO [02-21|14:41:56] HTTP endpoint opened url=http://localhost:8550
------- Signer info -------
* extapi_version : 2.0.0
* intapi_version : 1.2.0
* extapi_http : http://localhost:8550
* extapi_ipc : <nil>
INFO [02-21|14:41:56] error occurred during execution error="ReferenceError: 'OnSignerStartup' is not defined"
```
And then test signing, once with `bazonk` and once without:
```
#curl -H "Content-Type: application/json" -X POST --data "{\"jsonrpc\":\"2.0\",\"method\":\"account_sign\",\"params\":[\"0x694267f14675d7e1b9494fd8d72fefe1755710fa\",\"0x$(xxd -pu <<< ' bazonk baz gaz')\"],\"id\":67}" http://localhost:8550/
{"jsonrpc":"2.0","id":67,"result":"0x93e6161840c3ae1efc26dc68dedab6e8fc233bb3fefa1b4645dbf6609b93dace160572ea4ab33240256bb6d3dadb60dcd9c515d6374d3cf614ee897408d41d541c"}
#curl -H "Content-Type: application/json" -X POST --data "{\"jsonrpc\":\"2.0\",\"method\":\"account_sign\",\"params\":[\"0x694267f14675d7e1b9494fd8d72fefe1755710fa\",\"0x$(xxd -pu <<< ' bonk baz gaz')\"],\"id\":67}" http://localhost:8550/
{"jsonrpc":"2.0","id":67,"error":{"code":-32000,"message":"Request denied"}}
```
Meanwhile, in the signer output:
```text
INFO [02-21|14:42:41] Op approved
INFO [02-21|14:42:56] Op rejected
```
The signer also stores all traffic over the external API in a log file. The last 4 lines shows the two requests and their responses:
```text
#tail audit.log -n 4
t=2018-02-21T14:42:41+0100 lvl=info msg=Sign api=signer type=request metadata="{\"remote\":\"127.0.0.1:49706\",\"local\":\"localhost:8550\",\"scheme\":\"HTTP/1.1\"}" addr="0x694267f14675d7e1b9494fd8d72fefe1755710fa [chksum INVALID]" data=202062617a6f6e6b2062617a2067617a0a
t=2018-02-21T14:42:42+0100 lvl=info msg=Sign api=signer type=response data=93e6161840c3ae1efc26dc68dedab6e8fc233bb3fefa1b4645dbf6609b93dace160572ea4ab33240256bb6d3dadb60dcd9c515d6374d3cf614ee897408d41d541c error=nil
t=2018-02-21T14:42:56+0100 lvl=info msg=Sign api=signer type=request metadata="{\"remote\":\"127.0.0.1:49708\",\"local\":\"localhost:8550\",\"scheme\":\"HTTP/1.1\"}" addr="0x694267f14675d7e1b9494fd8d72fefe1755710fa [chksum INVALID]" data=2020626f6e6b2062617a2067617a0a
t=2018-02-21T14:42:56+0100 lvl=info msg=Sign api=signer type=response data= error="Request denied"
```