mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-18 09:53:48 +00:00
signer/storage: implement aes-gcm-backed credential storage
This commit is contained in:
parent
76a8c7caf0
commit
ec83d9d0ab
16 changed files with 1059 additions and 217 deletions
|
|
@ -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)
|
||||
--loglevel value log level to emit to the screen (default: 4)
|
||||
--keystore value Directory for the keystore (default: "/home/martin/.ethereum/keystore")
|
||||
--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)
|
||||
--4bytedb value File containing 4byte-identifiers (default: "./4byte.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.
|
||||
--help, -h show help
|
||||
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
|
||||
- Currently, the Geth API:s use `common.Address` in the arguments to transaction submission (e.g `to` field). This
|
||||
- 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.
|
||||
- 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
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
// "bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
|
|
|||
|
|
@ -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{}) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)) }
|
||||
|
|
|
|||
|
|
@ -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,152 +53,463 @@ 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()
|
||||
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",
|
||||
}
|
||||
keystoreFlag = cli.StringFlag{
|
||||
Name: "keystore",
|
||||
Value: filepath.Join(node.DefaultDataDir(), "keystore"),
|
||||
Usage: "Directory for the keystore",
|
||||
}
|
||||
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,
|
||||
}
|
||||
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",
|
||||
}
|
||||
customDBFlag = cli.StringFlag{
|
||||
Name: "4bytedb-custom",
|
||||
Usage: "File used for writing new 4byte-identifiers submitted via API",
|
||||
Value: "./4byte-custom.json",
|
||||
}
|
||||
auditLogFlag = cli.StringFlag{
|
||||
Name: "auditlog",
|
||||
Usage: "File used to emit audit logs. Set to \"\" to disable",
|
||||
Value: "audit.log",
|
||||
}
|
||||
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.",
|
||||
}
|
||||
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.`,
|
||||
}
|
||||
|
||||
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{
|
||||
cli.IntFlag{
|
||||
Name: "loglevel",
|
||||
Value: 4,
|
||||
Usage: "log level to emit to the screen",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "keystore",
|
||||
Value: filepath.Join(node.DefaultDataDir(), "keystore"),
|
||||
Usage: "Directory for the keystore",
|
||||
},
|
||||
logLevelFlag,
|
||||
keystoreFlag,
|
||||
configdirFlag,
|
||||
utils.NetworkIdFlag,
|
||||
utils.LightKDFFlag,
|
||||
utils.NoUSBFlag,
|
||||
utils.RPCListenAddrFlag,
|
||||
cli.IntFlag{
|
||||
Name: "rpcport",
|
||||
Usage: "HTTP-RPC server listening port",
|
||||
Value: node.DefaultHTTPPort + 5,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "4bytedb",
|
||||
Usage: "File containing 4byte-identifiers",
|
||||
Value: "./4byte.json",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "4bytedb-custom",
|
||||
Usage: "File used for writing new 4byte-identifiers submitted via API",
|
||||
Value: "./4byte-custom.json",
|
||||
},
|
||||
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{
|
||||
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{
|
||||
Name: "stdio-ui-test",
|
||||
Usage: "Mechanism to test interface between signer and UI. Requires 'stdio-ui'.",
|
||||
},
|
||||
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
|
||||
}
|
||||
|
||||
app.Action = func(c *cli.Context) error {
|
||||
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)
|
||||
|
||||
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))))
|
||||
// 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
|
||||
}
|
||||
|
||||
if c.Bool("stdio-ui") {
|
||||
ui = core.NewStdIOUI()
|
||||
} else {
|
||||
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"))
|
||||
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
|
||||
)
|
||||
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()
|
||||
}
|
||||
db, err := core.NewAbiDBFromFiles(c.String(dBFlag.Name), c.String(customDBFlag.Name))
|
||||
if err != nil {
|
||||
utils.Fatalf(err.Error())
|
||||
}
|
||||
log.Info("Loaded 4byte db", "signatures", db.Size(), "file", c.String("4bytedb"))
|
||||
|
||||
var (
|
||||
api core.ExternalAPI
|
||||
listener net.Listener
|
||||
server = rpc.NewServer()
|
||||
)
|
||||
|
||||
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())
|
||||
}
|
||||
log.Info("Loaded 4byte db", "signatures", db.Size(), "file", c.String("4bytedb"))
|
||||
vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10]))
|
||||
|
||||
var (
|
||||
api core.ExternalAPI
|
||||
listener net.Listener
|
||||
server = rpc.NewServer()
|
||||
)
|
||||
// Generate domain specific keys
|
||||
pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey)
|
||||
jskey := crypto.Keccak256([]byte("jsstorage"), stretchedKey)
|
||||
confkey := crypto.Keccak256([]byte("config"), stretchedKey)
|
||||
|
||||
api_impl := core.NewSignerAPI(
|
||||
c.Int64(utils.NetworkIdFlag.Name),
|
||||
c.String("keystore"),
|
||||
c.Bool(utils.NoUSBFlag.Name),
|
||||
ui, db,
|
||||
c.Bool(utils.LightKDFFlag.Name))
|
||||
// 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)
|
||||
|
||||
api = api_impl
|
||||
|
||||
// Audit logging
|
||||
if logfile := c.String("auditlog"); logfile != "" {
|
||||
api, err = core.NewAuditLogger(logfile, api_impl)
|
||||
if err != nil {
|
||||
utils.Fatalf(err.Error())
|
||||
//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))
|
||||
}
|
||||
log.Info("Audit logs configured", "file", logfile)
|
||||
}
|
||||
// register signer API with server
|
||||
if err = server.RegisterName("account", api); err != nil {
|
||||
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"))
|
||||
if listener, err = net.Listen("tcp", endpoint); err != nil {
|
||||
utils.Fatalf("Could not start http listener: %v", err)
|
||||
}
|
||||
extapi_url := fmt.Sprintf("http://%s", endpoint)
|
||||
log.Info("HTTP endpoint opened", "url", extapi_url)
|
||||
cors := []string{"*"}
|
||||
|
||||
if c.Bool("stdio-ui-test") {
|
||||
log.Info("Performing UI test")
|
||||
go testExternalUI(api_impl)
|
||||
}
|
||||
ui.OnSignerStartup(core.StartupInfo{
|
||||
Info: map[string]interface{}{
|
||||
"extapi_version": EXT_API_VERSION,
|
||||
"intapi_version": INT_API_VERSION,
|
||||
"extapi_http": extapi_url,
|
||||
"extapi_ipc": nil,
|
||||
},
|
||||
})
|
||||
|
||||
rpc.NewHTTPServer(cors, server).Serve(listener)
|
||||
|
||||
return nil
|
||||
}
|
||||
app.Run(os.Args)
|
||||
|
||||
apiImpl := core.NewSignerAPI(
|
||||
c.Int64(utils.NetworkIdFlag.Name),
|
||||
c.String(keystoreFlag.Name),
|
||||
c.Bool(utils.NoUSBFlag.Name),
|
||||
ui, db,
|
||||
c.Bool(utils.LightKDFFlag.Name))
|
||||
|
||||
api = apiImpl
|
||||
|
||||
// Audit logging
|
||||
if logfile := c.String(auditLogFlag.Name); logfile != "" {
|
||||
api, err = core.NewAuditLogger(logfile, api)
|
||||
if err != nil {
|
||||
utils.Fatalf(err.Error())
|
||||
}
|
||||
log.Info("Audit logs configured", "file", logfile)
|
||||
}
|
||||
// register signer API with server
|
||||
if err = server.RegisterName("account", api); err != nil {
|
||||
utils.Fatalf("Could not register signer API: %v", err)
|
||||
}
|
||||
|
||||
// start http server
|
||||
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)
|
||||
}
|
||||
extapi_url := fmt.Sprintf("http://%s", endpoint)
|
||||
log.Info("HTTP endpoint opened", "url", extapi_url)
|
||||
cors := []string{"*"}
|
||||
|
||||
if c.Bool(testFlag.Name) {
|
||||
log.Info("Performing UI test")
|
||||
go testExternalUI(apiImpl)
|
||||
}
|
||||
ui.OnSignerStartup(core.StartupInfo{
|
||||
Info: map[string]interface{}{
|
||||
"extapi_version": EXT_API_VERSION,
|
||||
"intapi_version": INT_API_VERSION,
|
||||
"extapi_http": extapi_url,
|
||||
"extapi_ipc": nil,
|
||||
},
|
||||
})
|
||||
|
||||
rpc.NewHTTPServer(cors, server).Serve(listener)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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/
|
||||
|
||||
|
||||
**/
|
||||
|
|
|
|||
|
|
@ -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,18 +49,18 @@ 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
|
||||
jsRules string // The rules to use
|
||||
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(),
|
||||
jsRules: "",
|
||||
next: next,
|
||||
storage: jsbackend,
|
||||
credentials: credentialsBackend,
|
||||
jsRules: "",
|
||||
}
|
||||
|
||||
return c, nil
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
148
cmd/signer/storage/aes_gcm_storage.go
Normal file
148
cmd/signer/storage/aes_gcm_storage.go
Normal 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
|
||||
}
|
||||
99
cmd/signer/storage/aes_gcm_storage_test.go
Normal file
99
cmd/signer/storage/aes_gcm_storage_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
data: make(map[string]string),
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
|
|
|||
198
cmd/signer/tutorial.md
Normal file
198
cmd/signer/tutorial.md
Normal 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"
|
||||
```
|
||||
Loading…
Reference in a new issue