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