github.com/luckypickle/go-ethereum-vet@v1.14.2/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 // signer is a utility that can be used so sign transactions and 18 // arbitrary data. 19 package main 20 21 import ( 22 "bufio" 23 "context" 24 "crypto/rand" 25 "crypto/sha256" 26 "encoding/hex" 27 "encoding/json" 28 "fmt" 29 "io" 30 "io/ioutil" 31 "os" 32 "os/signal" 33 "os/user" 34 "path/filepath" 35 "runtime" 36 "strings" 37 38 "github.com/luckypickle/go-ethereum-vet/cmd/utils" 39 "github.com/luckypickle/go-ethereum-vet/common" 40 "github.com/luckypickle/go-ethereum-vet/crypto" 41 "github.com/luckypickle/go-ethereum-vet/log" 42 "github.com/luckypickle/go-ethereum-vet/node" 43 "github.com/luckypickle/go-ethereum-vet/rpc" 44 "github.com/luckypickle/go-ethereum-vet/signer/core" 45 "github.com/luckypickle/go-ethereum-vet/signer/rules" 46 "github.com/luckypickle/go-ethereum-vet/signer/storage" 47 "gopkg.in/urfave/cli.v1" 48 ) 49 50 // ExternalAPIVersion -- see extapi_changelog.md 51 const ExternalAPIVersion = "2.0.0" 52 53 // InternalAPIVersion -- see intapi_changelog.md 54 const InternalAPIVersion = "2.0.0" 55 56 const legalWarning = ` 57 WARNING! 58 59 Clef is alpha software, and not yet publically released. This software has _not_ been audited, and there 60 are no guarantees about the workings of this software. It may contain severe flaws. You should not use this software 61 unless you agree to take full responsibility for doing so, and know what you are doing. 62 63 TLDR; THIS IS NOT PRODUCTION-READY SOFTWARE! 64 65 ` 66 67 var ( 68 logLevelFlag = cli.IntFlag{ 69 Name: "loglevel", 70 Value: 4, 71 Usage: "log level to emit to the screen", 72 } 73 keystoreFlag = cli.StringFlag{ 74 Name: "keystore", 75 Value: filepath.Join(node.DefaultDataDir(), "keystore"), 76 Usage: "Directory for the keystore", 77 } 78 configdirFlag = cli.StringFlag{ 79 Name: "configdir", 80 Value: DefaultConfigDir(), 81 Usage: "Directory for Clef configuration", 82 } 83 rpcPortFlag = cli.IntFlag{ 84 Name: "rpcport", 85 Usage: "HTTP-RPC server listening port", 86 Value: node.DefaultHTTPPort + 5, 87 } 88 signerSecretFlag = cli.StringFlag{ 89 Name: "signersecret", 90 Usage: "A file containing the password used to encrypt Clef credentials, e.g. keystore credentials and ruleset hash", 91 } 92 dBFlag = cli.StringFlag{ 93 Name: "4bytedb", 94 Usage: "File containing 4byte-identifiers", 95 Value: "./4byte.json", 96 } 97 customDBFlag = cli.StringFlag{ 98 Name: "4bytedb-custom", 99 Usage: "File used for writing new 4byte-identifiers submitted via API", 100 Value: "./4byte-custom.json", 101 } 102 auditLogFlag = cli.StringFlag{ 103 Name: "auditlog", 104 Usage: "File used to emit audit logs. Set to \"\" to disable", 105 Value: "audit.log", 106 } 107 ruleFlag = cli.StringFlag{ 108 Name: "rules", 109 Usage: "Enable rule-engine", 110 Value: "rules.json", 111 } 112 stdiouiFlag = cli.BoolFlag{ 113 Name: "stdio-ui", 114 Usage: "Use STDIN/STDOUT as a channel for an external UI. " + 115 "This means that an STDIN/STDOUT is used for RPC-communication with a e.g. a graphical user " + 116 "interface, and can be used when Clef is started by an external process.", 117 } 118 testFlag = cli.BoolFlag{ 119 Name: "stdio-ui-test", 120 Usage: "Mechanism to test interface between Clef and UI. Requires 'stdio-ui'.", 121 } 122 app = cli.NewApp() 123 initCommand = cli.Command{ 124 Action: utils.MigrateFlags(initializeSecrets), 125 Name: "init", 126 Usage: "Initialize the signer, generate secret storage", 127 ArgsUsage: "", 128 Flags: []cli.Flag{ 129 logLevelFlag, 130 configdirFlag, 131 }, 132 Description: ` 133 The init command generates a master seed which Clef can use to store credentials and data needed for 134 the rule-engine to work.`, 135 } 136 attestCommand = cli.Command{ 137 Action: utils.MigrateFlags(attestFile), 138 Name: "attest", 139 Usage: "Attest that a js-file is to be used", 140 ArgsUsage: "<sha256sum>", 141 Flags: []cli.Flag{ 142 logLevelFlag, 143 configdirFlag, 144 signerSecretFlag, 145 }, 146 Description: ` 147 The attest command stores the sha256 of the rule.js-file that you want to use for automatic processing of 148 incoming requests. 149 150 Whenever you make an edit to the rule file, you need to use attestation to tell 151 Clef that the file is 'safe' to execute.`, 152 } 153 154 addCredentialCommand = cli.Command{ 155 Action: utils.MigrateFlags(addCredential), 156 Name: "addpw", 157 Usage: "Store a credential for a keystore file", 158 ArgsUsage: "<address> <password>", 159 Flags: []cli.Flag{ 160 logLevelFlag, 161 configdirFlag, 162 signerSecretFlag, 163 }, 164 Description: ` 165 The addpw command stores a password for a given address (keyfile). If you invoke it with only one parameter, it will 166 remove any stored credential for that address (keyfile) 167 `, 168 } 169 ) 170 171 func init() { 172 app.Name = "Clef" 173 app.Usage = "Manage Ethereum account operations" 174 app.Flags = []cli.Flag{ 175 logLevelFlag, 176 keystoreFlag, 177 configdirFlag, 178 utils.NetworkIdFlag, 179 utils.LightKDFFlag, 180 utils.NoUSBFlag, 181 utils.RPCListenAddrFlag, 182 utils.RPCVirtualHostsFlag, 183 utils.IPCDisabledFlag, 184 utils.IPCPathFlag, 185 utils.RPCEnabledFlag, 186 rpcPortFlag, 187 signerSecretFlag, 188 dBFlag, 189 customDBFlag, 190 auditLogFlag, 191 ruleFlag, 192 stdiouiFlag, 193 testFlag, 194 } 195 app.Action = signer 196 app.Commands = []cli.Command{initCommand, attestCommand, addCredentialCommand} 197 198 } 199 func main() { 200 if err := app.Run(os.Args); err != nil { 201 fmt.Fprintln(os.Stderr, err) 202 os.Exit(1) 203 } 204 } 205 206 func initializeSecrets(c *cli.Context) error { 207 if err := initialize(c); err != nil { 208 return err 209 } 210 configDir := c.String(configdirFlag.Name) 211 212 masterSeed := make([]byte, 256) 213 n, err := io.ReadFull(rand.Reader, masterSeed) 214 if err != nil { 215 return err 216 } 217 if n != len(masterSeed) { 218 return fmt.Errorf("failed to read enough random") 219 } 220 err = os.Mkdir(configDir, 0700) 221 if err != nil && !os.IsExist(err) { 222 return err 223 } 224 location := filepath.Join(configDir, "secrets.dat") 225 if _, err := os.Stat(location); err == nil { 226 return fmt.Errorf("file %v already exists, will not overwrite", location) 227 } 228 err = ioutil.WriteFile(location, masterSeed, 0700) 229 if err != nil { 230 return err 231 } 232 fmt.Printf("A master seed has been generated into %s\n", location) 233 fmt.Printf(` 234 This is required to be able to store credentials, such as : 235 * Passwords for keystores (used by rule engine) 236 * Storage for javascript rules 237 * Hash of rule-file 238 239 You should treat that file with utmost secrecy, and make a backup of it. 240 NOTE: This file does not contain your accounts. Those need to be backed up separately! 241 242 `) 243 return nil 244 } 245 func attestFile(ctx *cli.Context) error { 246 if len(ctx.Args()) < 1 { 247 utils.Fatalf("This command requires an argument.") 248 } 249 if err := initialize(ctx); err != nil { 250 return err 251 } 252 253 stretchedKey, err := readMasterKey(ctx) 254 if err != nil { 255 utils.Fatalf(err.Error()) 256 } 257 configDir := ctx.String(configdirFlag.Name) 258 vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10])) 259 confKey := crypto.Keccak256([]byte("config"), stretchedKey) 260 261 // Initialize the encrypted storages 262 configStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "config.json"), confKey) 263 val := ctx.Args().First() 264 configStorage.Put("ruleset_sha256", val) 265 log.Info("Ruleset attestation updated", "sha256", val) 266 return nil 267 } 268 269 func addCredential(ctx *cli.Context) error { 270 if len(ctx.Args()) < 1 { 271 utils.Fatalf("This command requires at leaste one argument.") 272 } 273 if err := initialize(ctx); err != nil { 274 return err 275 } 276 277 stretchedKey, err := readMasterKey(ctx) 278 if err != nil { 279 utils.Fatalf(err.Error()) 280 } 281 configDir := ctx.String(configdirFlag.Name) 282 vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10])) 283 pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey) 284 285 // Initialize the encrypted storages 286 pwStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey) 287 key := ctx.Args().First() 288 value := "" 289 if len(ctx.Args()) > 1 { 290 value = ctx.Args().Get(1) 291 } 292 pwStorage.Put(key, value) 293 log.Info("Credential store updated", "key", key) 294 return nil 295 } 296 297 func initialize(c *cli.Context) error { 298 // Set up the logger to print everything 299 logOutput := os.Stdout 300 if c.Bool(stdiouiFlag.Name) { 301 logOutput = os.Stderr 302 // If using the stdioui, we can't do the 'confirm'-flow 303 fmt.Fprintf(logOutput, legalWarning) 304 } else { 305 if !confirm(legalWarning) { 306 return fmt.Errorf("aborted by user") 307 } 308 } 309 310 log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(c.Int(logLevelFlag.Name)), log.StreamHandler(logOutput, log.TerminalFormat(true)))) 311 return nil 312 } 313 314 func signer(c *cli.Context) error { 315 if err := initialize(c); err != nil { 316 return err 317 } 318 var ( 319 ui core.SignerUI 320 ) 321 if c.Bool(stdiouiFlag.Name) { 322 log.Info("Using stdin/stdout as UI-channel") 323 ui = core.NewStdIOUI() 324 } else { 325 log.Info("Using CLI as UI-channel") 326 ui = core.NewCommandlineUI() 327 } 328 db, err := core.NewAbiDBFromFiles(c.String(dBFlag.Name), c.String(customDBFlag.Name)) 329 if err != nil { 330 utils.Fatalf(err.Error()) 331 } 332 log.Info("Loaded 4byte db", "signatures", db.Size(), "file", c.String("4bytedb")) 333 334 var ( 335 api core.ExternalAPI 336 ) 337 338 configDir := c.String(configdirFlag.Name) 339 if stretchedKey, err := readMasterKey(c); err != nil { 340 log.Info("No master seed provided, rules disabled") 341 } else { 342 343 if err != nil { 344 utils.Fatalf(err.Error()) 345 } 346 vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10])) 347 348 // Generate domain specific keys 349 pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey) 350 jskey := crypto.Keccak256([]byte("jsstorage"), stretchedKey) 351 confkey := crypto.Keccak256([]byte("config"), stretchedKey) 352 353 // Initialize the encrypted storages 354 pwStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey) 355 jsStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "jsstorage.json"), jskey) 356 configStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "config.json"), confkey) 357 358 //Do we have a rule-file? 359 ruleJS, err := ioutil.ReadFile(c.String(ruleFlag.Name)) 360 if err != nil { 361 log.Info("Could not load rulefile, rules not enabled", "file", "rulefile") 362 } else { 363 hasher := sha256.New() 364 hasher.Write(ruleJS) 365 shasum := hasher.Sum(nil) 366 storedShasum := configStorage.Get("ruleset_sha256") 367 if storedShasum != hex.EncodeToString(shasum) { 368 log.Info("Could not validate ruleset hash, rules not enabled", "got", hex.EncodeToString(shasum), "expected", storedShasum) 369 } else { 370 // Initialize rules 371 ruleEngine, err := rules.NewRuleEvaluator(ui, jsStorage, pwStorage) 372 if err != nil { 373 utils.Fatalf(err.Error()) 374 } 375 ruleEngine.Init(string(ruleJS)) 376 ui = ruleEngine 377 log.Info("Rule engine configured", "file", c.String(ruleFlag.Name)) 378 } 379 } 380 } 381 382 apiImpl := core.NewSignerAPI( 383 c.Int64(utils.NetworkIdFlag.Name), 384 c.String(keystoreFlag.Name), 385 c.Bool(utils.NoUSBFlag.Name), 386 ui, db, 387 c.Bool(utils.LightKDFFlag.Name)) 388 389 api = apiImpl 390 391 // Audit logging 392 if logfile := c.String(auditLogFlag.Name); logfile != "" { 393 api, err = core.NewAuditLogger(logfile, api) 394 if err != nil { 395 utils.Fatalf(err.Error()) 396 } 397 log.Info("Audit logs configured", "file", logfile) 398 } 399 // register signer API with server 400 var ( 401 extapiURL = "n/a" 402 ipcapiURL = "n/a" 403 ) 404 rpcAPI := []rpc.API{ 405 { 406 Namespace: "account", 407 Public: true, 408 Service: api, 409 Version: "1.0"}, 410 } 411 if c.Bool(utils.RPCEnabledFlag.Name) { 412 413 vhosts := splitAndTrim(c.GlobalString(utils.RPCVirtualHostsFlag.Name)) 414 cors := splitAndTrim(c.GlobalString(utils.RPCCORSDomainFlag.Name)) 415 416 // start http server 417 httpEndpoint := fmt.Sprintf("%s:%d", c.String(utils.RPCListenAddrFlag.Name), c.Int(rpcPortFlag.Name)) 418 listener, _, err := rpc.StartHTTPEndpoint(httpEndpoint, rpcAPI, []string{"account"}, cors, vhosts, rpc.DefaultHTTPTimeouts) 419 if err != nil { 420 utils.Fatalf("Could not start RPC api: %v", err) 421 } 422 extapiURL = fmt.Sprintf("http://%s", httpEndpoint) 423 log.Info("HTTP endpoint opened", "url", extapiURL) 424 425 defer func() { 426 listener.Close() 427 log.Info("HTTP endpoint closed", "url", httpEndpoint) 428 }() 429 430 } 431 if !c.Bool(utils.IPCDisabledFlag.Name) { 432 if c.IsSet(utils.IPCPathFlag.Name) { 433 ipcapiURL = c.String(utils.IPCPathFlag.Name) 434 } else { 435 ipcapiURL = filepath.Join(configDir, "clef.ipc") 436 } 437 438 listener, _, err := rpc.StartIPCEndpoint(ipcapiURL, rpcAPI) 439 if err != nil { 440 utils.Fatalf("Could not start IPC api: %v", err) 441 } 442 log.Info("IPC endpoint opened", "url", ipcapiURL) 443 defer func() { 444 listener.Close() 445 log.Info("IPC endpoint closed", "url", ipcapiURL) 446 }() 447 448 } 449 450 if c.Bool(testFlag.Name) { 451 log.Info("Performing UI test") 452 go testExternalUI(apiImpl) 453 } 454 ui.OnSignerStartup(core.StartupInfo{ 455 Info: map[string]interface{}{ 456 "extapi_version": ExternalAPIVersion, 457 "intapi_version": InternalAPIVersion, 458 "extapi_http": extapiURL, 459 "extapi_ipc": ipcapiURL, 460 }, 461 }) 462 463 abortChan := make(chan os.Signal, 1) 464 signal.Notify(abortChan, os.Interrupt) 465 466 sig := <-abortChan 467 log.Info("Exiting...", "signal", sig) 468 469 return nil 470 } 471 472 // splitAndTrim splits input separated by a comma 473 // and trims excessive white space from the substrings. 474 func splitAndTrim(input string) []string { 475 result := strings.Split(input, ",") 476 for i, r := range result { 477 result[i] = strings.TrimSpace(r) 478 } 479 return result 480 } 481 482 // DefaultConfigDir is the default config directory to use for the vaults and other 483 // persistence requirements. 484 func DefaultConfigDir() string { 485 // Try to place the data folder in the user's home dir 486 home := homeDir() 487 if home != "" { 488 if runtime.GOOS == "darwin" { 489 return filepath.Join(home, "Library", "Signer") 490 } else if runtime.GOOS == "windows" { 491 return filepath.Join(home, "AppData", "Roaming", "Signer") 492 } else { 493 return filepath.Join(home, ".clef") 494 } 495 } 496 // As we cannot guess a stable location, return empty and handle later 497 return "" 498 } 499 500 func homeDir() string { 501 if home := os.Getenv("HOME"); home != "" { 502 return home 503 } 504 if usr, err := user.Current(); err == nil { 505 return usr.HomeDir 506 } 507 return "" 508 } 509 func readMasterKey(ctx *cli.Context) ([]byte, error) { 510 var ( 511 file string 512 configDir = ctx.String(configdirFlag.Name) 513 ) 514 if ctx.IsSet(signerSecretFlag.Name) { 515 file = ctx.String(signerSecretFlag.Name) 516 } else { 517 file = filepath.Join(configDir, "secrets.dat") 518 } 519 if err := checkFile(file); err != nil { 520 return nil, err 521 } 522 masterKey, err := ioutil.ReadFile(file) 523 if err != nil { 524 return nil, err 525 } 526 if len(masterKey) < 256 { 527 return nil, fmt.Errorf("master key of insufficient length, expected >255 bytes, got %d", len(masterKey)) 528 } 529 // Create vault location 530 vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), masterKey)[:10])) 531 err = os.Mkdir(vaultLocation, 0700) 532 if err != nil && !os.IsExist(err) { 533 return nil, err 534 } 535 //!TODO, use KDF to stretch the master key 536 // stretched_key := stretch_key(master_key) 537 538 return masterKey, nil 539 } 540 541 // checkFile is a convenience function to check if a file 542 // * exists 543 // * is mode 0600 544 func checkFile(filename string) error { 545 info, err := os.Stat(filename) 546 if err != nil { 547 return fmt.Errorf("failed stat on %s: %v", filename, err) 548 } 549 // Check the unix permission bits 550 if info.Mode().Perm()&077 != 0 { 551 return fmt.Errorf("file (%v) has insecure file permissions (%v)", filename, info.Mode().String()) 552 } 553 return nil 554 } 555 556 // confirm displays a text and asks for user confirmation 557 func confirm(text string) bool { 558 fmt.Printf(text) 559 fmt.Printf("\nEnter 'ok' to proceed:\n>") 560 561 text, err := bufio.NewReader(os.Stdin).ReadString('\n') 562 if err != nil { 563 log.Crit("Failed to read user input", "err", err) 564 } 565 566 if text := strings.TrimSpace(text); text == "ok" { 567 return true 568 } 569 return false 570 } 571 572 func testExternalUI(api *core.SignerAPI) { 573 574 ctx := context.WithValue(context.Background(), "remote", "clef binary") 575 ctx = context.WithValue(ctx, "scheme", "in-proc") 576 ctx = context.WithValue(ctx, "local", "main") 577 578 errs := make([]string, 0) 579 580 api.UI.ShowInfo("Testing 'ShowInfo'") 581 api.UI.ShowError("Testing 'ShowError'") 582 583 checkErr := func(method string, err error) { 584 if err != nil && err != core.ErrRequestDenied { 585 errs = append(errs, fmt.Sprintf("%v: %v", method, err.Error())) 586 } 587 } 588 var err error 589 590 _, err = api.SignTransaction(ctx, core.SendTxArgs{From: common.MixedcaseAddress{}}, nil) 591 checkErr("SignTransaction", err) 592 _, err = api.Sign(ctx, common.MixedcaseAddress{}, common.Hex2Bytes("01020304")) 593 checkErr("Sign", err) 594 _, err = api.List(ctx) 595 checkErr("List", err) 596 _, err = api.New(ctx) 597 checkErr("New", err) 598 _, err = api.Export(ctx, common.Address{}) 599 checkErr("Export", err) 600 _, err = api.Import(ctx, json.RawMessage{}) 601 checkErr("Import", err) 602 603 api.UI.ShowInfo("Tests completed") 604 605 if len(errs) > 0 { 606 log.Error("Got errors") 607 for _, e := range errs { 608 log.Error(e) 609 } 610 } else { 611 log.Info("No errors") 612 } 613 614 } 615 616 /** 617 //Create Account 618 619 curl -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_new","params":["test"],"id":67}' localhost:8550 620 621 // List accounts 622 623 curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_list","params":[""],"id":67}' http://localhost:8550/ 624 625 // Make Transaction 626 // safeSend(0x12) 627 // 4401a6e40000000000000000000000000000000000000000000000000000000000000012 628 629 // supplied abi 630 curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_signTransaction","params":[{"from":"0x82A2A876D39022B3019932D30Cd9c97ad5616813","gas":"0x333","gasPrice":"0x123","nonce":"0x0","to":"0x07a565b7ed7d7a678680a4c162885bedbb695fe0", "value":"0x10", "data":"0x4401a6e40000000000000000000000000000000000000000000000000000000000000012"},"test"],"id":67}' http://localhost:8550/ 631 632 // Not supplied 633 curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_signTransaction","params":[{"from":"0x82A2A876D39022B3019932D30Cd9c97ad5616813","gas":"0x333","gasPrice":"0x123","nonce":"0x0","to":"0x07a565b7ed7d7a678680a4c162885bedbb695fe0", "value":"0x10", "data":"0x4401a6e40000000000000000000000000000000000000000000000000000000000000012"}],"id":67}' http://localhost:8550/ 634 635 // Sign data 636 637 curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_sign","params":["0x694267f14675d7e1b9494fd8d72fefe1755710fa","bazonk gaz baz"],"id":67}' http://localhost:8550/ 638 639 640 **/