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