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