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