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