github.com/core-coin/go-core/v2@v2.1.9/signer/core/api.go (about) 1 // Copyright 2018 by the Authors 2 // This file is part of the go-core library. 3 // 4 // The go-core library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser 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 // The go-core library 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 Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the go-core library. 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 "math/big" 25 "reflect" 26 27 "github.com/core-coin/go-core/v2/internal/xcbapi" 28 "github.com/core-coin/go-core/v2/rpc" 29 30 "github.com/core-coin/go-core/v2/accounts" 31 "github.com/core-coin/go-core/v2/accounts/keystore" 32 "github.com/core-coin/go-core/v2/common" 33 "github.com/core-coin/go-core/v2/common/hexutil" 34 "github.com/core-coin/go-core/v2/log" 35 "github.com/core-coin/go-core/v2/rlp" 36 "github.com/core-coin/go-core/v2/signer/storage" 37 ) 38 39 const ( 40 // numberOfAccountsToDerive For hardware wallets, the number of accounts to derive 41 numberOfAccountsToDerive = 10 42 // ExternalAPIVersion -- see extapi_changelog.md 43 ExternalAPIVersion = "6.1.0" 44 // InternalAPIVersion -- see intapi_changelog.md 45 InternalAPIVersion = "7.0.1" 46 ) 47 48 // ExternalAPI defines the external API through which signing requests are made. 49 type ExternalAPI interface { 50 // List available accounts 51 List(ctx context.Context) ([]common.Address, error) 52 // New request to create a new account 53 New(ctx context.Context) (common.Address, error) 54 // SignTransaction request to sign the specified transaction 55 SignTransaction(ctx context.Context, args SendTxArgs, methodSelector *string) (*xcbapi.SignTransactionResult, error) 56 // SignData - request to sign the given data (plus prefix) 57 SignData(ctx context.Context, contentType string, addr common.Address, data interface{}) (hexutil.Bytes, error) 58 // SignTypedData - request to sign the given structured data (plus prefix) 59 SignTypedData(ctx context.Context, addr common.Address, data TypedData) (hexutil.Bytes, error) 60 // EcRecover - recover public key from given message and signature 61 EcRecover(ctx context.Context, data hexutil.Bytes, sig hexutil.Bytes) (common.Address, error) 62 // Version info about the APIs 63 Version(ctx context.Context) (string, error) 64 // SignGnosisSafeTransaction signs/confirms a gnosis-safe multisig transaction 65 SignGnosisSafeTx(ctx context.Context, signerAddress common.Address, gnosisTx GnosisSafeTx, methodSelector *string) (*GnosisSafeTx, error) 66 } 67 68 // UIClientAPI specifies what method a UI needs to implement to be able to be used as a 69 // UI for the signer 70 type UIClientAPI interface { 71 // ApproveTx prompt the user for confirmation to request to sign Transaction 72 ApproveTx(request *SignTxRequest) (SignTxResponse, error) 73 // ApproveSignData prompt the user for confirmation to request to sign data 74 ApproveSignData(request *SignDataRequest) (SignDataResponse, error) 75 // ApproveListing prompt the user for confirmation to list accounts 76 // the list of accounts to list can be modified by the UI 77 ApproveListing(request *ListRequest) (ListResponse, error) 78 // ApproveNewAccount prompt the user for confirmation to create new Account, and reveal to caller 79 ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error) 80 // ShowError displays error message to user 81 ShowError(message string) 82 // ShowInfo displays info message to user 83 ShowInfo(message string) 84 // OnApprovedTx notifies the UI about a transaction having been successfully signed. 85 // This method can be used by a UI to keep track of e.g. how much has been sent to a particular recipient. 86 OnApprovedTx(tx xcbapi.SignTransactionResult) 87 // OnSignerStartup is invoked when the signer boots, and tells the UI info about external API location and version 88 // information 89 OnSignerStartup(info StartupInfo) 90 // OnInputRequired is invoked when clef requires user input, for example master password or 91 // pin-code for unlocking hardware wallets 92 OnInputRequired(info UserInputRequest) (UserInputResponse, error) 93 // RegisterUIServer tells the UI to use the given UIServerAPI for ui->clef communication 94 RegisterUIServer(api *UIServerAPI) 95 } 96 97 // Validator defines the methods required to validate a transaction against some 98 // sanity defaults as well as any underlying 4byte method database. 99 // 100 // Use fourbyte.Database as an implementation. It is separated out of this package 101 // to allow pieces of the signer package to be used without having to load the 102 // 7MB embedded 4byte dump. 103 type Validator interface { 104 // ValidateTransaction does a number of checks on the supplied transaction, and 105 // returns either a list of warnings, or an error (indicating that the transaction 106 // should be immediately rejected). 107 ValidateTransaction(selector *string, tx *SendTxArgs) (*ValidationMessages, error) 108 } 109 110 // SignerAPI defines the actual implementation of ExternalAPI 111 type SignerAPI struct { 112 networkID *big.Int 113 am *accounts.Manager 114 UI UIClientAPI 115 validator Validator 116 rejectMode bool 117 credentials storage.Storage 118 } 119 120 // Metadata about a request 121 type Metadata struct { 122 Remote string `json:"remote"` 123 Local string `json:"local"` 124 Scheme string `json:"scheme"` 125 UserAgent string `json:"User-Agent"` 126 Origin string `json:"Origin"` 127 } 128 129 func StartClefAccountManager(ksLocation string, lightKDF bool) *accounts.Manager { 130 var ( 131 backends []accounts.Backend 132 n, p = keystore.StandardScryptN, keystore.StandardScryptP 133 ) 134 if lightKDF { 135 n, p = keystore.LightScryptN, keystore.LightScryptP 136 } 137 // support password based accounts 138 if len(ksLocation) > 0 { 139 backends = append(backends, keystore.NewKeyStore(ksLocation, n, p)) 140 } 141 142 // Clef doesn't allow insecure http account unlock. 143 return accounts.NewManager(&accounts.Config{InsecureUnlockAllowed: false}, backends...) 144 } 145 146 // MetadataFromContext extracts Metadata from a given context.Context 147 func MetadataFromContext(ctx context.Context) Metadata { 148 info := rpc.PeerInfoFromContext(ctx) 149 150 m := Metadata{"NA", "NA", "NA", "", ""} // batman 151 152 if info.Transport != "" { 153 if info.Transport == "http" { 154 m.Scheme = info.HTTP.Version 155 } 156 m.Scheme = info.Transport 157 } 158 if info.RemoteAddr != "" { 159 m.Remote = info.RemoteAddr 160 } 161 if info.HTTP.Host != "" { 162 m.Local = info.HTTP.Host 163 } 164 m.Origin = info.HTTP.Origin 165 m.UserAgent = info.HTTP.UserAgent 166 return m 167 } 168 169 // String implements Stringer interface 170 func (m Metadata) String() string { 171 s, err := json.Marshal(m) 172 if err == nil { 173 return string(s) 174 } 175 return err.Error() 176 } 177 178 // types for the requests/response types between signer and UI 179 type ( 180 // SignTxRequest contains info about a Transaction to sign 181 SignTxRequest struct { 182 Transaction SendTxArgs `json:"transaction"` 183 Callinfo []ValidationInfo `json:"call_info"` 184 Meta Metadata `json:"meta"` 185 } 186 // SignTxResponse result from SignTxRequest 187 SignTxResponse struct { 188 //The UI may make changes to the TX 189 Transaction SendTxArgs `json:"transaction"` 190 Approved bool `json:"approved"` 191 } 192 SignDataRequest struct { 193 ContentType string `json:"content_type"` 194 Address common.Address `json:"address"` 195 Rawdata []byte `json:"raw_data"` 196 Messages []*NameValueType `json:"messages"` 197 Callinfo []ValidationInfo `json:"call_info"` 198 Hash hexutil.Bytes `json:"hash"` 199 Meta Metadata `json:"meta"` 200 } 201 SignDataResponse struct { 202 Approved bool `json:"approved"` 203 } 204 NewAccountRequest struct { 205 Meta Metadata `json:"meta"` 206 } 207 NewAccountResponse struct { 208 Approved bool `json:"approved"` 209 } 210 ListRequest struct { 211 Accounts []accounts.Account `json:"accounts"` 212 Meta Metadata `json:"meta"` 213 } 214 ListResponse struct { 215 Accounts []accounts.Account `json:"accounts"` 216 } 217 Message struct { 218 Text string `json:"text"` 219 } 220 StartupInfo struct { 221 Info map[string]interface{} `json:"info"` 222 } 223 UserInputRequest struct { 224 Title string `json:"title"` 225 Prompt string `json:"prompt"` 226 IsPassword bool `json:"isPassword"` 227 } 228 UserInputResponse struct { 229 Text string `json:"text"` 230 } 231 ) 232 233 var ErrRequestDenied = errors.New("request denied") 234 235 // NewSignerAPI creates a new API that can be used for Account management. 236 // ksLocation specifies the directory where to store the password protected private 237 // key that is generated when a new Account is created. 238 func NewSignerAPI(am *accounts.Manager, networkID int64, ui UIClientAPI, validator Validator, advancedMode bool, credentials storage.Storage) *SignerAPI { 239 if advancedMode { 240 log.Info("Clef is in advanced mode: will warn instead of reject") 241 } 242 signer := &SignerAPI{big.NewInt(networkID), am, ui, validator, !advancedMode, credentials} 243 return signer 244 } 245 246 // derivationLoop listens for wallet events 247 func (api *SignerAPI) derivationLoop(events chan accounts.WalletEvent) { 248 // Listen for wallet event till termination 249 for event := range events { 250 switch event.Kind { 251 case accounts.WalletArrived: 252 if err := event.Wallet.Open(""); err != nil { 253 log.Warn("New wallet appeared, failed to open", "url", event.Wallet.URL(), "err", err) 254 } 255 case accounts.WalletOpened: 256 status, _ := event.Wallet.Status() 257 log.Info("New wallet appeared", "url", event.Wallet.URL(), "status", status) 258 var derive = func(limit int, next func() accounts.DerivationPath) { 259 // Derive first N accounts, hardcoded for now 260 for i := 0; i < limit; i++ { 261 path := next() 262 if acc, err := event.Wallet.Derive(path, true); err != nil { 263 log.Warn("Account derivation failed", "error", err) 264 } else { 265 log.Info("Derived account", "address", acc.Address, "path", path) 266 } 267 } 268 } 269 log.Info("Deriving default paths") 270 derive(numberOfAccountsToDerive, accounts.DefaultIterator(accounts.DefaultBaseDerivationPath)) 271 case accounts.WalletDropped: 272 log.Info("Old wallet dropped", "url", event.Wallet.URL()) 273 event.Wallet.Close() 274 } 275 } 276 } 277 278 // List returns the set of wallet this signer manages. Each wallet can contain 279 // multiple accounts. 280 func (api *SignerAPI) List(ctx context.Context) ([]common.Address, error) { 281 var accs = make([]accounts.Account, 0) 282 // accs is initialized as empty list, not nil. We use 'nil' to signal 283 // rejection, as opposed to an empty list. 284 for _, wallet := range api.am.Wallets() { 285 accs = append(accs, wallet.Accounts()...) 286 } 287 result, err := api.UI.ApproveListing(&ListRequest{Accounts: accs, Meta: MetadataFromContext(ctx)}) 288 if err != nil { 289 return nil, err 290 } 291 if result.Accounts == nil { 292 return nil, ErrRequestDenied 293 } 294 addresses := make([]common.Address, 0) 295 for _, acc := range result.Accounts { 296 addresses = append(addresses, acc.Address) 297 } 298 return addresses, nil 299 } 300 301 // New creates a new password protected Account. The private key is protected with 302 // the given password. Users are responsible to backup the private key that is stored 303 // in the keystore location thas was specified when this API was created. 304 func (api *SignerAPI) New(ctx context.Context) (common.Address, error) { 305 if be := api.am.Backends(keystore.KeyStoreType); len(be) == 0 { 306 return common.Address{}, errors.New("password based accounts not supported") 307 } 308 if resp, err := api.UI.ApproveNewAccount(&NewAccountRequest{MetadataFromContext(ctx)}); err != nil { 309 return common.Address{}, err 310 } else if !resp.Approved { 311 return common.Address{}, ErrRequestDenied 312 } 313 return api.newAccount() 314 } 315 316 // newAccount is the internal method to create a new account. It should be used 317 // _after_ user-approval has been obtained 318 func (api *SignerAPI) newAccount() (common.Address, error) { 319 be := api.am.Backends(keystore.KeyStoreType) 320 if len(be) == 0 { 321 return common.Address{}, errors.New("password based accounts not supported") 322 } 323 // Three retries to get a valid password 324 for i := 0; i < 3; i++ { 325 resp, err := api.UI.OnInputRequired(UserInputRequest{ 326 "New account password", 327 fmt.Sprintf("Please enter a password for the new account to be created (attempt %d of 3)", i), 328 true}) 329 if err != nil { 330 log.Warn("error obtaining password", "attempt", i, "error", err) 331 continue 332 } 333 if pwErr := ValidatePasswordFormat(resp.Text); pwErr != nil { 334 api.UI.ShowError(fmt.Sprintf("Account creation attempt #%d failed due to password requirements: %v", (i + 1), pwErr)) 335 } else { 336 // No error 337 acc, err := be[0].(*keystore.KeyStore).NewAccount(resp.Text) 338 log.Info("Your new key was generated", "address", acc.Address) 339 log.Warn("Please backup your key file!", "path", acc.URL.Path) 340 log.Warn("Please remember your password!") 341 return acc.Address, err 342 } 343 } 344 // Otherwise fail, with generic error message 345 return common.Address{}, errors.New("account creation failed") 346 } 347 348 // logDiff logs the difference between the incoming (original) transaction and the one returned from the signer. 349 // it also returns 'true' if the transaction was modified, to make it possible to configure the signer not to allow 350 // UI-modifications to requests 351 func logDiff(original *SignTxRequest, new *SignTxResponse) bool { 352 modified := false 353 if f0, f1 := original.Transaction.From, new.Transaction.From; !reflect.DeepEqual(f0, f1) { 354 log.Info("Sender-account changed by UI", "was", f0, "is", f1) 355 modified = true 356 } 357 if t0, t1 := original.Transaction.To, new.Transaction.To; !reflect.DeepEqual(t0, t1) { 358 log.Info("Recipient-account changed by UI", "was", t0, "is", t1) 359 modified = true 360 } 361 if g0, g1 := original.Transaction.Energy, new.Transaction.Energy; g0 != g1 { 362 modified = true 363 log.Info("Energy changed by UI", "was", g0, "is", g1) 364 } 365 if g0, g1 := big.Int(original.Transaction.EnergyPrice), big.Int(new.Transaction.EnergyPrice); g0.Cmp(&g1) != 0 { 366 modified = true 367 log.Info("EnergyPrice changed by UI", "was", g0, "is", g1) 368 } 369 if v0, v1 := big.Int(original.Transaction.Value), big.Int(new.Transaction.Value); v0.Cmp(&v1) != 0 { 370 modified = true 371 log.Info("Value changed by UI", "was", v0, "is", v1) 372 } 373 if d0, d1 := original.Transaction.Data, new.Transaction.Data; d0 != d1 { 374 d0s := "" 375 d1s := "" 376 if d0 != nil { 377 d0s = hexutil.Encode(*d0) 378 } 379 if d1 != nil { 380 d1s = hexutil.Encode(*d1) 381 } 382 if d1s != d0s { 383 modified = true 384 log.Info("Data changed by UI", "was", d0s, "is", d1s) 385 } 386 } 387 if n0, n1 := original.Transaction.Nonce, new.Transaction.Nonce; n0 != n1 { 388 modified = true 389 log.Info("Nonce changed by UI", "was", n0, "is", n1) 390 } 391 return modified 392 } 393 394 func (api *SignerAPI) lookupPassword(address common.Address) (string, error) { 395 return api.credentials.Get(address.Hex()) 396 } 397 398 func (api *SignerAPI) lookupOrQueryPassword(address common.Address, title, prompt string) (string, error) { 399 // Look up the password and return if available 400 if pw, err := api.lookupPassword(address); err == nil { 401 return pw, nil 402 } 403 // Password unavailable, request it from the user 404 pwResp, err := api.UI.OnInputRequired(UserInputRequest{title, prompt, true}) 405 if err != nil { 406 log.Warn("error obtaining password", "error", err) 407 // We'll not forward the error here, in case the error contains info about the response from the UI, 408 // which could leak the password if it was malformed json or something 409 return "", errors.New("internal error") 410 } 411 return pwResp.Text, nil 412 } 413 414 // SignTransaction signs the given Transaction and returns it both as json and rlp-encoded form 415 func (api *SignerAPI) SignTransaction(ctx context.Context, args SendTxArgs, methodSelector *string) (*xcbapi.SignTransactionResult, error) { 416 var ( 417 err error 418 result SignTxResponse 419 ) 420 msgs, err := api.validator.ValidateTransaction(methodSelector, &args) 421 if err != nil { 422 return nil, err 423 } 424 // If we are in 'rejectMode', then reject rather than show the user warnings 425 if api.rejectMode { 426 if err := msgs.getWarnings(); err != nil { 427 return nil, err 428 } 429 } 430 req := SignTxRequest{ 431 Transaction: args, 432 Meta: MetadataFromContext(ctx), 433 Callinfo: msgs.Messages, 434 } 435 // Process approval 436 result, err = api.UI.ApproveTx(&req) 437 if err != nil { 438 return nil, err 439 } 440 if !result.Approved { 441 return nil, ErrRequestDenied 442 } 443 // Log changes made by the UI to the signing-request 444 logDiff(&req, &result) 445 var ( 446 acc accounts.Account 447 wallet accounts.Wallet 448 ) 449 acc = accounts.Account{Address: result.Transaction.From} 450 wallet, err = api.am.Find(acc) 451 if err != nil { 452 return nil, err 453 } 454 // Convert fields into a real transaction 455 var unsignedTx = result.Transaction.toTransaction() 456 // Get the password for the transaction 457 pw, err := api.lookupOrQueryPassword(acc.Address, "Account password", 458 fmt.Sprintf("Please enter the password for account %s", acc.Address.String())) 459 if err != nil { 460 return nil, err 461 } 462 // The one to sign is the one that was returned from the UI 463 signedTx, err := wallet.SignTxWithPassphrase(acc, pw, unsignedTx, api.networkID) 464 if err != nil { 465 api.UI.ShowError(err.Error()) 466 return nil, err 467 } 468 469 rlpdata, err := rlp.EncodeToBytes(signedTx) 470 if err != nil { 471 return nil, err 472 } 473 response := xcbapi.SignTransactionResult{Raw: rlpdata, Tx: signedTx} 474 475 // Finally, send the signed tx to the UI 476 api.UI.OnApprovedTx(response) 477 // ...and to the external caller 478 return &response, nil 479 480 } 481 482 func (api *SignerAPI) SignGnosisSafeTx(ctx context.Context, signerAddress common.Address, gnosisTx GnosisSafeTx, methodSelector *string) (*GnosisSafeTx, error) { 483 // Do the usual validations, but on the last-stage transaction 484 args := gnosisTx.ArgsForValidation() 485 msgs, err := api.validator.ValidateTransaction(methodSelector, args) 486 if err != nil { 487 return nil, err 488 } 489 // If we are in 'rejectMode', then reject rather than show the user warnings 490 if api.rejectMode { 491 if err := msgs.getWarnings(); err != nil { 492 return nil, err 493 } 494 } 495 typedData := gnosisTx.ToTypedData() 496 signature, preimage, err := api.signTypedData(ctx, signerAddress, typedData, msgs) 497 if err != nil { 498 return nil, err 499 } 500 501 gnosisTx.Signature = signature 502 gnosisTx.SafeTxHash = common.BytesToHash(preimage) 503 gnosisTx.Sender = signerAddress 504 505 return &gnosisTx, nil 506 } 507 508 // Returns the external api version. This method does not require user acceptance. Available methods are 509 // available via enumeration anyway, and this info does not contain user-specific data 510 func (api *SignerAPI) Version(ctx context.Context) (string, error) { 511 return ExternalAPIVersion, nil 512 }