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