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