github.com/ConsenSys/Quorum@v20.10.0+incompatible/cmd/clef/main.go (about) 1 // Copyright 2018 The go-ethereum Authors 2 // This file is part of go-ethereum. 3 // 4 // go-ethereum is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // go-ethereum is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. 16 17 package main 18 19 import ( 20 "bufio" 21 "context" 22 "crypto/rand" 23 "crypto/sha256" 24 "encoding/hex" 25 "encoding/json" 26 "fmt" 27 "io" 28 "io/ioutil" 29 "math/big" 30 "os" 31 "os/signal" 32 "os/user" 33 "path/filepath" 34 "runtime" 35 "strings" 36 "syscall" 37 "time" 38 39 "github.com/ethereum/go-ethereum/accounts" 40 "github.com/ethereum/go-ethereum/accounts/keystore" 41 "github.com/ethereum/go-ethereum/accounts/pluggable" 42 "github.com/ethereum/go-ethereum/cmd/utils" 43 "github.com/ethereum/go-ethereum/common" 44 "github.com/ethereum/go-ethereum/common/hexutil" 45 "github.com/ethereum/go-ethereum/console" 46 "github.com/ethereum/go-ethereum/core/types" 47 "github.com/ethereum/go-ethereum/crypto" 48 "github.com/ethereum/go-ethereum/internal/debug" 49 "github.com/ethereum/go-ethereum/internal/ethapi" 50 "github.com/ethereum/go-ethereum/log" 51 "github.com/ethereum/go-ethereum/node" 52 "github.com/ethereum/go-ethereum/params" 53 "github.com/ethereum/go-ethereum/plugin" 54 "github.com/ethereum/go-ethereum/plugin/account" 55 "github.com/ethereum/go-ethereum/rlp" 56 "github.com/ethereum/go-ethereum/rpc" 57 "github.com/ethereum/go-ethereum/signer/core" 58 "github.com/ethereum/go-ethereum/signer/fourbyte" 59 "github.com/ethereum/go-ethereum/signer/rules" 60 "github.com/ethereum/go-ethereum/signer/storage" 61 colorable "github.com/mattn/go-colorable" 62 "github.com/mattn/go-isatty" 63 "gopkg.in/urfave/cli.v1" 64 ) 65 66 const legalWarning = ` 67 WARNING! 68 69 Clef is an account management tool. It may, like any software, contain bugs. 70 71 Please take care to 72 - backup your keystore files, 73 - verify that the keystore(s) can be opened with your password. 74 75 Clef is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 76 without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 77 PURPOSE. See the GNU General Public License for more details. 78 ` 79 80 var ( 81 logLevelFlag = cli.IntFlag{ 82 Name: "loglevel", 83 Value: 4, 84 Usage: "log level to emit to the screen", 85 } 86 advancedMode = cli.BoolFlag{ 87 Name: "advanced", 88 Usage: "If enabled, issues warnings instead of rejections for suspicious requests. Default off", 89 } 90 keystoreFlag = cli.StringFlag{ 91 Name: "keystore", 92 Value: filepath.Join(node.DefaultDataDir(), "keystore"), 93 Usage: "Directory for the keystore", 94 } 95 configdirFlag = cli.StringFlag{ 96 Name: "configdir", 97 Value: DefaultConfigDir(), 98 Usage: "Directory for Clef configuration", 99 } 100 chainIdFlag = cli.Int64Flag{ 101 Name: "chainid", 102 Value: params.MainnetChainConfig.ChainID.Int64(), 103 Usage: "Chain id to use for signing (1=mainnet, 3=Ropsten, 4=Rinkeby, 5=Goerli)", 104 } 105 rpcPortFlag = cli.IntFlag{ 106 Name: "rpcport", 107 Usage: "HTTP-RPC server listening port", 108 Value: node.DefaultHTTPPort + 5, 109 } 110 signerSecretFlag = cli.StringFlag{ 111 Name: "signersecret", 112 Usage: "A file containing the (encrypted) master seed to encrypt Clef data, e.g. keystore credentials and ruleset hash", 113 } 114 customDBFlag = cli.StringFlag{ 115 Name: "4bytedb-custom", 116 Usage: "File used for writing new 4byte-identifiers submitted via API", 117 Value: "./4byte-custom.json", 118 } 119 auditLogFlag = cli.StringFlag{ 120 Name: "auditlog", 121 Usage: "File used to emit audit logs. Set to \"\" to disable", 122 Value: "audit.log", 123 } 124 ruleFlag = cli.StringFlag{ 125 Name: "rules", 126 Usage: "Path to the rule file to auto-authorize requests with", 127 } 128 stdiouiFlag = cli.BoolFlag{ 129 Name: "stdio-ui", 130 Usage: "Use STDIN/STDOUT as a channel for an external UI. " + 131 "This means that an STDIN/STDOUT is used for RPC-communication with a e.g. a graphical user " + 132 "interface, and can be used when Clef is started by an external process.", 133 } 134 testFlag = cli.BoolFlag{ 135 Name: "stdio-ui-test", 136 Usage: "Mechanism to test interface between Clef and UI. Requires 'stdio-ui'.", 137 } 138 app = cli.NewApp() 139 initCommand = cli.Command{ 140 Action: utils.MigrateFlags(initializeSecrets), 141 Name: "init", 142 Usage: "Initialize the signer, generate secret storage", 143 ArgsUsage: "", 144 Flags: []cli.Flag{ 145 logLevelFlag, 146 configdirFlag, 147 }, 148 Description: ` 149 The init command generates a master seed which Clef can use to store credentials and data needed for 150 the rule-engine to work.`, 151 } 152 attestCommand = cli.Command{ 153 Action: utils.MigrateFlags(attestFile), 154 Name: "attest", 155 Usage: "Attest that a js-file is to be used", 156 ArgsUsage: "<sha256sum>", 157 Flags: []cli.Flag{ 158 logLevelFlag, 159 configdirFlag, 160 signerSecretFlag, 161 }, 162 Description: ` 163 The attest command stores the sha256 of the rule.js-file that you want to use for automatic processing of 164 incoming requests. 165 166 Whenever you make an edit to the rule file, you need to use attestation to tell 167 Clef that the file is 'safe' to execute.`, 168 } 169 setCredentialCommand = cli.Command{ 170 Action: utils.MigrateFlags(setCredential), 171 Name: "setpw", 172 Usage: "Store a credential for a keystore file", 173 ArgsUsage: "<address>", 174 Flags: []cli.Flag{ 175 logLevelFlag, 176 configdirFlag, 177 signerSecretFlag, 178 }, 179 Description: ` 180 The setpw command stores a password for a given address (keyfile). 181 `} 182 delCredentialCommand = cli.Command{ 183 Action: utils.MigrateFlags(removeCredential), 184 Name: "delpw", 185 Usage: "Remove a credential for a keystore file", 186 ArgsUsage: "<address>", 187 Flags: []cli.Flag{ 188 logLevelFlag, 189 configdirFlag, 190 signerSecretFlag, 191 }, 192 Description: ` 193 The delpw command removes a password for a given address (keyfile). 194 `} 195 gendocCommand = cli.Command{ 196 Action: GenDoc, 197 Name: "gendoc", 198 Usage: "Generate documentation about json-rpc format", 199 Description: ` 200 The gendoc generates example structures of the json-rpc communication types. 201 `} 202 ) 203 204 // <Quorum> 205 var supportedPlugins = []plugin.PluginInterfaceName{plugin.AccountPluginInterfaceName} 206 207 // </Quorum> 208 209 func init() { 210 app.Name = "Clef" 211 app.Usage = "Manage Ethereum account operations" 212 app.Flags = []cli.Flag{ 213 logLevelFlag, 214 keystoreFlag, 215 configdirFlag, 216 chainIdFlag, 217 utils.LightKDFFlag, 218 utils.NoUSBFlag, 219 utils.SmartCardDaemonPathFlag, 220 utils.RPCListenAddrFlag, 221 utils.RPCVirtualHostsFlag, 222 utils.IPCDisabledFlag, 223 utils.IPCPathFlag, 224 utils.RPCEnabledFlag, 225 rpcPortFlag, 226 signerSecretFlag, 227 customDBFlag, 228 auditLogFlag, 229 ruleFlag, 230 stdiouiFlag, 231 testFlag, 232 advancedMode, 233 // <Quorum> 234 utils.PluginSettingsFlag, 235 utils.PluginLocalVerifyFlag, 236 utils.PluginPublicKeyFlag, 237 utils.PluginSkipVerifyFlag, 238 // </Quorum> 239 } 240 app.Action = signer 241 app.Commands = []cli.Command{initCommand, attestCommand, setCredentialCommand, delCredentialCommand, gendocCommand} 242 } 243 244 func main() { 245 if err := app.Run(os.Args); err != nil { 246 fmt.Fprintln(os.Stderr, err) 247 os.Exit(1) 248 } 249 } 250 251 func initializeSecrets(c *cli.Context) error { 252 // Get past the legal message 253 if err := initialize(c); err != nil { 254 return err 255 } 256 // Ensure the master key does not yet exist, we're not willing to overwrite 257 configDir := c.GlobalString(configdirFlag.Name) 258 if err := os.Mkdir(configDir, 0700); err != nil && !os.IsExist(err) { 259 return err 260 } 261 location := filepath.Join(configDir, "masterseed.json") 262 if _, err := os.Stat(location); err == nil { 263 return fmt.Errorf("master key %v already exists, will not overwrite", location) 264 } 265 // Key file does not exist yet, generate a new one and encrypt it 266 masterSeed := make([]byte, 256) 267 num, err := io.ReadFull(rand.Reader, masterSeed) 268 if err != nil { 269 return err 270 } 271 if num != len(masterSeed) { 272 return fmt.Errorf("failed to read enough random") 273 } 274 n, p := keystore.StandardScryptN, keystore.StandardScryptP 275 if c.GlobalBool(utils.LightKDFFlag.Name) { 276 n, p = keystore.LightScryptN, keystore.LightScryptP 277 } 278 text := "The master seed of clef will be locked with a password.\nPlease specify a password. Do not forget this password!" 279 var password string 280 for { 281 password = getPassPhrase(text, true) 282 if err := core.ValidatePasswordFormat(password); err != nil { 283 fmt.Printf("invalid password: %v\n", err) 284 } else { 285 fmt.Println() 286 break 287 } 288 } 289 cipherSeed, err := encryptSeed(masterSeed, []byte(password), n, p) 290 if err != nil { 291 return fmt.Errorf("failed to encrypt master seed: %v", err) 292 } 293 // Double check the master key path to ensure nothing wrote there in between 294 if err = os.Mkdir(configDir, 0700); err != nil && !os.IsExist(err) { 295 return err 296 } 297 if _, err := os.Stat(location); err == nil { 298 return fmt.Errorf("master key %v already exists, will not overwrite", location) 299 } 300 // Write the file and print the usual warning message 301 if err = ioutil.WriteFile(location, cipherSeed, 0400); err != nil { 302 return err 303 } 304 fmt.Printf("A master seed has been generated into %s\n", location) 305 fmt.Printf(` 306 This is required to be able to store credentials, such as: 307 * Passwords for keystores (used by rule engine) 308 * Storage for JavaScript auto-signing rules 309 * Hash of JavaScript rule-file 310 311 You should treat 'masterseed.json' with utmost secrecy and make a backup of it! 312 * The password is necessary but not enough, you need to back up the master seed too! 313 * The master seed does not contain your accounts, those need to be backed up separately! 314 315 `) 316 return nil 317 } 318 func attestFile(ctx *cli.Context) error { 319 if len(ctx.Args()) < 1 { 320 utils.Fatalf("This command requires an argument.") 321 } 322 if err := initialize(ctx); err != nil { 323 return err 324 } 325 326 stretchedKey, err := readMasterKey(ctx, nil) 327 if err != nil { 328 utils.Fatalf(err.Error()) 329 } 330 configDir := ctx.GlobalString(configdirFlag.Name) 331 vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10])) 332 confKey := crypto.Keccak256([]byte("config"), stretchedKey) 333 334 // Initialize the encrypted storages 335 configStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "config.json"), confKey) 336 val := ctx.Args().First() 337 configStorage.Put("ruleset_sha256", val) 338 log.Info("Ruleset attestation updated", "sha256", val) 339 return nil 340 } 341 342 func setCredential(ctx *cli.Context) error { 343 if len(ctx.Args()) < 1 { 344 utils.Fatalf("This command requires an address to be passed as an argument") 345 } 346 if err := initialize(ctx); err != nil { 347 return err 348 } 349 addr := ctx.Args().First() 350 if !common.IsHexAddress(addr) { 351 utils.Fatalf("Invalid address specified: %s", addr) 352 } 353 address := common.HexToAddress(addr) 354 password := getPassPhrase("Please enter a password to store for this address:", true) 355 fmt.Println() 356 357 stretchedKey, err := readMasterKey(ctx, nil) 358 if err != nil { 359 utils.Fatalf(err.Error()) 360 } 361 configDir := ctx.GlobalString(configdirFlag.Name) 362 vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10])) 363 pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey) 364 365 pwStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey) 366 pwStorage.Put(address.Hex(), password) 367 368 log.Info("Credential store updated", "set", address) 369 return nil 370 } 371 372 func removeCredential(ctx *cli.Context) error { 373 if len(ctx.Args()) < 1 { 374 utils.Fatalf("This command requires an address to be passed as an argument") 375 } 376 if err := initialize(ctx); err != nil { 377 return err 378 } 379 addr := ctx.Args().First() 380 if !common.IsHexAddress(addr) { 381 utils.Fatalf("Invalid address specified: %s", addr) 382 } 383 address := common.HexToAddress(addr) 384 385 stretchedKey, err := readMasterKey(ctx, nil) 386 if err != nil { 387 utils.Fatalf(err.Error()) 388 } 389 configDir := ctx.GlobalString(configdirFlag.Name) 390 vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10])) 391 pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey) 392 393 pwStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey) 394 pwStorage.Del(address.Hex()) 395 396 log.Info("Credential store updated", "unset", address) 397 return nil 398 } 399 400 func initialize(c *cli.Context) error { 401 // Set up the logger to print everything 402 logOutput := os.Stdout 403 if c.GlobalBool(stdiouiFlag.Name) { 404 logOutput = os.Stderr 405 // If using the stdioui, we can't do the 'confirm'-flow 406 fmt.Fprintf(logOutput, legalWarning) 407 } else { 408 if !confirm(legalWarning) { 409 return fmt.Errorf("aborted by user") 410 } 411 fmt.Println() 412 } 413 usecolor := (isatty.IsTerminal(os.Stderr.Fd()) || isatty.IsCygwinTerminal(os.Stderr.Fd())) && os.Getenv("TERM") != "dumb" 414 output := io.Writer(logOutput) 415 if usecolor { 416 output = colorable.NewColorable(logOutput) 417 } 418 log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(c.Int(logLevelFlag.Name)), log.StreamHandler(output, log.TerminalFormat(usecolor)))) 419 420 return nil 421 } 422 423 // ipcEndpoint resolves an IPC endpoint based on a configured value, taking into 424 // account the set data folders as well as the designated platform we're currently 425 // running on. 426 func ipcEndpoint(ipcPath, datadir string) string { 427 // On windows we can only use plain top-level pipes 428 if runtime.GOOS == "windows" { 429 if strings.HasPrefix(ipcPath, `\\.\pipe\`) { 430 return ipcPath 431 } 432 return `\\.\pipe\` + ipcPath 433 } 434 // Resolve names into the data directory full paths otherwise 435 if filepath.Base(ipcPath) == ipcPath { 436 if datadir == "" { 437 return filepath.Join(os.TempDir(), ipcPath) 438 } 439 return filepath.Join(datadir, ipcPath) 440 } 441 return ipcPath 442 } 443 444 func signer(c *cli.Context) error { 445 // If we have some unrecognized command, bail out 446 if args := c.Args(); len(args) > 0 { 447 return fmt.Errorf("invalid command: %q", args[0]) 448 } 449 if err := initialize(c); err != nil { 450 return err 451 } 452 var ( 453 ui core.UIClientAPI 454 ) 455 if c.GlobalBool(stdiouiFlag.Name) { 456 log.Info("Using stdin/stdout as UI-channel") 457 ui = core.NewStdIOUI() 458 } else { 459 log.Info("Using CLI as UI-channel") 460 ui = core.NewCommandlineUI() 461 } 462 // 4bytedb data 463 fourByteLocal := c.GlobalString(customDBFlag.Name) 464 db, err := fourbyte.NewWithFile(fourByteLocal) 465 if err != nil { 466 utils.Fatalf(err.Error()) 467 } 468 embeds, locals := db.Size() 469 log.Info("Loaded 4byte database", "embeds", embeds, "locals", locals, "local", fourByteLocal) 470 471 var ( 472 api core.ExternalAPI 473 pwStorage storage.Storage = &storage.NoStorage{} 474 ) 475 476 configDir := c.GlobalString(configdirFlag.Name) 477 if stretchedKey, err := readMasterKey(c, ui); err != nil { 478 log.Warn("Failed to open master, rules disabled", "err", err) 479 } else { 480 vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10])) 481 482 // Generate domain specific keys 483 pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey) 484 jskey := crypto.Keccak256([]byte("jsstorage"), stretchedKey) 485 confkey := crypto.Keccak256([]byte("config"), stretchedKey) 486 487 // Initialize the encrypted storages 488 pwStorage = storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey) 489 jsStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "jsstorage.json"), jskey) 490 configStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "config.json"), confkey) 491 492 // Do we have a rule-file? 493 if ruleFile := c.GlobalString(ruleFlag.Name); ruleFile != "" { 494 ruleJS, err := ioutil.ReadFile(ruleFile) 495 if err != nil { 496 log.Warn("Could not load rules, disabling", "file", ruleFile, "err", err) 497 } else { 498 shasum := sha256.Sum256(ruleJS) 499 foundShaSum := hex.EncodeToString(shasum[:]) 500 storedShasum, _ := configStorage.Get("ruleset_sha256") 501 if storedShasum != foundShaSum { 502 log.Warn("Rule hash not attested, disabling", "hash", foundShaSum, "attested", storedShasum) 503 } else { 504 // Initialize rules 505 ruleEngine, err := rules.NewRuleEvaluator(ui, jsStorage) 506 if err != nil { 507 utils.Fatalf(err.Error()) 508 } 509 ruleEngine.Init(string(ruleJS)) 510 ui = ruleEngine 511 log.Info("Rule engine configured", "file", c.String(ruleFlag.Name)) 512 } 513 } 514 } 515 } 516 var ( 517 chainId = c.GlobalInt64(chainIdFlag.Name) 518 ksLoc = c.GlobalString(keystoreFlag.Name) 519 lightKdf = c.GlobalBool(utils.LightKDFFlag.Name) 520 advanced = c.GlobalBool(advancedMode.Name) 521 nousb = c.GlobalBool(utils.NoUSBFlag.Name) 522 scpath = c.GlobalString(utils.SmartCardDaemonPathFlag.Name) 523 ) 524 log.Info("Starting signer", "chainid", chainId, "keystore", ksLoc, 525 "light-kdf", lightKdf, "advanced", advanced) 526 527 // <Quorum> start the plugin manager 528 var ( 529 pm *plugin.PluginManager 530 pluginConf *plugin.Settings 531 ) 532 if c.IsSet(utils.PluginSettingsFlag.Name) { 533 log.Info("Using plugins") 534 pm, pluginConf, err = startPluginManager(c) 535 if err != nil { 536 utils.Fatalf(err.Error()) 537 } 538 539 // setup goroutine to stop plugin manager when clef stops 540 go func() { 541 sigc := make(chan os.Signal, 1) 542 signal.Notify(sigc, syscall.SIGINT, syscall.SIGTERM) 543 defer signal.Stop(sigc) 544 <-sigc 545 log.Info("Got interrupt, shutting down...") 546 go pm.Stop() 547 for i := 10; i > 0; i-- { 548 <-sigc 549 if i > 1 { 550 log.Warn("Already shutting down, interrupt more to panic.", "times", i-1) 551 } 552 } 553 debug.Exit() // ensure trace and CPU profile data is flushed. 554 debug.LoudPanic("boom") 555 }() 556 } 557 // </Quorum> 558 559 am := core.StartClefAccountManager(ksLoc, nousb, lightKdf, pluginConf, scpath) 560 561 // <Quorum> setup the pluggable accounts backend with the plugin 562 if pm != nil && pm.IsEnabled(plugin.AccountPluginInterfaceName) { 563 b := am.Backends(pluggable.BackendType)[0].(*pluggable.Backend) 564 if err := pm.AddAccountPluginToBackend(b); err != nil { 565 return err 566 } 567 } 568 // </Quorum> 569 570 apiImpl := core.NewSignerAPI(am, chainId, nousb, ui, db, advanced, pwStorage) 571 572 // Establish the bidirectional communication, by creating a new UI backend and registering 573 // it with the UI. 574 ui.RegisterUIServer(core.NewUIServerAPI(apiImpl)) 575 api = apiImpl 576 // Audit logging 577 if logfile := c.GlobalString(auditLogFlag.Name); logfile != "" { 578 api, err = core.NewAuditLogger(logfile, api) 579 if err != nil { 580 utils.Fatalf(err.Error()) 581 } 582 log.Info("Audit logs configured", "file", logfile) 583 } 584 // register signer API with server 585 var ( 586 extapiURL = "n/a" 587 ipcapiURL = "n/a" 588 ) 589 rpcAPI := []rpc.API{ 590 { 591 Namespace: "account", 592 Public: true, 593 Service: api, 594 Version: "1.0"}, 595 } 596 597 // <Quorum> 598 if pm != nil { 599 rpcAPI = addPluginAPIs(pm, rpcAPI, ui) 600 } 601 // </Quorum> 602 603 if c.GlobalBool(utils.RPCEnabledFlag.Name) { 604 vhosts := splitAndTrim(c.GlobalString(utils.RPCVirtualHostsFlag.Name)) 605 cors := splitAndTrim(c.GlobalString(utils.RPCCORSDomainFlag.Name)) 606 607 // start http server 608 httpEndpoint := fmt.Sprintf("%s:%d", c.GlobalString(utils.RPCListenAddrFlag.Name), c.Int(rpcPortFlag.Name)) 609 listener, _, _, err := rpc.StartHTTPEndpoint(httpEndpoint, rpcAPI, []string{"account", "plugin@account"}, cors, vhosts, rpc.DefaultHTTPTimeouts, nil, nil) 610 if err != nil { 611 utils.Fatalf("Could not start RPC api: %v", err) 612 } 613 extapiURL = fmt.Sprintf("http://%s", httpEndpoint) 614 log.Info("HTTP endpoint opened", "url", extapiURL) 615 616 defer func() { 617 listener.Close() 618 log.Info("HTTP endpoint closed", "url", httpEndpoint) 619 }() 620 } 621 if !c.GlobalBool(utils.IPCDisabledFlag.Name) { 622 givenPath := c.GlobalString(utils.IPCPathFlag.Name) 623 ipcapiURL = ipcEndpoint(filepath.Join(givenPath, "clef.ipc"), configDir) 624 listener, _, err := rpc.StartIPCEndpoint(ipcapiURL, rpcAPI) 625 if err != nil { 626 utils.Fatalf("Could not start IPC api: %v", err) 627 } 628 log.Info("IPC endpoint opened", "url", ipcapiURL) 629 defer func() { 630 listener.Close() 631 log.Info("IPC endpoint closed", "url", ipcapiURL) 632 }() 633 } 634 635 if c.GlobalBool(testFlag.Name) { 636 log.Info("Performing UI test") 637 go testExternalUI(apiImpl) 638 } 639 ui.OnSignerStartup(core.StartupInfo{ 640 Info: map[string]interface{}{ 641 "intapi_version": core.InternalAPIVersion, 642 "extapi_version": core.ExternalAPIVersion, 643 "extapi_http": extapiURL, 644 "extapi_ipc": ipcapiURL, 645 }, 646 }) 647 648 abortChan := make(chan os.Signal) 649 signal.Notify(abortChan, os.Interrupt) 650 651 sig := <-abortChan 652 log.Info("Exiting...", "signal", sig) 653 654 return nil 655 } 656 657 // <Quorum/> 658 // startPluginManager gets plugin config and starts a new PluginManager 659 func startPluginManager(c *cli.Context) (*plugin.PluginManager, *plugin.Settings, error) { 660 nodeConf := new(node.Config) 661 if err := utils.SetPlugins(c, nodeConf); err != nil { 662 return nil, nil, err 663 } 664 pluginConf := nodeConf.Plugins 665 666 if err := pluginConf.CheckSettingsAreSupported(supportedPlugins); err != nil { 667 return nil, nil, err 668 } 669 670 pm, err := plugin.NewPluginManager("clef", pluginConf, c.Bool(utils.PluginSkipVerifyFlag.Name), c.Bool(utils.PluginLocalVerifyFlag.Name), c.String(utils.PluginPublicKeyFlag.Name)) 671 if err != nil { 672 return nil, nil, err 673 } 674 if err := pm.Start(nil); err != nil { 675 return nil, nil, err 676 } 677 return pm, pluginConf, nil 678 } 679 680 // addPluginAPIs adds the exposed plugin APIs to Clef's API. 681 // It alters some of the plugin APIs so that calls to them will require UI approval. 682 func addPluginAPIs(pm *plugin.PluginManager, rpcAPI []rpc.API, ui core.UIClientAPI) []rpc.API { 683 // pm.APIs() returns a slice of values hence the following approach that may look clumsy 684 var ( 685 stdPluginAPIs = pm.APIs() 686 approvalPluginAPIs = make([]rpc.API, len(stdPluginAPIs)) 687 ) 688 689 for i, api := range stdPluginAPIs { 690 switch s := api.Service.(type) { 691 case account.CreatorService: 692 api.Service = core.NewApprovalCreatorService(s, ui) 693 } 694 approvalPluginAPIs[i] = api 695 } 696 return append(rpcAPI, approvalPluginAPIs...) 697 } 698 699 // splitAndTrim splits input separated by a comma 700 // and trims excessive white space from the substrings. 701 func splitAndTrim(input string) []string { 702 result := strings.Split(input, ",") 703 for i, r := range result { 704 result[i] = strings.TrimSpace(r) 705 } 706 return result 707 } 708 709 // DefaultConfigDir is the default config directory to use for the vaults and other 710 // persistence requirements. 711 func DefaultConfigDir() string { 712 // Try to place the data folder in the user's home dir 713 home := homeDir() 714 if home != "" { 715 if runtime.GOOS == "darwin" { 716 return filepath.Join(home, "Library", "Signer") 717 } else if runtime.GOOS == "windows" { 718 appdata := os.Getenv("APPDATA") 719 if appdata != "" { 720 return filepath.Join(appdata, "Signer") 721 } else { 722 return filepath.Join(home, "AppData", "Roaming", "Signer") 723 } 724 } else { 725 return filepath.Join(home, ".clef") 726 } 727 } 728 // As we cannot guess a stable location, return empty and handle later 729 return "" 730 } 731 732 func homeDir() string { 733 if home := os.Getenv("HOME"); home != "" { 734 return home 735 } 736 if usr, err := user.Current(); err == nil { 737 return usr.HomeDir 738 } 739 return "" 740 } 741 func readMasterKey(ctx *cli.Context, ui core.UIClientAPI) ([]byte, error) { 742 var ( 743 file string 744 configDir = ctx.GlobalString(configdirFlag.Name) 745 ) 746 if ctx.GlobalIsSet(signerSecretFlag.Name) { 747 file = ctx.GlobalString(signerSecretFlag.Name) 748 } else { 749 file = filepath.Join(configDir, "masterseed.json") 750 } 751 if err := checkFile(file); err != nil { 752 return nil, err 753 } 754 cipherKey, err := ioutil.ReadFile(file) 755 if err != nil { 756 return nil, err 757 } 758 var password string 759 // If ui is not nil, get the password from ui. 760 if ui != nil { 761 resp, err := ui.OnInputRequired(core.UserInputRequest{ 762 Title: "Master Password", 763 Prompt: "Please enter the password to decrypt the master seed", 764 IsPassword: true}) 765 if err != nil { 766 return nil, err 767 } 768 password = resp.Text 769 } else { 770 password = getPassPhrase("Decrypt master seed of clef", false) 771 } 772 masterSeed, err := decryptSeed(cipherKey, password) 773 if err != nil { 774 return nil, fmt.Errorf("failed to decrypt the master seed of clef") 775 } 776 if len(masterSeed) < 256 { 777 return nil, fmt.Errorf("master seed of insufficient length, expected >255 bytes, got %d", len(masterSeed)) 778 } 779 // Create vault location 780 vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), masterSeed)[:10])) 781 err = os.Mkdir(vaultLocation, 0700) 782 if err != nil && !os.IsExist(err) { 783 return nil, err 784 } 785 return masterSeed, nil 786 } 787 788 // checkFile is a convenience function to check if a file 789 // * exists 790 // * is mode 0400 791 func checkFile(filename string) error { 792 info, err := os.Stat(filename) 793 if err != nil { 794 return fmt.Errorf("failed stat on %s: %v", filename, err) 795 } 796 // Check the unix permission bits 797 if info.Mode().Perm()&0377 != 0 { 798 return fmt.Errorf("file (%v) has insecure file permissions (%v)", filename, info.Mode().String()) 799 } 800 return nil 801 } 802 803 // confirm displays a text and asks for user confirmation 804 func confirm(text string) bool { 805 fmt.Printf(text) 806 fmt.Printf("\nEnter 'ok' to proceed:\n> ") 807 808 text, err := bufio.NewReader(os.Stdin).ReadString('\n') 809 if err != nil { 810 log.Crit("Failed to read user input", "err", err) 811 } 812 if text := strings.TrimSpace(text); text == "ok" { 813 return true 814 } 815 return false 816 } 817 818 func testExternalUI(api *core.SignerAPI) { 819 820 ctx := context.WithValue(context.Background(), "remote", "clef binary") 821 ctx = context.WithValue(ctx, "scheme", "in-proc") 822 ctx = context.WithValue(ctx, "local", "main") 823 errs := make([]string, 0) 824 825 a := common.HexToAddress("0xdeadbeef000000000000000000000000deadbeef") 826 addErr := func(errStr string) { 827 log.Info("Test error", "err", errStr) 828 errs = append(errs, errStr) 829 } 830 831 queryUser := func(q string) string { 832 resp, err := api.UI.OnInputRequired(core.UserInputRequest{ 833 Title: "Testing", 834 Prompt: q, 835 }) 836 if err != nil { 837 addErr(err.Error()) 838 } 839 return resp.Text 840 } 841 expectResponse := func(testcase, question, expect string) { 842 if got := queryUser(question); got != expect { 843 addErr(fmt.Sprintf("%s: got %v, expected %v", testcase, got, expect)) 844 } 845 } 846 expectApprove := func(testcase string, err error) { 847 if err == nil || err == accounts.ErrUnknownAccount { 848 return 849 } 850 addErr(fmt.Sprintf("%v: expected no error, got %v", testcase, err.Error())) 851 } 852 expectDeny := func(testcase string, err error) { 853 if err == nil || err != core.ErrRequestDenied { 854 addErr(fmt.Sprintf("%v: expected ErrRequestDenied, got %v", testcase, err)) 855 } 856 } 857 var delay = 1 * time.Second 858 // Test display of info and error 859 { 860 api.UI.ShowInfo("If you see this message, enter 'yes' to next question") 861 time.Sleep(delay) 862 expectResponse("showinfo", "Did you see the message? [yes/no]", "yes") 863 api.UI.ShowError("If you see this message, enter 'yes' to the next question") 864 time.Sleep(delay) 865 expectResponse("showerror", "Did you see the message? [yes/no]", "yes") 866 } 867 { // Sign data test - clique header 868 api.UI.ShowInfo("Please approve the next request for signing a clique header") 869 time.Sleep(delay) 870 cliqueHeader := types.Header{ 871 common.HexToHash("0000H45H"), 872 common.HexToHash("0000H45H"), 873 common.HexToAddress("0000H45H"), 874 common.HexToHash("0000H00H"), 875 common.HexToHash("0000H45H"), 876 common.HexToHash("0000H45H"), 877 types.Bloom{}, 878 big.NewInt(1337), 879 big.NewInt(1337), 880 1338, 881 1338, 882 1338, 883 []byte("Extra data Extra data Extra data Extra data Extra data Extra data Extra data Extra data"), 884 common.HexToHash("0x0000H45H"), 885 types.BlockNonce{}, 886 } 887 cliqueRlp, err := rlp.EncodeToBytes(cliqueHeader) 888 if err != nil { 889 utils.Fatalf("Should not error: %v", err) 890 } 891 addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899") 892 _, err = api.SignData(ctx, accounts.MimetypeClique, *addr, hexutil.Encode(cliqueRlp)) 893 expectApprove("signdata - clique header", err) 894 } 895 { // Sign data test - typed data 896 api.UI.ShowInfo("Please approve the next request for signing EIP-712 typed data") 897 time.Sleep(delay) 898 addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899") 899 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!"}}` 900 //_, err := api.SignData(ctx, accounts.MimetypeTypedData, *addr, hexutil.Encode([]byte(data))) 901 var typedData core.TypedData 902 json.Unmarshal([]byte(data), &typedData) 903 _, err := api.SignTypedData(ctx, *addr, typedData) 904 expectApprove("sign 712 typed data", err) 905 } 906 { // Sign data test - plain text 907 api.UI.ShowInfo("Please approve the next request for signing text") 908 time.Sleep(delay) 909 addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899") 910 _, err := api.SignData(ctx, accounts.MimetypeTextPlain, *addr, hexutil.Encode([]byte("hello world"))) 911 expectApprove("signdata - text", err) 912 } 913 { // Sign data test - plain text reject 914 api.UI.ShowInfo("Please deny the next request for signing text") 915 time.Sleep(delay) 916 addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899") 917 _, err := api.SignData(ctx, accounts.MimetypeTextPlain, *addr, hexutil.Encode([]byte("hello world"))) 918 expectDeny("signdata - text", err) 919 } 920 { // Sign transaction 921 922 api.UI.ShowInfo("Please reject next transaction") 923 time.Sleep(delay) 924 data := hexutil.Bytes([]byte{}) 925 to := common.NewMixedcaseAddress(a) 926 tx := core.SendTxArgs{ 927 Data: &data, 928 Nonce: 0x1, 929 Value: hexutil.Big(*big.NewInt(6)), 930 From: common.NewMixedcaseAddress(a), 931 To: &to, 932 GasPrice: hexutil.Big(*big.NewInt(5)), 933 Gas: 1000, 934 Input: nil, 935 } 936 _, err := api.SignTransaction(ctx, tx, nil) 937 expectDeny("signtransaction [1]", err) 938 expectResponse("signtransaction [2]", "Did you see any warnings for the last transaction? (yes/no)", "no") 939 } 940 { // Listing 941 api.UI.ShowInfo("Please reject listing-request") 942 time.Sleep(delay) 943 _, err := api.List(ctx) 944 expectDeny("list", err) 945 } 946 { // Import 947 api.UI.ShowInfo("Please reject new account-request") 948 time.Sleep(delay) 949 _, err := api.New(ctx) 950 expectDeny("newaccount", err) 951 } 952 { // Metadata 953 api.UI.ShowInfo("Please check if you see the Origin in next listing (approve or deny)") 954 time.Sleep(delay) 955 api.List(context.WithValue(ctx, "Origin", "origin.com")) 956 expectResponse("metadata - origin", "Did you see origin (origin.com)? [yes/no] ", "yes") 957 } 958 959 for _, e := range errs { 960 log.Error(e) 961 } 962 result := fmt.Sprintf("Tests completed. %d errors:\n%s\n", len(errs), strings.Join(errs, "\n")) 963 api.UI.ShowInfo(result) 964 965 } 966 967 // getPassPhrase retrieves the password associated with clef, either fetched 968 // from a list of preloaded passphrases, or requested interactively from the user. 969 // TODO: there are many `getPassPhrase` functions, it will be better to abstract them into one. 970 func getPassPhrase(prompt string, confirmation bool) string { 971 fmt.Println(prompt) 972 password, err := console.Stdin.PromptPassword("Password: ") 973 if err != nil { 974 utils.Fatalf("Failed to read password: %v", err) 975 } 976 if confirmation { 977 confirm, err := console.Stdin.PromptPassword("Repeat password: ") 978 if err != nil { 979 utils.Fatalf("Failed to read password confirmation: %v", err) 980 } 981 if password != confirm { 982 utils.Fatalf("Passwords do not match") 983 } 984 } 985 return password 986 } 987 988 type encryptedSeedStorage struct { 989 Description string `json:"description"` 990 Version int `json:"version"` 991 Params keystore.CryptoJSON `json:"params"` 992 } 993 994 // encryptSeed uses a similar scheme as the keystore uses, but with a different wrapping, 995 // to encrypt the master seed 996 func encryptSeed(seed []byte, auth []byte, scryptN, scryptP int) ([]byte, error) { 997 cryptoStruct, err := keystore.EncryptDataV3(seed, auth, scryptN, scryptP) 998 if err != nil { 999 return nil, err 1000 } 1001 return json.Marshal(&encryptedSeedStorage{"Clef seed", 1, cryptoStruct}) 1002 } 1003 1004 // decryptSeed decrypts the master seed 1005 func decryptSeed(keyjson []byte, auth string) ([]byte, error) { 1006 var encSeed encryptedSeedStorage 1007 if err := json.Unmarshal(keyjson, &encSeed); err != nil { 1008 return nil, err 1009 } 1010 if encSeed.Version != 1 { 1011 log.Warn(fmt.Sprintf("unsupported encryption format of seed: %d, operation will likely fail", encSeed.Version)) 1012 } 1013 seed, err := keystore.DecryptDataV3(encSeed.Params, auth) 1014 if err != nil { 1015 return nil, err 1016 } 1017 return seed, err 1018 } 1019 1020 // GenDoc outputs examples of all structures used in json-rpc communication 1021 func GenDoc(ctx *cli.Context) { 1022 1023 var ( 1024 a = common.HexToAddress("0xdeadbeef000000000000000000000000deadbeef") 1025 b = common.HexToAddress("0x1111111122222222222233333333334444444444") 1026 meta = core.Metadata{ 1027 Scheme: "http", 1028 Local: "localhost:8545", 1029 Origin: "www.malicious.ru", 1030 Remote: "localhost:9999", 1031 UserAgent: "Firefox 3.2", 1032 } 1033 output []string 1034 add = func(name, desc string, v interface{}) { 1035 if data, err := json.MarshalIndent(v, "", " "); err == nil { 1036 output = append(output, fmt.Sprintf("### %s\n\n%s\n\nExample:\n```json\n%s\n```", name, desc, data)) 1037 } else { 1038 log.Error("Error generating output", "err", err) 1039 } 1040 } 1041 ) 1042 1043 { // Sign plain text request 1044 desc := "SignDataRequest contains information about a pending request to sign some data. " + 1045 "The data to be signed can be of various types, defined by content-type. Clef has done most " + 1046 "of the work in canonicalizing and making sense of the data, and it's up to the UI to present" + 1047 "the user with the contents of the `message`" 1048 sighash, msg := accounts.TextAndHash([]byte("hello world")) 1049 messages := []*core.NameValueType{{"message", msg, accounts.MimetypeTextPlain}} 1050 1051 add("SignDataRequest", desc, &core.SignDataRequest{ 1052 Address: common.NewMixedcaseAddress(a), 1053 Meta: meta, 1054 ContentType: accounts.MimetypeTextPlain, 1055 Rawdata: []byte(msg), 1056 Messages: messages, 1057 Hash: sighash}) 1058 } 1059 { // Sign plain text response 1060 add("SignDataResponse - approve", "Response to SignDataRequest", 1061 &core.SignDataResponse{Approved: true}) 1062 add("SignDataResponse - deny", "Response to SignDataRequest", 1063 &core.SignDataResponse{}) 1064 } 1065 { // Sign transaction request 1066 desc := "SignTxRequest contains information about a pending request to sign a transaction. " + 1067 "Aside from the transaction itself, there is also a `call_info`-struct. That struct contains " + 1068 "messages of various types, that the user should be informed of." + 1069 "\n\n" + 1070 "As in any request, it's important to consider that the `meta` info also contains untrusted data." + 1071 "\n\n" + 1072 "The `transaction` (on input into clef) can have either `data` or `input` -- if both are set, " + 1073 "they must be identical, otherwise an error is generated. " + 1074 "However, Clef will always use `data` when passing this struct on (if Clef does otherwise, please file a ticket)" 1075 1076 data := hexutil.Bytes([]byte{0x01, 0x02, 0x03, 0x04}) 1077 add("SignTxRequest", desc, &core.SignTxRequest{ 1078 Meta: meta, 1079 Callinfo: []core.ValidationInfo{ 1080 {"Warning", "Something looks odd, show this message as a warning"}, 1081 {"Info", "User should see this aswell"}, 1082 }, 1083 Transaction: core.SendTxArgs{ 1084 Data: &data, 1085 Nonce: 0x1, 1086 Value: hexutil.Big(*big.NewInt(6)), 1087 From: common.NewMixedcaseAddress(a), 1088 To: nil, 1089 GasPrice: hexutil.Big(*big.NewInt(5)), 1090 Gas: 1000, 1091 Input: nil, 1092 }}) 1093 } 1094 { // Sign tx response 1095 data := hexutil.Bytes([]byte{0x04, 0x03, 0x02, 0x01}) 1096 add("SignTxResponse - approve", "Response to request to sign a transaction. This response needs to contain the `transaction`"+ 1097 ", because the UI is free to make modifications to the transaction.", 1098 &core.SignTxResponse{Approved: true, 1099 Transaction: core.SendTxArgs{ 1100 Data: &data, 1101 Nonce: 0x4, 1102 Value: hexutil.Big(*big.NewInt(6)), 1103 From: common.NewMixedcaseAddress(a), 1104 To: nil, 1105 GasPrice: hexutil.Big(*big.NewInt(5)), 1106 Gas: 1000, 1107 Input: nil, 1108 }}) 1109 add("SignTxResponse - deny", "Response to SignTxRequest. When denying a request, there's no need to "+ 1110 "provide the transaction in return", 1111 &core.SignTxResponse{}) 1112 } 1113 { // WHen a signed tx is ready to go out 1114 desc := "SignTransactionResult is used in the call `clef` -> `OnApprovedTx(result)`" + 1115 "\n\n" + 1116 "This occurs _after_ successful completion of the entire signing procedure, but right before the signed " + 1117 "transaction is passed to the external caller. This method (and data) can be used by the UI to signal " + 1118 "to the user that the transaction was signed, but it is primarily useful for ruleset implementations." + 1119 "\n\n" + 1120 "A ruleset that implements a rate limitation needs to know what transactions are sent out to the external " + 1121 "interface. By hooking into this methods, the ruleset can maintain track of that count." + 1122 "\n\n" + 1123 "**OBS:** Note that if an attacker can restore your `clef` data to a previous point in time" + 1124 " (e.g through a backup), the attacker can reset such windows, even if he/she is unable to decrypt the content. " + 1125 "\n\n" + 1126 "The `OnApproved` method cannot be responded to, it's purely informative" 1127 1128 rlpdata := common.FromHex("0xf85d640101948a8eafb1cf62bfbeb1741769dae1a9dd47996192018026a0716bd90515acb1e68e5ac5867aa11a1e65399c3349d479f5fb698554ebc6f293a04e8a4ebfff434e971e0ef12c5bf3a881b06fd04fc3f8b8a7291fb67a26a1d4ed") 1129 var tx types.Transaction 1130 rlp.DecodeBytes(rlpdata, &tx) 1131 add("OnApproved - SignTransactionResult", desc, ðapi.SignTransactionResult{Raw: rlpdata, Tx: &tx}) 1132 1133 } 1134 { // User input 1135 add("UserInputRequest", "Sent when clef needs the user to provide data. If 'password' is true, the input field should be treated accordingly (echo-free)", 1136 &core.UserInputRequest{IsPassword: true, Title: "The title here", Prompt: "The question to ask the user"}) 1137 add("UserInputResponse", "Response to UserInputRequest", 1138 &core.UserInputResponse{Text: "The textual response from user"}) 1139 } 1140 { // List request 1141 add("ListRequest", "Sent when a request has been made to list addresses. The UI is provided with the "+ 1142 "full `account`s, including local directory names. Note: this information is not passed back to the external caller, "+ 1143 "who only sees the `address`es. ", 1144 &core.ListRequest{ 1145 Meta: meta, 1146 Accounts: []accounts.Account{ 1147 {a, accounts.URL{Scheme: "keystore", Path: "/path/to/keyfile/a"}}, 1148 {b, accounts.URL{Scheme: "keystore", Path: "/path/to/keyfile/b"}}}, 1149 }) 1150 1151 add("ListResponse", "Response to list request. The response contains a list of all addresses to show to the caller. "+ 1152 "Note: the UI is free to respond with any address the caller, regardless of whether it exists or not", 1153 &core.ListResponse{ 1154 Accounts: []accounts.Account{ 1155 {common.HexToAddress("0xcowbeef000000cowbeef00000000000000000c0w"), accounts.URL{Path: ".. ignored .."}}, 1156 {common.HexToAddress("0xffffffffffffffffffffffffffffffffffffffff"), accounts.URL{}}, 1157 }}) 1158 } 1159 1160 fmt.Println(`## UI Client interface 1161 1162 These data types are defined in the channel between clef and the UI`) 1163 for _, elem := range output { 1164 fmt.Println(elem) 1165 } 1166 }