github.com/n1ghtfa1l/go-vnt@v0.6.4-alpha.6/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/vntchain/go-vnt/cmd/utils" 39 "github.com/vntchain/go-vnt/common" 40 "github.com/vntchain/go-vnt/crypto" 41 "github.com/vntchain/go-vnt/log" 42 "github.com/vntchain/go-vnt/node" 43 "github.com/vntchain/go-vnt/rpc" 44 "github.com/vntchain/go-vnt/signer/core" 45 "github.com/vntchain/go-vnt/signer/rules" 46 "github.com/vntchain/go-vnt/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 VNT account operations" 174 app.Flags = []cli.Flag{ 175 logLevelFlag, 176 keystoreFlag, 177 configdirFlag, 178 utils.NetworkIdFlag, 179 utils.LightKDFFlag, 180 utils.RPCListenAddrFlag, 181 utils.RPCVirtualHostsFlag, 182 utils.IPCDisabledFlag, 183 utils.IPCPathFlag, 184 utils.RPCEnabledFlag, 185 rpcPortFlag, 186 signerSecretFlag, 187 dBFlag, 188 customDBFlag, 189 auditLogFlag, 190 ruleFlag, 191 stdiouiFlag, 192 testFlag, 193 } 194 app.Action = signer 195 app.Commands = []cli.Command{initCommand, attestCommand, addCredentialCommand} 196 197 } 198 func main() { 199 if err := app.Run(os.Args); err != nil { 200 fmt.Fprintln(os.Stderr, err) 201 os.Exit(1) 202 } 203 } 204 205 func initializeSecrets(c *cli.Context) error { 206 if err := initialize(c); err != nil { 207 return err 208 } 209 configDir := c.String(configdirFlag.Name) 210 211 masterSeed := make([]byte, 256) 212 n, err := io.ReadFull(rand.Reader, masterSeed) 213 if err != nil { 214 return err 215 } 216 if n != len(masterSeed) { 217 return fmt.Errorf("failed to read enough random") 218 } 219 err = os.Mkdir(configDir, 0700) 220 if err != nil && !os.IsExist(err) { 221 return err 222 } 223 location := filepath.Join(configDir, "secrets.dat") 224 if _, err := os.Stat(location); err == nil { 225 return fmt.Errorf("file %v already exists, will not overwrite", location) 226 } 227 err = ioutil.WriteFile(location, masterSeed, 0700) 228 if err != nil { 229 return err 230 } 231 fmt.Printf("A master seed has been generated into %s\n", location) 232 fmt.Printf(` 233 This is required to be able to store credentials, such as : 234 * Passwords for keystores (used by rule engine) 235 * Storage for javascript rules 236 * Hash of rule-file 237 238 You should treat that file with utmost secrecy, and make a backup of it. 239 NOTE: This file does not contain your accounts. Those need to be backed up separately! 240 241 `) 242 return nil 243 } 244 func attestFile(ctx *cli.Context) error { 245 if len(ctx.Args()) < 1 { 246 utils.Fatalf("This command requires an argument.") 247 } 248 if err := initialize(ctx); err != nil { 249 return err 250 } 251 252 stretchedKey, err := readMasterKey(ctx) 253 if err != nil { 254 utils.Fatalf(err.Error()) 255 } 256 configDir := ctx.String(configdirFlag.Name) 257 vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10])) 258 confKey := crypto.Keccak256([]byte("config"), stretchedKey) 259 260 // Initialize the encrypted storages 261 configStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "config.json"), confKey) 262 val := ctx.Args().First() 263 configStorage.Put("ruleset_sha256", val) 264 log.Info("Ruleset attestation updated", "sha256", val) 265 return nil 266 } 267 268 func addCredential(ctx *cli.Context) error { 269 if len(ctx.Args()) < 1 { 270 utils.Fatalf("This command requires at leaste one argument.") 271 } 272 if err := initialize(ctx); err != nil { 273 return err 274 } 275 276 stretchedKey, err := readMasterKey(ctx) 277 if err != nil { 278 utils.Fatalf(err.Error()) 279 } 280 configDir := ctx.String(configdirFlag.Name) 281 vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10])) 282 pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey) 283 284 // Initialize the encrypted storages 285 pwStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey) 286 key := ctx.Args().First() 287 value := "" 288 if len(ctx.Args()) > 1 { 289 value = ctx.Args().Get(1) 290 } 291 pwStorage.Put(key, value) 292 log.Info("Credential store updated", "key", key) 293 return nil 294 } 295 296 func initialize(c *cli.Context) error { 297 // Set up the logger to print everything 298 logOutput := os.Stdout 299 if c.Bool(stdiouiFlag.Name) { 300 logOutput = os.Stderr 301 // If using the stdioui, we can't do the 'confirm'-flow 302 fmt.Fprintf(logOutput, legalWarning) 303 } else { 304 if !confirm(legalWarning) { 305 return fmt.Errorf("aborted by user") 306 } 307 } 308 309 log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(c.Int(logLevelFlag.Name)), log.StreamHandler(logOutput, log.TerminalFormat(true)))) 310 return nil 311 } 312 313 func signer(c *cli.Context) error { 314 if err := initialize(c); err != nil { 315 return err 316 } 317 var ( 318 ui core.SignerUI 319 ) 320 if c.Bool(stdiouiFlag.Name) { 321 log.Info("Using stdin/stdout as UI-channel") 322 ui = core.NewStdIOUI() 323 } else { 324 log.Info("Using CLI as UI-channel") 325 ui = core.NewCommandlineUI() 326 } 327 db, err := core.NewAbiDBFromFiles(c.String(dBFlag.Name), c.String(customDBFlag.Name)) 328 if err != nil { 329 utils.Fatalf(err.Error()) 330 } 331 log.Info("Loaded 4byte db", "signatures", db.Size(), "file", c.String("4bytedb")) 332 333 var ( 334 api core.ExternalAPI 335 ) 336 337 configDir := c.String(configdirFlag.Name) 338 if stretchedKey, err := readMasterKey(c); err != nil { 339 log.Info("No master seed provided, rules disabled") 340 } else { 341 342 if err != nil { 343 utils.Fatalf(err.Error()) 344 } 345 vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10])) 346 347 // Generate domain specific keys 348 pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey) 349 jskey := crypto.Keccak256([]byte("jsstorage"), stretchedKey) 350 confkey := crypto.Keccak256([]byte("config"), stretchedKey) 351 352 // Initialize the encrypted storages 353 pwStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey) 354 jsStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "jsstorage.json"), jskey) 355 configStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "config.json"), confkey) 356 357 //Do we have a rule-file? 358 ruleJS, err := ioutil.ReadFile(c.String(ruleFlag.Name)) 359 if err != nil { 360 log.Info("Could not load rulefile, rules not enabled", "file", "rulefile") 361 } else { 362 hasher := sha256.New() 363 hasher.Write(ruleJS) 364 shasum := hasher.Sum(nil) 365 storedShasum := configStorage.Get("ruleset_sha256") 366 if storedShasum != hex.EncodeToString(shasum) { 367 log.Info("Could not validate ruleset hash, rules not enabled", "got", hex.EncodeToString(shasum), "expected", storedShasum) 368 } else { 369 // Initialize rules 370 ruleEngine, err := rules.NewRuleEvaluator(ui, jsStorage, pwStorage) 371 if err != nil { 372 utils.Fatalf(err.Error()) 373 } 374 ruleEngine.Init(string(ruleJS)) 375 ui = ruleEngine 376 log.Info("Rule engine configured", "file", c.String(ruleFlag.Name)) 377 } 378 } 379 } 380 381 apiImpl := core.NewSignerAPI( 382 c.Int64(utils.NetworkIdFlag.Name), 383 c.String(keystoreFlag.Name), 384 ui, db, 385 c.Bool(utils.LightKDFFlag.Name)) 386 387 api = apiImpl 388 389 // Audit logging 390 if logfile := c.String(auditLogFlag.Name); logfile != "" { 391 api, err = core.NewAuditLogger(logfile, api) 392 if err != nil { 393 utils.Fatalf(err.Error()) 394 } 395 log.Info("Audit logs configured", "file", logfile) 396 } 397 // register signer API with server 398 var ( 399 extapiURL = "n/a" 400 ipcapiURL = "n/a" 401 ) 402 rpcAPI := []rpc.API{ 403 { 404 Namespace: "account", 405 Public: true, 406 Service: api, 407 Version: "1.0"}, 408 } 409 if c.Bool(utils.RPCEnabledFlag.Name) { 410 411 vhosts := splitAndTrim(c.GlobalString(utils.RPCVirtualHostsFlag.Name)) 412 cors := splitAndTrim(c.GlobalString(utils.RPCCORSDomainFlag.Name)) 413 414 // start http server 415 httpEndpoint := fmt.Sprintf("%s:%d", c.String(utils.RPCListenAddrFlag.Name), c.Int(rpcPortFlag.Name)) 416 listener, _, err := rpc.StartHTTPEndpoint(httpEndpoint, rpcAPI, []string{"account"}, cors, vhosts) 417 if err != nil { 418 utils.Fatalf("Could not start RPC api: %v", err) 419 } 420 extapiURL = fmt.Sprintf("http://%s", httpEndpoint) 421 log.Info("HTTP endpoint opened", "url", extapiURL) 422 423 defer func() { 424 listener.Close() 425 log.Info("HTTP endpoint closed", "url", httpEndpoint) 426 }() 427 428 } 429 if !c.Bool(utils.IPCDisabledFlag.Name) { 430 if c.IsSet(utils.IPCPathFlag.Name) { 431 ipcapiURL = c.String(utils.IPCPathFlag.Name) 432 } else { 433 ipcapiURL = filepath.Join(configDir, "clef.ipc") 434 } 435 436 listener, _, err := rpc.StartIPCEndpoint(ipcapiURL, rpcAPI) 437 if err != nil { 438 utils.Fatalf("Could not start IPC api: %v", err) 439 } 440 log.Info("IPC endpoint opened", "url", ipcapiURL) 441 defer func() { 442 listener.Close() 443 log.Info("IPC endpoint closed", "url", ipcapiURL) 444 }() 445 446 } 447 448 if c.Bool(testFlag.Name) { 449 log.Info("Performing UI test") 450 go testExternalUI(apiImpl) 451 } 452 ui.OnSignerStartup(core.StartupInfo{ 453 Info: map[string]interface{}{ 454 "extapi_version": ExternalAPIVersion, 455 "intapi_version": InternalAPIVersion, 456 "extapi_http": extapiURL, 457 "extapi_ipc": ipcapiURL, 458 }, 459 }) 460 461 abortChan := make(chan os.Signal) 462 signal.Notify(abortChan, os.Interrupt) 463 464 sig := <-abortChan 465 log.Info("Exiting...", "signal", sig) 466 467 return nil 468 } 469 470 // splitAndTrim splits input separated by a comma 471 // and trims excessive white space from the substrings. 472 func splitAndTrim(input string) []string { 473 result := strings.Split(input, ",") 474 for i, r := range result { 475 result[i] = strings.TrimSpace(r) 476 } 477 return result 478 } 479 480 // DefaultConfigDir is the default config directory to use for the vaults and other 481 // persistence requirements. 482 func DefaultConfigDir() string { 483 // Try to place the data folder in the user's home dir 484 home := homeDir() 485 if home != "" { 486 if runtime.GOOS == "darwin" { 487 return filepath.Join(home, "Library", "Signer") 488 } else if runtime.GOOS == "windows" { 489 return filepath.Join(home, "AppData", "Roaming", "Signer") 490 } else { 491 return filepath.Join(home, ".clef") 492 } 493 } 494 // As we cannot guess a stable location, return empty and handle later 495 return "" 496 } 497 498 func homeDir() string { 499 if home := os.Getenv("HOME"); home != "" { 500 return home 501 } 502 if usr, err := user.Current(); err == nil { 503 return usr.HomeDir 504 } 505 return "" 506 } 507 func readMasterKey(ctx *cli.Context) ([]byte, error) { 508 var ( 509 file string 510 configDir = ctx.String(configdirFlag.Name) 511 ) 512 if ctx.IsSet(signerSecretFlag.Name) { 513 file = ctx.String(signerSecretFlag.Name) 514 } else { 515 file = filepath.Join(configDir, "secrets.dat") 516 } 517 if err := checkFile(file); err != nil { 518 return nil, err 519 } 520 masterKey, err := ioutil.ReadFile(file) 521 if err != nil { 522 return nil, err 523 } 524 if len(masterKey) < 256 { 525 return nil, fmt.Errorf("master key of insufficient length, expected >255 bytes, got %d", len(masterKey)) 526 } 527 // Create vault location 528 vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), masterKey)[:10])) 529 err = os.Mkdir(vaultLocation, 0700) 530 if err != nil && !os.IsExist(err) { 531 return nil, err 532 } 533 //!TODO, use KDF to stretch the master key 534 // stretched_key := stretch_key(master_key) 535 536 return masterKey, nil 537 } 538 539 // checkFile is a convenience function to check if a file 540 // * exists 541 // * is mode 0600 542 func checkFile(filename string) error { 543 info, err := os.Stat(filename) 544 if err != nil { 545 return fmt.Errorf("failed stat on %s: %v", filename, err) 546 } 547 // Check the unix permission bits 548 if info.Mode().Perm()&077 != 0 { 549 return fmt.Errorf("file (%v) has insecure file permissions (%v)", filename, info.Mode().String()) 550 } 551 return nil 552 } 553 554 // confirm displays a text and asks for user confirmation 555 func confirm(text string) bool { 556 fmt.Printf(text) 557 fmt.Printf("\nEnter 'ok' to proceed:\n>") 558 559 text, err := bufio.NewReader(os.Stdin).ReadString('\n') 560 if err != nil { 561 log.Crit("Failed to read user input", "err", err) 562 } 563 564 if text := strings.TrimSpace(text); text == "ok" { 565 return true 566 } 567 return false 568 } 569 570 func testExternalUI(api *core.SignerAPI) { 571 572 ctx := context.WithValue(context.Background(), "remote", "clef binary") 573 ctx = context.WithValue(ctx, "scheme", "in-proc") 574 ctx = context.WithValue(ctx, "local", "main") 575 576 errs := make([]string, 0) 577 578 api.UI.ShowInfo("Testing 'ShowInfo'") 579 api.UI.ShowError("Testing 'ShowError'") 580 581 checkErr := func(method string, err error) { 582 if err != nil && err != core.ErrRequestDenied { 583 errs = append(errs, fmt.Sprintf("%v: %v", method, err.Error())) 584 } 585 } 586 var err error 587 588 _, err = api.SignTransaction(ctx, core.SendTxArgs{From: common.MixedcaseAddress{}}, nil) 589 checkErr("SignTransaction", err) 590 _, err = api.Sign(ctx, common.MixedcaseAddress{}, common.Hex2Bytes("01020304")) 591 checkErr("Sign", err) 592 _, err = api.List(ctx) 593 checkErr("List", err) 594 _, err = api.New(ctx) 595 checkErr("New", err) 596 _, err = api.Export(ctx, common.Address{}) 597 checkErr("Export", err) 598 _, err = api.Import(ctx, json.RawMessage{}) 599 checkErr("Import", err) 600 601 api.UI.ShowInfo("Tests completed") 602 603 if len(errs) > 0 { 604 log.Error("Got errors") 605 for _, e := range errs { 606 log.Error(e) 607 } 608 } else { 609 log.Info("No errors") 610 } 611 612 } 613 614 /** 615 //Create Account 616 617 curl -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_new","params":["test"],"id":67}' localhost:8550 618 619 // List accounts 620 621 curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_list","params":[""],"id":67}' http://localhost:8550/ 622 623 // Make Transaction 624 // safeSend(0x12) 625 // 4401a6e40000000000000000000000000000000000000000000000000000000000000012 626 627 // supplied abi 628 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/ 629 630 // Not supplied 631 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/ 632 633 // Sign data 634 635 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/ 636 637 638 **/