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