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