| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122 |
- // Copyright 2018 The go-ethereum Authors
- // This file is part of go-ethereum.
- //
- // go-ethereum is free software: you can redistribute it and/or modify
- // it under the terms of the GNU General Public License as published by
- // the Free Software Foundation, either version 3 of the License, or
- // (at your option) any later version.
- //
- // go-ethereum is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU General Public License for more details.
- //
- // You should have received a copy of the GNU General Public License
- // along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
- package main
- import (
- "bufio"
- "context"
- "crypto/rand"
- "crypto/sha256"
- "encoding/hex"
- "encoding/json"
- "fmt"
- "io"
- "io/ioutil"
- "math/big"
- "os"
- "os/signal"
- "os/user"
- "path/filepath"
- "runtime"
- "strings"
- "time"
- "github.com/ethereum/go-ethereum/accounts"
- "github.com/ethereum/go-ethereum/accounts/keystore"
- "github.com/ethereum/go-ethereum/cmd/utils"
- "github.com/ethereum/go-ethereum/common"
- "github.com/ethereum/go-ethereum/common/hexutil"
- "github.com/ethereum/go-ethereum/console/prompt"
- "github.com/ethereum/go-ethereum/core/types"
- "github.com/ethereum/go-ethereum/crypto"
- "github.com/ethereum/go-ethereum/internal/ethapi"
- "github.com/ethereum/go-ethereum/log"
- "github.com/ethereum/go-ethereum/node"
- "github.com/ethereum/go-ethereum/params"
- "github.com/ethereum/go-ethereum/rlp"
- "github.com/ethereum/go-ethereum/rpc"
- "github.com/ethereum/go-ethereum/signer/core"
- "github.com/ethereum/go-ethereum/signer/fourbyte"
- "github.com/ethereum/go-ethereum/signer/rules"
- "github.com/ethereum/go-ethereum/signer/storage"
- colorable "github.com/mattn/go-colorable"
- "github.com/mattn/go-isatty"
- "gopkg.in/urfave/cli.v1"
- )
- const legalWarning = `
- WARNING!
- Clef is an account management tool. It may, like any software, contain bugs.
- Please take care to
- - backup your keystore files,
- - verify that the keystore(s) can be opened with your password.
- Clef is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
- without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
- PURPOSE. See the GNU General Public License for more details.
- `
- var (
- logLevelFlag = cli.IntFlag{
- Name: "loglevel",
- Value: 4,
- Usage: "log level to emit to the screen",
- }
- advancedMode = cli.BoolFlag{
- Name: "advanced",
- Usage: "If enabled, issues warnings instead of rejections for suspicious requests. Default off",
- }
- acceptFlag = cli.BoolFlag{
- Name: "suppress-bootwarn",
- Usage: "If set, does not show the warning during boot",
- }
- 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 Clef configuration",
- }
- chainIdFlag = cli.Int64Flag{
- Name: "chainid",
- Value: params.MainnetChainConfig.ChainID.Int64(),
- Usage: "Chain id to use for signing (1=mainnet, 3=Ropsten, 4=Rinkeby, 5=Goerli)",
- }
- 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 (encrypted) master seed to encrypt Clef data, e.g. keystore credentials and ruleset hash",
- }
- 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: "Path to the rule file to auto-authorize requests with",
- }
- 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 Clef is started by an external process.",
- }
- testFlag = cli.BoolFlag{
- Name: "stdio-ui-test",
- Usage: "Mechanism to test interface between Clef 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 Clef 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
- Clef that the file is 'safe' to execute.`,
- }
- setCredentialCommand = cli.Command{
- Action: utils.MigrateFlags(setCredential),
- Name: "setpw",
- Usage: "Store a credential for a keystore file",
- ArgsUsage: "<address>",
- Flags: []cli.Flag{
- logLevelFlag,
- configdirFlag,
- signerSecretFlag,
- },
- Description: `
- The setpw command stores a password for a given address (keyfile).
- `}
- delCredentialCommand = cli.Command{
- Action: utils.MigrateFlags(removeCredential),
- Name: "delpw",
- Usage: "Remove a credential for a keystore file",
- ArgsUsage: "<address>",
- Flags: []cli.Flag{
- logLevelFlag,
- configdirFlag,
- signerSecretFlag,
- },
- Description: `
- The delpw command removes a password for a given address (keyfile).
- `}
- newAccountCommand = cli.Command{
- Action: utils.MigrateFlags(newAccount),
- Name: "newaccount",
- Usage: "Create a new account",
- ArgsUsage: "",
- Flags: []cli.Flag{
- logLevelFlag,
- keystoreFlag,
- utils.LightKDFFlag,
- acceptFlag,
- },
- Description: `
- The newaccount command creates a new keystore-backed account. It is a convenience-method
- which can be used in lieu of an external UI.`,
- }
- gendocCommand = cli.Command{
- Action: GenDoc,
- Name: "gendoc",
- Usage: "Generate documentation about json-rpc format",
- Description: `
- The gendoc generates example structures of the json-rpc communication types.
- `}
- )
- func init() {
- app.Name = "Clef"
- app.Usage = "Manage Ethereum account operations"
- app.Flags = []cli.Flag{
- logLevelFlag,
- keystoreFlag,
- configdirFlag,
- chainIdFlag,
- utils.LightKDFFlag,
- utils.NoUSBFlag,
- utils.SmartCardDaemonPathFlag,
- utils.HTTPListenAddrFlag,
- utils.HTTPVirtualHostsFlag,
- utils.IPCDisabledFlag,
- utils.IPCPathFlag,
- utils.HTTPEnabledFlag,
- rpcPortFlag,
- signerSecretFlag,
- customDBFlag,
- auditLogFlag,
- ruleFlag,
- stdiouiFlag,
- testFlag,
- advancedMode,
- acceptFlag,
- }
- app.Action = signer
- app.Commands = []cli.Command{initCommand,
- attestCommand,
- setCredentialCommand,
- delCredentialCommand,
- newAccountCommand,
- gendocCommand}
- cli.CommandHelpTemplate = utils.OriginCommandHelpTemplate
- }
- func main() {
- if err := app.Run(os.Args); err != nil {
- fmt.Fprintln(os.Stderr, err)
- os.Exit(1)
- }
- }
- func initializeSecrets(c *cli.Context) error {
- // Get past the legal message
- if err := initialize(c); err != nil {
- return err
- }
- // Ensure the master key does not yet exist, we're not willing to overwrite
- configDir := c.GlobalString(configdirFlag.Name)
- if err := os.Mkdir(configDir, 0700); err != nil && !os.IsExist(err) {
- return err
- }
- location := filepath.Join(configDir, "masterseed.json")
- if _, err := os.Stat(location); err == nil {
- return fmt.Errorf("master key %v already exists, will not overwrite", location)
- }
- // Key file does not exist yet, generate a new one and encrypt it
- masterSeed := make([]byte, 256)
- num, err := io.ReadFull(rand.Reader, masterSeed)
- if err != nil {
- return err
- }
- if num != len(masterSeed) {
- return fmt.Errorf("failed to read enough random")
- }
- n, p := keystore.StandardScryptN, keystore.StandardScryptP
- if c.GlobalBool(utils.LightKDFFlag.Name) {
- n, p = keystore.LightScryptN, keystore.LightScryptP
- }
- text := "The master seed of clef will be locked with a password.\nPlease specify a password. Do not forget this password!"
- var password string
- for {
- password = getPassPhrase(text, true)
- if err := core.ValidatePasswordFormat(password); err != nil {
- fmt.Printf("invalid password: %v\n", err)
- } else {
- fmt.Println()
- break
- }
- }
- cipherSeed, err := encryptSeed(masterSeed, []byte(password), n, p)
- if err != nil {
- return fmt.Errorf("failed to encrypt master seed: %v", err)
- }
- // Double check the master key path to ensure nothing wrote there in between
- if err = os.Mkdir(configDir, 0700); err != nil && !os.IsExist(err) {
- return err
- }
- if _, err := os.Stat(location); err == nil {
- return fmt.Errorf("master key %v already exists, will not overwrite", location)
- }
- // Write the file and print the usual warning message
- if err = ioutil.WriteFile(location, cipherSeed, 0400); 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 auto-signing rules
- * Hash of JavaScript rule-file
- You should treat 'masterseed.json' with utmost secrecy and make a backup of it!
- * The password is necessary but not enough, you need to back up the master seed too!
- * The master seed 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, nil)
- if err != nil {
- utils.Fatalf(err.Error())
- }
- configDir := ctx.GlobalString(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 setCredential(ctx *cli.Context) error {
- if len(ctx.Args()) < 1 {
- utils.Fatalf("This command requires an address to be passed as an argument")
- }
- if err := initialize(ctx); err != nil {
- return err
- }
- addr := ctx.Args().First()
- if !common.IsHexAddress(addr) {
- utils.Fatalf("Invalid address specified: %s", addr)
- }
- address := common.HexToAddress(addr)
- password := getPassPhrase("Please enter a password to store for this address:", true)
- fmt.Println()
- stretchedKey, err := readMasterKey(ctx, nil)
- if err != nil {
- utils.Fatalf(err.Error())
- }
- configDir := ctx.GlobalString(configdirFlag.Name)
- vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10]))
- pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey)
- pwStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey)
- pwStorage.Put(address.Hex(), password)
- log.Info("Credential store updated", "set", address)
- return nil
- }
- func removeCredential(ctx *cli.Context) error {
- if len(ctx.Args()) < 1 {
- utils.Fatalf("This command requires an address to be passed as an argument")
- }
- if err := initialize(ctx); err != nil {
- return err
- }
- addr := ctx.Args().First()
- if !common.IsHexAddress(addr) {
- utils.Fatalf("Invalid address specified: %s", addr)
- }
- address := common.HexToAddress(addr)
- stretchedKey, err := readMasterKey(ctx, nil)
- if err != nil {
- utils.Fatalf(err.Error())
- }
- configDir := ctx.GlobalString(configdirFlag.Name)
- vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10]))
- pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey)
- pwStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey)
- pwStorage.Del(address.Hex())
- log.Info("Credential store updated", "unset", address)
- return nil
- }
- func newAccount(c *cli.Context) error {
- if err := initialize(c); err != nil {
- return err
- }
- // The newaccount is meant for users using the CLI, since 'real' external
- // UIs can use the UI-api instead. So we'll just use the native CLI UI here.
- var (
- ui = core.NewCommandlineUI()
- pwStorage storage.Storage = &storage.NoStorage{}
- ksLoc = c.GlobalString(keystoreFlag.Name)
- lightKdf = c.GlobalBool(utils.LightKDFFlag.Name)
- )
- log.Info("Starting clef", "keystore", ksLoc, "light-kdf", lightKdf)
- am := core.StartClefAccountManager(ksLoc, true, lightKdf, "")
- // This gives is us access to the external API
- apiImpl := core.NewSignerAPI(am, 0, true, ui, nil, false, pwStorage)
- // This gives us access to the internal API
- internalApi := core.NewUIServerAPI(apiImpl)
- addr, err := internalApi.New(context.Background())
- if err == nil {
- fmt.Printf("Generated account %v\n", addr.String())
- }
- return err
- }
- func initialize(c *cli.Context) error {
- // Set up the logger to print everything
- logOutput := os.Stdout
- if c.GlobalBool(stdiouiFlag.Name) {
- logOutput = os.Stderr
- // If using the stdioui, we can't do the 'confirm'-flow
- if !c.GlobalBool(acceptFlag.Name) {
- fmt.Fprint(logOutput, legalWarning)
- }
- } else if !c.GlobalBool(acceptFlag.Name) {
- if !confirm(legalWarning) {
- return fmt.Errorf("aborted by user")
- }
- fmt.Println()
- }
- usecolor := (isatty.IsTerminal(os.Stderr.Fd()) || isatty.IsCygwinTerminal(os.Stderr.Fd())) && os.Getenv("TERM") != "dumb"
- output := io.Writer(logOutput)
- if usecolor {
- output = colorable.NewColorable(logOutput)
- }
- log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(c.Int(logLevelFlag.Name)), log.StreamHandler(output, log.TerminalFormat(usecolor))))
- return nil
- }
- // ipcEndpoint resolves an IPC endpoint based on a configured value, taking into
- // account the set data folders as well as the designated platform we're currently
- // running on.
- func ipcEndpoint(ipcPath, datadir string) string {
- // On windows we can only use plain top-level pipes
- if runtime.GOOS == "windows" {
- if strings.HasPrefix(ipcPath, `\\.\pipe\`) {
- return ipcPath
- }
- return `\\.\pipe\` + ipcPath
- }
- // Resolve names into the data directory full paths otherwise
- if filepath.Base(ipcPath) == ipcPath {
- if datadir == "" {
- return filepath.Join(os.TempDir(), ipcPath)
- }
- return filepath.Join(datadir, ipcPath)
- }
- return ipcPath
- }
- func signer(c *cli.Context) error {
- // If we have some unrecognized command, bail out
- if args := c.Args(); len(args) > 0 {
- return fmt.Errorf("invalid command: %q", args[0])
- }
- if err := initialize(c); err != nil {
- return err
- }
- var (
- ui core.UIClientAPI
- )
- if c.GlobalBool(stdiouiFlag.Name) {
- log.Info("Using stdin/stdout as UI-channel")
- ui = core.NewStdIOUI()
- } else {
- log.Info("Using CLI as UI-channel")
- ui = core.NewCommandlineUI()
- }
- // 4bytedb data
- fourByteLocal := c.GlobalString(customDBFlag.Name)
- db, err := fourbyte.NewWithFile(fourByteLocal)
- if err != nil {
- utils.Fatalf(err.Error())
- }
- embeds, locals := db.Size()
- log.Info("Loaded 4byte database", "embeds", embeds, "locals", locals, "local", fourByteLocal)
- var (
- api core.ExternalAPI
- pwStorage storage.Storage = &storage.NoStorage{}
- )
- configDir := c.GlobalString(configdirFlag.Name)
- if stretchedKey, err := readMasterKey(c, ui); err != nil {
- log.Warn("Failed to open master, rules disabled", "err", err)
- } else {
- 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
- pwStorage = storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey)
- jsStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "jsstorage.json"), jskey)
- configStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "config.json"), confkey)
- // Do we have a rule-file?
- if ruleFile := c.GlobalString(ruleFlag.Name); ruleFile != "" {
- ruleJS, err := ioutil.ReadFile(ruleFile)
- if err != nil {
- log.Warn("Could not load rules, disabling", "file", ruleFile, "err", err)
- } else {
- shasum := sha256.Sum256(ruleJS)
- foundShaSum := hex.EncodeToString(shasum[:])
- storedShasum, _ := configStorage.Get("ruleset_sha256")
- if storedShasum != foundShaSum {
- log.Warn("Rule hash not attested, disabling", "hash", foundShaSum, "attested", storedShasum)
- } else {
- // Initialize rules
- ruleEngine, err := rules.NewRuleEvaluator(ui, jsStorage)
- if err != nil {
- utils.Fatalf(err.Error())
- }
- ruleEngine.Init(string(ruleJS))
- ui = ruleEngine
- log.Info("Rule engine configured", "file", c.String(ruleFlag.Name))
- }
- }
- }
- }
- var (
- chainId = c.GlobalInt64(chainIdFlag.Name)
- ksLoc = c.GlobalString(keystoreFlag.Name)
- lightKdf = c.GlobalBool(utils.LightKDFFlag.Name)
- advanced = c.GlobalBool(advancedMode.Name)
- nousb = c.GlobalBool(utils.NoUSBFlag.Name)
- scpath = c.GlobalString(utils.SmartCardDaemonPathFlag.Name)
- )
- log.Info("Starting signer", "chainid", chainId, "keystore", ksLoc,
- "light-kdf", lightKdf, "advanced", advanced)
- am := core.StartClefAccountManager(ksLoc, nousb, lightKdf, scpath)
- apiImpl := core.NewSignerAPI(am, chainId, nousb, ui, db, advanced, pwStorage)
- // Establish the bidirectional communication, by creating a new UI backend and registering
- // it with the UI.
- ui.RegisterUIServer(core.NewUIServerAPI(apiImpl))
- api = apiImpl
- // Audit logging
- if logfile := c.GlobalString(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
- var (
- extapiURL = "n/a"
- ipcapiURL = "n/a"
- )
- rpcAPI := []rpc.API{
- {
- Namespace: "account",
- Public: true,
- Service: api,
- Version: "1.0"},
- }
- if c.GlobalBool(utils.HTTPEnabledFlag.Name) {
- vhosts := splitAndTrim(c.GlobalString(utils.HTTPVirtualHostsFlag.Name))
- cors := splitAndTrim(c.GlobalString(utils.HTTPCORSDomainFlag.Name))
- srv := rpc.NewServer()
- err := node.RegisterApisFromWhitelist(rpcAPI, []string{"account"}, srv, false)
- if err != nil {
- utils.Fatalf("Could not register API: %w", err)
- }
- handler := node.NewHTTPHandlerStack(srv, cors, vhosts)
- // start http server
- httpEndpoint := fmt.Sprintf("%s:%d", c.GlobalString(utils.HTTPListenAddrFlag.Name), c.Int(rpcPortFlag.Name))
- httpServer, addr, err := node.StartHTTPEndpoint(httpEndpoint, rpc.DefaultHTTPTimeouts, handler)
- if err != nil {
- utils.Fatalf("Could not start RPC api: %v", err)
- }
- extapiURL = fmt.Sprintf("http://%v/", addr)
- log.Info("HTTP endpoint opened", "url", extapiURL)
- defer func() {
- // Don't bother imposing a timeout here.
- httpServer.Shutdown(context.Background())
- log.Info("HTTP endpoint closed", "url", extapiURL)
- }()
- }
- if !c.GlobalBool(utils.IPCDisabledFlag.Name) {
- givenPath := c.GlobalString(utils.IPCPathFlag.Name)
- ipcapiURL = ipcEndpoint(filepath.Join(givenPath, "clef.ipc"), configDir)
- listener, _, err := rpc.StartIPCEndpoint(ipcapiURL, rpcAPI)
- if err != nil {
- utils.Fatalf("Could not start IPC api: %v", err)
- }
- log.Info("IPC endpoint opened", "url", ipcapiURL)
- defer func() {
- listener.Close()
- log.Info("IPC endpoint closed", "url", ipcapiURL)
- }()
- }
- if c.GlobalBool(testFlag.Name) {
- log.Info("Performing UI test")
- go testExternalUI(apiImpl)
- }
- ui.OnSignerStartup(core.StartupInfo{
- Info: map[string]interface{}{
- "intapi_version": core.InternalAPIVersion,
- "extapi_version": core.ExternalAPIVersion,
- "extapi_http": extapiURL,
- "extapi_ipc": ipcapiURL,
- },
- })
- abortChan := make(chan os.Signal, 1)
- signal.Notify(abortChan, os.Interrupt)
- sig := <-abortChan
- log.Info("Exiting...", "signal", sig)
- return nil
- }
- // splitAndTrim splits input separated by a comma
- // and trims excessive white space from the substrings.
- func splitAndTrim(input string) []string {
- result := strings.Split(input, ",")
- for i, r := range result {
- result[i] = strings.TrimSpace(r)
- }
- return result
- }
- // 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" {
- appdata := os.Getenv("APPDATA")
- if appdata != "" {
- return filepath.Join(appdata, "Signer")
- } else {
- return filepath.Join(home, "AppData", "Roaming", "Signer")
- }
- } else {
- return filepath.Join(home, ".clef")
- }
- }
- // 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, ui core.UIClientAPI) ([]byte, error) {
- var (
- file string
- configDir = ctx.GlobalString(configdirFlag.Name)
- )
- if ctx.GlobalIsSet(signerSecretFlag.Name) {
- file = ctx.GlobalString(signerSecretFlag.Name)
- } else {
- file = filepath.Join(configDir, "masterseed.json")
- }
- if err := checkFile(file); err != nil {
- return nil, err
- }
- cipherKey, err := ioutil.ReadFile(file)
- if err != nil {
- return nil, err
- }
- var password string
- // If ui is not nil, get the password from ui.
- if ui != nil {
- resp, err := ui.OnInputRequired(core.UserInputRequest{
- Title: "Master Password",
- Prompt: "Please enter the password to decrypt the master seed",
- IsPassword: true})
- if err != nil {
- return nil, err
- }
- password = resp.Text
- } else {
- password = getPassPhrase("Decrypt master seed of clef", false)
- }
- masterSeed, err := decryptSeed(cipherKey, password)
- if err != nil {
- return nil, fmt.Errorf("failed to decrypt the master seed of clef")
- }
- if len(masterSeed) < 256 {
- return nil, fmt.Errorf("master seed of insufficient length, expected >255 bytes, got %d", len(masterSeed))
- }
- // Create vault location
- vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), masterSeed)[:10]))
- err = os.Mkdir(vaultLocation, 0700)
- if err != nil && !os.IsExist(err) {
- return nil, err
- }
- return masterSeed, nil
- }
- // checkFile is a convenience function to check if a file
- // * exists
- // * is mode 0400
- 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()&0377 != 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.Print(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) {
- ctx := context.WithValue(context.Background(), "remote", "clef binary")
- ctx = context.WithValue(ctx, "scheme", "in-proc")
- ctx = context.WithValue(ctx, "local", "main")
- errs := make([]string, 0)
- a := common.HexToAddress("0xdeadbeef000000000000000000000000deadbeef")
- addErr := func(errStr string) {
- log.Info("Test error", "err", errStr)
- errs = append(errs, errStr)
- }
- queryUser := func(q string) string {
- resp, err := api.UI.OnInputRequired(core.UserInputRequest{
- Title: "Testing",
- Prompt: q,
- })
- if err != nil {
- addErr(err.Error())
- }
- return resp.Text
- }
- expectResponse := func(testcase, question, expect string) {
- if got := queryUser(question); got != expect {
- addErr(fmt.Sprintf("%s: got %v, expected %v", testcase, got, expect))
- }
- }
- expectApprove := func(testcase string, err error) {
- if err == nil || err == accounts.ErrUnknownAccount {
- return
- }
- addErr(fmt.Sprintf("%v: expected no error, got %v", testcase, err.Error()))
- }
- expectDeny := func(testcase string, err error) {
- if err == nil || err != core.ErrRequestDenied {
- addErr(fmt.Sprintf("%v: expected ErrRequestDenied, got %v", testcase, err))
- }
- }
- var delay = 1 * time.Second
- // Test display of info and error
- {
- api.UI.ShowInfo("If you see this message, enter 'yes' to next question")
- time.Sleep(delay)
- expectResponse("showinfo", "Did you see the message? [yes/no]", "yes")
- api.UI.ShowError("If you see this message, enter 'yes' to the next question")
- time.Sleep(delay)
- expectResponse("showerror", "Did you see the message? [yes/no]", "yes")
- }
- { // Sign data test - clique header
- api.UI.ShowInfo("Please approve the next request for signing a clique header")
- time.Sleep(delay)
- cliqueHeader := types.Header{
- ParentHash: common.HexToHash("0000H45H"),
- UncleHash: common.HexToHash("0000H45H"),
- Coinbase: common.HexToAddress("0000H45H"),
- Root: common.HexToHash("0000H00H"),
- TxHash: common.HexToHash("0000H45H"),
- ReceiptHash: common.HexToHash("0000H45H"),
- Difficulty: big.NewInt(1337),
- Number: big.NewInt(1337),
- GasLimit: 1338,
- GasUsed: 1338,
- Time: 1338,
- Extra: []byte("Extra data Extra data Extra data Extra data Extra data Extra data Extra data Extra data"),
- MixDigest: common.HexToHash("0x0000H45H"),
- }
- cliqueRlp, err := rlp.EncodeToBytes(cliqueHeader)
- if err != nil {
- utils.Fatalf("Should not error: %v", err)
- }
- addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899")
- _, err = api.SignData(ctx, accounts.MimetypeClique, *addr, hexutil.Encode(cliqueRlp))
- expectApprove("signdata - clique header", err)
- }
- { // Sign data test - typed data
- api.UI.ShowInfo("Please approve the next request for signing EIP-712 typed data")
- time.Sleep(delay)
- addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899")
- data := `{"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Person":[{"name":"name","type":"string"},{"name":"test","type":"uint8"},{"name":"wallet","type":"address"}],"Mail":[{"name":"from","type":"Person"},{"name":"to","type":"Person"},{"name":"contents","type":"string"}]},"primaryType":"Mail","domain":{"name":"Ether Mail","version":"1","chainId":"1","verifyingContract":"0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"},"message":{"from":{"name":"Cow","test":"3","wallet":"0xcD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"},"to":{"name":"Bob","wallet":"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB","test":"2"},"contents":"Hello, Bob!"}}`
- //_, err := api.SignData(ctx, accounts.MimetypeTypedData, *addr, hexutil.Encode([]byte(data)))
- var typedData core.TypedData
- json.Unmarshal([]byte(data), &typedData)
- _, err := api.SignTypedData(ctx, *addr, typedData)
- expectApprove("sign 712 typed data", err)
- }
- { // Sign data test - plain text
- api.UI.ShowInfo("Please approve the next request for signing text")
- time.Sleep(delay)
- addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899")
- _, err := api.SignData(ctx, accounts.MimetypeTextPlain, *addr, hexutil.Encode([]byte("hello world")))
- expectApprove("signdata - text", err)
- }
- { // Sign data test - plain text reject
- api.UI.ShowInfo("Please deny the next request for signing text")
- time.Sleep(delay)
- addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899")
- _, err := api.SignData(ctx, accounts.MimetypeTextPlain, *addr, hexutil.Encode([]byte("hello world")))
- expectDeny("signdata - text", err)
- }
- { // Sign transaction
- api.UI.ShowInfo("Please reject next transaction")
- time.Sleep(delay)
- data := hexutil.Bytes([]byte{})
- to := common.NewMixedcaseAddress(a)
- tx := core.SendTxArgs{
- Data: &data,
- Nonce: 0x1,
- Value: hexutil.Big(*big.NewInt(6)),
- From: common.NewMixedcaseAddress(a),
- To: &to,
- GasPrice: hexutil.Big(*big.NewInt(5)),
- Gas: 1000,
- Input: nil,
- }
- _, err := api.SignTransaction(ctx, tx, nil)
- expectDeny("signtransaction [1]", err)
- expectResponse("signtransaction [2]", "Did you see any warnings for the last transaction? (yes/no)", "no")
- }
- { // Listing
- api.UI.ShowInfo("Please reject listing-request")
- time.Sleep(delay)
- _, err := api.List(ctx)
- expectDeny("list", err)
- }
- { // Import
- api.UI.ShowInfo("Please reject new account-request")
- time.Sleep(delay)
- _, err := api.New(ctx)
- expectDeny("newaccount", err)
- }
- { // Metadata
- api.UI.ShowInfo("Please check if you see the Origin in next listing (approve or deny)")
- time.Sleep(delay)
- api.List(context.WithValue(ctx, "Origin", "origin.com"))
- expectResponse("metadata - origin", "Did you see origin (origin.com)? [yes/no] ", "yes")
- }
- for _, e := range errs {
- log.Error(e)
- }
- result := fmt.Sprintf("Tests completed. %d errors:\n%s\n", len(errs), strings.Join(errs, "\n"))
- api.UI.ShowInfo(result)
- }
- // getPassPhrase retrieves the password associated with clef, either fetched
- // from a list of preloaded passphrases, or requested interactively from the user.
- // TODO: there are many `getPassPhrase` functions, it will be better to abstract them into one.
- func getPassPhrase(query string, confirmation bool) string {
- fmt.Println(query)
- password, err := prompt.Stdin.PromptPassword("Password: ")
- if err != nil {
- utils.Fatalf("Failed to read password: %v", err)
- }
- if confirmation {
- confirm, err := prompt.Stdin.PromptPassword("Repeat password: ")
- if err != nil {
- utils.Fatalf("Failed to read password confirmation: %v", err)
- }
- if password != confirm {
- utils.Fatalf("Passwords do not match")
- }
- }
- return password
- }
- type encryptedSeedStorage struct {
- Description string `json:"description"`
- Version int `json:"version"`
- Params keystore.CryptoJSON `json:"params"`
- }
- // encryptSeed uses a similar scheme as the keystore uses, but with a different wrapping,
- // to encrypt the master seed
- func encryptSeed(seed []byte, auth []byte, scryptN, scryptP int) ([]byte, error) {
- cryptoStruct, err := keystore.EncryptDataV3(seed, auth, scryptN, scryptP)
- if err != nil {
- return nil, err
- }
- return json.Marshal(&encryptedSeedStorage{"Clef seed", 1, cryptoStruct})
- }
- // decryptSeed decrypts the master seed
- func decryptSeed(keyjson []byte, auth string) ([]byte, error) {
- var encSeed encryptedSeedStorage
- if err := json.Unmarshal(keyjson, &encSeed); err != nil {
- return nil, err
- }
- if encSeed.Version != 1 {
- log.Warn(fmt.Sprintf("unsupported encryption format of seed: %d, operation will likely fail", encSeed.Version))
- }
- seed, err := keystore.DecryptDataV3(encSeed.Params, auth)
- if err != nil {
- return nil, err
- }
- return seed, err
- }
- // GenDoc outputs examples of all structures used in json-rpc communication
- func GenDoc(ctx *cli.Context) {
- var (
- a = common.HexToAddress("0xdeadbeef000000000000000000000000deadbeef")
- b = common.HexToAddress("0x1111111122222222222233333333334444444444")
- meta = core.Metadata{
- Scheme: "http",
- Local: "localhost:8545",
- Origin: "www.malicious.ru",
- Remote: "localhost:9999",
- UserAgent: "Firefox 3.2",
- }
- output []string
- add = func(name, desc string, v interface{}) {
- if data, err := json.MarshalIndent(v, "", " "); err == nil {
- output = append(output, fmt.Sprintf("### %s\n\n%s\n\nExample:\n```json\n%s\n```", name, desc, data))
- } else {
- log.Error("Error generating output", "err", err)
- }
- }
- )
- { // Sign plain text request
- desc := "SignDataRequest contains information about a pending request to sign some data. " +
- "The data to be signed can be of various types, defined by content-type. Clef has done most " +
- "of the work in canonicalizing and making sense of the data, and it's up to the UI to present" +
- "the user with the contents of the `message`"
- sighash, msg := accounts.TextAndHash([]byte("hello world"))
- messages := []*core.NameValueType{{Name: "message", Value: msg, Typ: accounts.MimetypeTextPlain}}
- add("SignDataRequest", desc, &core.SignDataRequest{
- Address: common.NewMixedcaseAddress(a),
- Meta: meta,
- ContentType: accounts.MimetypeTextPlain,
- Rawdata: []byte(msg),
- Messages: messages,
- Hash: sighash})
- }
- { // Sign plain text response
- add("SignDataResponse - approve", "Response to SignDataRequest",
- &core.SignDataResponse{Approved: true})
- add("SignDataResponse - deny", "Response to SignDataRequest",
- &core.SignDataResponse{})
- }
- { // Sign transaction request
- desc := "SignTxRequest contains information about a pending request to sign a transaction. " +
- "Aside from the transaction itself, there is also a `call_info`-struct. That struct contains " +
- "messages of various types, that the user should be informed of." +
- "\n\n" +
- "As in any request, it's important to consider that the `meta` info also contains untrusted data." +
- "\n\n" +
- "The `transaction` (on input into clef) can have either `data` or `input` -- if both are set, " +
- "they must be identical, otherwise an error is generated. " +
- "However, Clef will always use `data` when passing this struct on (if Clef does otherwise, please file a ticket)"
- data := hexutil.Bytes([]byte{0x01, 0x02, 0x03, 0x04})
- add("SignTxRequest", desc, &core.SignTxRequest{
- Meta: meta,
- Callinfo: []core.ValidationInfo{
- {Typ: "Warning", Message: "Something looks odd, show this message as a warning"},
- {Typ: "Info", Message: "User should see this as well"},
- },
- Transaction: core.SendTxArgs{
- Data: &data,
- Nonce: 0x1,
- Value: hexutil.Big(*big.NewInt(6)),
- From: common.NewMixedcaseAddress(a),
- To: nil,
- GasPrice: hexutil.Big(*big.NewInt(5)),
- Gas: 1000,
- Input: nil,
- }})
- }
- { // Sign tx response
- data := hexutil.Bytes([]byte{0x04, 0x03, 0x02, 0x01})
- add("SignTxResponse - approve", "Response to request to sign a transaction. This response needs to contain the `transaction`"+
- ", because the UI is free to make modifications to the transaction.",
- &core.SignTxResponse{Approved: true,
- Transaction: core.SendTxArgs{
- Data: &data,
- Nonce: 0x4,
- Value: hexutil.Big(*big.NewInt(6)),
- From: common.NewMixedcaseAddress(a),
- To: nil,
- GasPrice: hexutil.Big(*big.NewInt(5)),
- Gas: 1000,
- Input: nil,
- }})
- add("SignTxResponse - deny", "Response to SignTxRequest. When denying a request, there's no need to "+
- "provide the transaction in return",
- &core.SignTxResponse{})
- }
- { // WHen a signed tx is ready to go out
- desc := "SignTransactionResult is used in the call `clef` -> `OnApprovedTx(result)`" +
- "\n\n" +
- "This occurs _after_ successful completion of the entire signing procedure, but right before the signed " +
- "transaction is passed to the external caller. This method (and data) can be used by the UI to signal " +
- "to the user that the transaction was signed, but it is primarily useful for ruleset implementations." +
- "\n\n" +
- "A ruleset that implements a rate limitation needs to know what transactions are sent out to the external " +
- "interface. By hooking into this methods, the ruleset can maintain track of that count." +
- "\n\n" +
- "**OBS:** Note that if an attacker can restore your `clef` data to a previous point in time" +
- " (e.g through a backup), the attacker can reset such windows, even if he/she is unable to decrypt the content. " +
- "\n\n" +
- "The `OnApproved` method cannot be responded to, it's purely informative"
- rlpdata := common.FromHex("0xf85d640101948a8eafb1cf62bfbeb1741769dae1a9dd47996192018026a0716bd90515acb1e68e5ac5867aa11a1e65399c3349d479f5fb698554ebc6f293a04e8a4ebfff434e971e0ef12c5bf3a881b06fd04fc3f8b8a7291fb67a26a1d4ed")
- var tx types.Transaction
- rlp.DecodeBytes(rlpdata, &tx)
- add("OnApproved - SignTransactionResult", desc, ðapi.SignTransactionResult{Raw: rlpdata, Tx: &tx})
- }
- { // User input
- add("UserInputRequest", "Sent when clef needs the user to provide data. If 'password' is true, the input field should be treated accordingly (echo-free)",
- &core.UserInputRequest{IsPassword: true, Title: "The title here", Prompt: "The question to ask the user"})
- add("UserInputResponse", "Response to UserInputRequest",
- &core.UserInputResponse{Text: "The textual response from user"})
- }
- { // List request
- add("ListRequest", "Sent when a request has been made to list addresses. The UI is provided with the "+
- "full `account`s, including local directory names. Note: this information is not passed back to the external caller, "+
- "who only sees the `address`es. ",
- &core.ListRequest{
- Meta: meta,
- Accounts: []accounts.Account{
- {Address: a, URL: accounts.URL{Scheme: "keystore", Path: "/path/to/keyfile/a"}},
- {Address: b, URL: accounts.URL{Scheme: "keystore", Path: "/path/to/keyfile/b"}}},
- })
- add("ListResponse", "Response to list request. The response contains a list of all addresses to show to the caller. "+
- "Note: the UI is free to respond with any address the caller, regardless of whether it exists or not",
- &core.ListResponse{
- Accounts: []accounts.Account{
- {
- Address: common.HexToAddress("0xcowbeef000000cowbeef00000000000000000c0w"),
- URL: accounts.URL{Path: ".. ignored .."},
- },
- {
- Address: common.HexToAddress("0xffffffffffffffffffffffffffffffffffffffff"),
- },
- }})
- }
- fmt.Println(`## UI Client interface
- These data types are defined in the channel between clef and the UI`)
- for _, elem := range output {
- fmt.Println(elem)
- }
- }
|