github.com/pslzym/go-ethereum@v1.8.17-0.20180926104442-4b6824e07b1b/signer/core/api.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 core 18 19 import ( 20 "context" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "io/ioutil" 25 "math/big" 26 "reflect" 27 28 "github.com/ethereum/go-ethereum/accounts" 29 "github.com/ethereum/go-ethereum/accounts/keystore" 30 "github.com/ethereum/go-ethereum/accounts/usbwallet" 31 "github.com/ethereum/go-ethereum/common" 32 "github.com/ethereum/go-ethereum/common/hexutil" 33 "github.com/ethereum/go-ethereum/crypto" 34 "github.com/ethereum/go-ethereum/internal/ethapi" 35 "github.com/ethereum/go-ethereum/log" 36 "github.com/ethereum/go-ethereum/rlp" 37 ) 38 39 // ExternalAPI defines the external API through which signing requests are made. 40 type ExternalAPI interface { 41 // List available accounts 42 List(ctx context.Context) ([]common.Address, error) 43 // New request to create a new account 44 New(ctx context.Context) (accounts.Account, error) 45 // SignTransaction request to sign the specified transaction 46 SignTransaction(ctx context.Context, args SendTxArgs, methodSelector *string) (*ethapi.SignTransactionResult, error) 47 // Sign - request to sign the given data (plus prefix) 48 Sign(ctx context.Context, addr common.MixedcaseAddress, data hexutil.Bytes) (hexutil.Bytes, error) 49 // Export - request to export an account 50 Export(ctx context.Context, addr common.Address) (json.RawMessage, error) 51 // Import - request to import an account 52 // Should be moved to Internal API, in next phase when we have 53 // bi-directional communication 54 //Import(ctx context.Context, keyJSON json.RawMessage) (Account, error) 55 } 56 57 // SignerUI specifies what method a UI needs to implement to be able to be used as a UI for the signer 58 type SignerUI interface { 59 // ApproveTx prompt the user for confirmation to request to sign Transaction 60 ApproveTx(request *SignTxRequest) (SignTxResponse, error) 61 // ApproveSignData prompt the user for confirmation to request to sign data 62 ApproveSignData(request *SignDataRequest) (SignDataResponse, error) 63 // ApproveExport prompt the user for confirmation to export encrypted Account json 64 ApproveExport(request *ExportRequest) (ExportResponse, error) 65 // ApproveImport prompt the user for confirmation to import Account json 66 ApproveImport(request *ImportRequest) (ImportResponse, error) 67 // ApproveListing prompt the user for confirmation to list accounts 68 // the list of accounts to list can be modified by the UI 69 ApproveListing(request *ListRequest) (ListResponse, error) 70 // ApproveNewAccount prompt the user for confirmation to create new Account, and reveal to caller 71 ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error) 72 // ShowError displays error message to user 73 ShowError(message string) 74 // ShowInfo displays info message to user 75 ShowInfo(message string) 76 // OnApprovedTx notifies the UI about a transaction having been successfully signed. 77 // This method can be used by a UI to keep track of e.g. how much has been sent to a particular recipient. 78 OnApprovedTx(tx ethapi.SignTransactionResult) 79 // OnSignerStartup is invoked when the signer boots, and tells the UI info about external API location and version 80 // information 81 OnSignerStartup(info StartupInfo) 82 } 83 84 // SignerAPI defines the actual implementation of ExternalAPI 85 type SignerAPI struct { 86 chainID *big.Int 87 am *accounts.Manager 88 UI SignerUI 89 validator *Validator 90 rejectMode bool 91 } 92 93 // Metadata about a request 94 type Metadata struct { 95 Remote string `json:"remote"` 96 Local string `json:"local"` 97 Scheme string `json:"scheme"` 98 UserAgent string `json:"User-Agent"` 99 Origin string `json:"Origin"` 100 } 101 102 // MetadataFromContext extracts Metadata from a given context.Context 103 func MetadataFromContext(ctx context.Context) Metadata { 104 m := Metadata{"NA", "NA", "NA", "", ""} // batman 105 106 if v := ctx.Value("remote"); v != nil { 107 m.Remote = v.(string) 108 } 109 if v := ctx.Value("scheme"); v != nil { 110 m.Scheme = v.(string) 111 } 112 if v := ctx.Value("local"); v != nil { 113 m.Local = v.(string) 114 } 115 if v := ctx.Value("Origin"); v != nil { 116 m.Origin = v.(string) 117 } 118 if v := ctx.Value("User-Agent"); v != nil { 119 m.UserAgent = v.(string) 120 } 121 return m 122 } 123 124 // String implements Stringer interface 125 func (m Metadata) String() string { 126 s, err := json.Marshal(m) 127 if err == nil { 128 return string(s) 129 } 130 return err.Error() 131 } 132 133 // types for the requests/response types between signer and UI 134 type ( 135 // SignTxRequest contains info about a Transaction to sign 136 SignTxRequest struct { 137 Transaction SendTxArgs `json:"transaction"` 138 Callinfo []ValidationInfo `json:"call_info"` 139 Meta Metadata `json:"meta"` 140 } 141 // SignTxResponse result from SignTxRequest 142 SignTxResponse struct { 143 //The UI may make changes to the TX 144 Transaction SendTxArgs `json:"transaction"` 145 Approved bool `json:"approved"` 146 Password string `json:"password"` 147 } 148 // ExportRequest info about query to export accounts 149 ExportRequest struct { 150 Address common.Address `json:"address"` 151 Meta Metadata `json:"meta"` 152 } 153 // ExportResponse response to export-request 154 ExportResponse struct { 155 Approved bool `json:"approved"` 156 } 157 // ImportRequest info about request to import an Account 158 ImportRequest struct { 159 Meta Metadata `json:"meta"` 160 } 161 ImportResponse struct { 162 Approved bool `json:"approved"` 163 OldPassword string `json:"old_password"` 164 NewPassword string `json:"new_password"` 165 } 166 SignDataRequest struct { 167 Address common.MixedcaseAddress `json:"address"` 168 Rawdata hexutil.Bytes `json:"raw_data"` 169 Message string `json:"message"` 170 Hash hexutil.Bytes `json:"hash"` 171 Meta Metadata `json:"meta"` 172 } 173 SignDataResponse struct { 174 Approved bool `json:"approved"` 175 Password string 176 } 177 NewAccountRequest struct { 178 Meta Metadata `json:"meta"` 179 } 180 NewAccountResponse struct { 181 Approved bool `json:"approved"` 182 Password string `json:"password"` 183 } 184 ListRequest struct { 185 Accounts []Account `json:"accounts"` 186 Meta Metadata `json:"meta"` 187 } 188 ListResponse struct { 189 Accounts []Account `json:"accounts"` 190 } 191 Message struct { 192 Text string `json:"text"` 193 } 194 StartupInfo struct { 195 Info map[string]interface{} `json:"info"` 196 } 197 ) 198 199 var ErrRequestDenied = errors.New("Request denied") 200 201 // NewSignerAPI creates a new API that can be used for Account management. 202 // ksLocation specifies the directory where to store the password protected private 203 // key that is generated when a new Account is created. 204 // noUSB disables USB support that is required to support hardware devices such as 205 // ledger and trezor. 206 func NewSignerAPI(chainID int64, ksLocation string, noUSB bool, ui SignerUI, abidb *AbiDb, lightKDF bool, advancedMode bool) *SignerAPI { 207 var ( 208 backends []accounts.Backend 209 n, p = keystore.StandardScryptN, keystore.StandardScryptP 210 ) 211 if lightKDF { 212 n, p = keystore.LightScryptN, keystore.LightScryptP 213 } 214 // support password based accounts 215 if len(ksLocation) > 0 { 216 backends = append(backends, keystore.NewKeyStore(ksLocation, n, p)) 217 } 218 if !noUSB { 219 // Start a USB hub for Ledger hardware wallets 220 if ledgerhub, err := usbwallet.NewLedgerHub(); err != nil { 221 log.Warn(fmt.Sprintf("Failed to start Ledger hub, disabling: %v", err)) 222 } else { 223 backends = append(backends, ledgerhub) 224 log.Debug("Ledger support enabled") 225 } 226 // Start a USB hub for Trezor hardware wallets 227 if trezorhub, err := usbwallet.NewTrezorHub(); err != nil { 228 log.Warn(fmt.Sprintf("Failed to start Trezor hub, disabling: %v", err)) 229 } else { 230 backends = append(backends, trezorhub) 231 log.Debug("Trezor support enabled") 232 } 233 } 234 if advancedMode { 235 log.Info("Clef is in advanced mode: will warn instead of reject") 236 } 237 return &SignerAPI{big.NewInt(chainID), accounts.NewManager(backends...), ui, NewValidator(abidb), !advancedMode} 238 } 239 240 // List returns the set of wallet this signer manages. Each wallet can contain 241 // multiple accounts. 242 func (api *SignerAPI) List(ctx context.Context) ([]common.Address, error) { 243 var accs []Account 244 for _, wallet := range api.am.Wallets() { 245 for _, acc := range wallet.Accounts() { 246 acc := Account{Typ: "Account", URL: wallet.URL(), Address: acc.Address} 247 accs = append(accs, acc) 248 } 249 } 250 result, err := api.UI.ApproveListing(&ListRequest{Accounts: accs, Meta: MetadataFromContext(ctx)}) 251 if err != nil { 252 return nil, err 253 } 254 if result.Accounts == nil { 255 return nil, ErrRequestDenied 256 257 } 258 259 addresses := make([]common.Address, 0) 260 for _, acc := range result.Accounts { 261 addresses = append(addresses, acc.Address) 262 } 263 264 return addresses, nil 265 } 266 267 // New creates a new password protected Account. The private key is protected with 268 // the given password. Users are responsible to backup the private key that is stored 269 // in the keystore location thas was specified when this API was created. 270 func (api *SignerAPI) New(ctx context.Context) (accounts.Account, error) { 271 be := api.am.Backends(keystore.KeyStoreType) 272 if len(be) == 0 { 273 return accounts.Account{}, errors.New("password based accounts not supported") 274 } 275 var ( 276 resp NewAccountResponse 277 err error 278 ) 279 // Three retries to get a valid password 280 for i := 0; i < 3; i++ { 281 resp, err = api.UI.ApproveNewAccount(&NewAccountRequest{MetadataFromContext(ctx)}) 282 if err != nil { 283 return accounts.Account{}, err 284 } 285 if !resp.Approved { 286 return accounts.Account{}, ErrRequestDenied 287 } 288 if pwErr := ValidatePasswordFormat(resp.Password); pwErr != nil { 289 api.UI.ShowError(fmt.Sprintf("Account creation attempt #%d failed due to password requirements: %v", (i + 1), pwErr)) 290 } else { 291 // No error 292 return be[0].(*keystore.KeyStore).NewAccount(resp.Password) 293 } 294 } 295 // Otherwise fail, with generic error message 296 return accounts.Account{}, errors.New("account creation failed") 297 } 298 299 // logDiff logs the difference between the incoming (original) transaction and the one returned from the signer. 300 // it also returns 'true' if the transaction was modified, to make it possible to configure the signer not to allow 301 // UI-modifications to requests 302 func logDiff(original *SignTxRequest, new *SignTxResponse) bool { 303 modified := false 304 if f0, f1 := original.Transaction.From, new.Transaction.From; !reflect.DeepEqual(f0, f1) { 305 log.Info("Sender-account changed by UI", "was", f0, "is", f1) 306 modified = true 307 } 308 if t0, t1 := original.Transaction.To, new.Transaction.To; !reflect.DeepEqual(t0, t1) { 309 log.Info("Recipient-account changed by UI", "was", t0, "is", t1) 310 modified = true 311 } 312 if g0, g1 := original.Transaction.Gas, new.Transaction.Gas; g0 != g1 { 313 modified = true 314 log.Info("Gas changed by UI", "was", g0, "is", g1) 315 } 316 if g0, g1 := big.Int(original.Transaction.GasPrice), big.Int(new.Transaction.GasPrice); g0.Cmp(&g1) != 0 { 317 modified = true 318 log.Info("GasPrice changed by UI", "was", g0, "is", g1) 319 } 320 if v0, v1 := big.Int(original.Transaction.Value), big.Int(new.Transaction.Value); v0.Cmp(&v1) != 0 { 321 modified = true 322 log.Info("Value changed by UI", "was", v0, "is", v1) 323 } 324 if d0, d1 := original.Transaction.Data, new.Transaction.Data; d0 != d1 { 325 d0s := "" 326 d1s := "" 327 if d0 != nil { 328 d0s = hexutil.Encode(*d0) 329 } 330 if d1 != nil { 331 d1s = hexutil.Encode(*d1) 332 } 333 if d1s != d0s { 334 modified = true 335 log.Info("Data changed by UI", "was", d0s, "is", d1s) 336 } 337 } 338 if n0, n1 := original.Transaction.Nonce, new.Transaction.Nonce; n0 != n1 { 339 modified = true 340 log.Info("Nonce changed by UI", "was", n0, "is", n1) 341 } 342 return modified 343 } 344 345 // SignTransaction signs the given Transaction and returns it both as json and rlp-encoded form 346 func (api *SignerAPI) SignTransaction(ctx context.Context, args SendTxArgs, methodSelector *string) (*ethapi.SignTransactionResult, error) { 347 var ( 348 err error 349 result SignTxResponse 350 ) 351 msgs, err := api.validator.ValidateTransaction(&args, methodSelector) 352 if err != nil { 353 return nil, err 354 } 355 // If we are in 'rejectMode', then reject rather than show the user warnings 356 if api.rejectMode { 357 if err := msgs.getWarnings(); err != nil { 358 return nil, err 359 } 360 } 361 362 req := SignTxRequest{ 363 Transaction: args, 364 Meta: MetadataFromContext(ctx), 365 Callinfo: msgs.Messages, 366 } 367 // Process approval 368 result, err = api.UI.ApproveTx(&req) 369 if err != nil { 370 return nil, err 371 } 372 if !result.Approved { 373 return nil, ErrRequestDenied 374 } 375 // Log changes made by the UI to the signing-request 376 logDiff(&req, &result) 377 var ( 378 acc accounts.Account 379 wallet accounts.Wallet 380 ) 381 acc = accounts.Account{Address: result.Transaction.From.Address()} 382 wallet, err = api.am.Find(acc) 383 if err != nil { 384 return nil, err 385 } 386 // Convert fields into a real transaction 387 var unsignedTx = result.Transaction.toTransaction() 388 389 // The one to sign is the one that was returned from the UI 390 signedTx, err := wallet.SignTxWithPassphrase(acc, result.Password, unsignedTx, api.chainID) 391 if err != nil { 392 api.UI.ShowError(err.Error()) 393 return nil, err 394 } 395 396 rlpdata, err := rlp.EncodeToBytes(signedTx) 397 response := ethapi.SignTransactionResult{Raw: rlpdata, Tx: signedTx} 398 399 // Finally, send the signed tx to the UI 400 api.UI.OnApprovedTx(response) 401 // ...and to the external caller 402 return &response, nil 403 404 } 405 406 // Sign calculates an Ethereum ECDSA signature for: 407 // keccack256("\x19Ethereum Signed Message:\n" + len(message) + message)) 408 // 409 // Note, the produced signature conforms to the secp256k1 curve R, S and V values, 410 // where the V value will be 27 or 28 for legacy reasons. 411 // 412 // The key used to calculate the signature is decrypted with the given password. 413 // 414 // https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_sign 415 func (api *SignerAPI) Sign(ctx context.Context, addr common.MixedcaseAddress, data hexutil.Bytes) (hexutil.Bytes, error) { 416 sighash, msg := SignHash(data) 417 // We make the request prior to looking up if we actually have the account, to prevent 418 // account-enumeration via the API 419 req := &SignDataRequest{Address: addr, Rawdata: data, Message: msg, Hash: sighash, Meta: MetadataFromContext(ctx)} 420 res, err := api.UI.ApproveSignData(req) 421 422 if err != nil { 423 return nil, err 424 } 425 if !res.Approved { 426 return nil, ErrRequestDenied 427 } 428 // Look up the wallet containing the requested signer 429 account := accounts.Account{Address: addr.Address()} 430 wallet, err := api.am.Find(account) 431 if err != nil { 432 return nil, err 433 } 434 // Assemble sign the data with the wallet 435 signature, err := wallet.SignHashWithPassphrase(account, res.Password, sighash) 436 if err != nil { 437 api.UI.ShowError(err.Error()) 438 return nil, err 439 } 440 signature[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper 441 return signature, nil 442 } 443 444 // SignHash is a helper function that calculates a hash for the given message that can be 445 // safely used to calculate a signature from. 446 // 447 // The hash is calculated as 448 // keccak256("\x19Ethereum Signed Message:\n"${message length}${message}). 449 // 450 // This gives context to the signed message and prevents signing of transactions. 451 func SignHash(data []byte) ([]byte, string) { 452 msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data) 453 return crypto.Keccak256([]byte(msg)), msg 454 } 455 456 // Export returns encrypted private key associated with the given address in web3 keystore format. 457 func (api *SignerAPI) Export(ctx context.Context, addr common.Address) (json.RawMessage, error) { 458 res, err := api.UI.ApproveExport(&ExportRequest{Address: addr, Meta: MetadataFromContext(ctx)}) 459 460 if err != nil { 461 return nil, err 462 } 463 if !res.Approved { 464 return nil, ErrRequestDenied 465 } 466 // Look up the wallet containing the requested signer 467 wallet, err := api.am.Find(accounts.Account{Address: addr}) 468 if err != nil { 469 return nil, err 470 } 471 if wallet.URL().Scheme != keystore.KeyStoreScheme { 472 return nil, fmt.Errorf("Account is not a keystore-account") 473 } 474 return ioutil.ReadFile(wallet.URL().Path) 475 } 476 477 // Import tries to import the given keyJSON in the local keystore. The keyJSON data is expected to be 478 // in web3 keystore format. It will decrypt the keyJSON with the given passphrase and on successful 479 // decryption it will encrypt the key with the given newPassphrase and store it in the keystore. 480 // OBS! This method is removed from the public API. It should not be exposed on the external API 481 // for a couple of reasons: 482 // 1. Even though it is encrypted, it should still be seen as sensitive data 483 // 2. It can be used to DoS clef, by using malicious data with e.g. extreme large 484 // values for the kdfparams. 485 func (api *SignerAPI) Import(ctx context.Context, keyJSON json.RawMessage) (Account, error) { 486 be := api.am.Backends(keystore.KeyStoreType) 487 488 if len(be) == 0 { 489 return Account{}, errors.New("password based accounts not supported") 490 } 491 res, err := api.UI.ApproveImport(&ImportRequest{Meta: MetadataFromContext(ctx)}) 492 493 if err != nil { 494 return Account{}, err 495 } 496 if !res.Approved { 497 return Account{}, ErrRequestDenied 498 } 499 acc, err := be[0].(*keystore.KeyStore).Import(keyJSON, res.OldPassword, res.NewPassword) 500 if err != nil { 501 api.UI.ShowError(err.Error()) 502 return Account{}, err 503 } 504 return Account{Typ: "Account", URL: acc.URL, Address: acc.Address}, nil 505 }