mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-23 13:16:42 +00:00
wip: starting to add tests
This commit is contained in:
parent
8a171dce1f
commit
324dce3f84
11 changed files with 193 additions and 15 deletions
|
|
@ -136,6 +136,11 @@ var (
|
|||
Name: "stdio-ui-test",
|
||||
Usage: "Mechanism to test interface between Clef and UI. Requires 'stdio-ui'.",
|
||||
}
|
||||
validateSIWEFlag = &cli.BoolFlag{
|
||||
Name: "validate-siwe",
|
||||
Usage: "Validate Sign-In with Etherium messages (ERC-4361)",
|
||||
Value: true,
|
||||
}
|
||||
initCommand = &cli.Command{
|
||||
Action: initializeSecrets,
|
||||
Name: "init",
|
||||
|
|
@ -283,6 +288,7 @@ func init() {
|
|||
ruleFlag,
|
||||
stdiouiFlag,
|
||||
testFlag,
|
||||
validateSIWEFlag,
|
||||
advancedMode,
|
||||
acceptFlag,
|
||||
}
|
||||
|
|
@ -409,7 +415,7 @@ func initInternalApi(c *cli.Context) (*core.UIServerAPI, core.UIClientAPI, error
|
|||
lightKdf = c.Bool(utils.LightKDFFlag.Name)
|
||||
)
|
||||
am := core.StartClefAccountManager(ksLoc, true, lightKdf, "")
|
||||
api := core.NewSignerAPI(am, 0, true, ui, nil, false, pwStorage)
|
||||
api := core.NewSignerAPI(am, 0, true, ui, nil, false, pwStorage, false)
|
||||
internalApi := core.NewUIServerAPI(api)
|
||||
return internalApi, ui, nil
|
||||
}
|
||||
|
|
@ -700,12 +706,13 @@ func signer(c *cli.Context) error {
|
|||
advanced = c.Bool(advancedMode.Name)
|
||||
nousb = c.Bool(utils.NoUSBFlag.Name)
|
||||
scpath = c.String(utils.SmartCardDaemonPathFlag.Name)
|
||||
validateSIWEFlag = c.Bool(validateSIWEFlag.Name)
|
||||
)
|
||||
log.Info("Starting signer", "chainid", chainId, "keystore", ksLoc,
|
||||
"light-kdf", lightKdf, "advanced", advanced)
|
||||
am := core.StartClefAccountManager(ksLoc, nousb, lightKdf, scpath)
|
||||
defer am.Close()
|
||||
apiImpl := core.NewSignerAPI(am, chainId, nousb, ui, db, advanced, pwStorage)
|
||||
apiImpl := core.NewSignerAPI(am, chainId, nousb, ui, db, advanced, pwStorage, validateSIWEFlag)
|
||||
|
||||
// Establish the bidirectional communication, by creating a new UI backend and registering
|
||||
// it with the UI.
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ type SignerAPI struct {
|
|||
UI UIClientAPI
|
||||
validator Validator
|
||||
rejectMode bool
|
||||
validateSIWEMode bool
|
||||
credentials storage.Storage
|
||||
}
|
||||
|
||||
|
|
@ -281,11 +282,11 @@ var ErrRequestDenied = errors.New("request denied")
|
|||
// key that is generated when a new Account is created.
|
||||
// noUSB disables USB support that is required to support hardware devices such as
|
||||
// ledger and trezor.
|
||||
func NewSignerAPI(am *accounts.Manager, chainID int64, noUSB bool, ui UIClientAPI, validator Validator, advancedMode bool, credentials storage.Storage) *SignerAPI {
|
||||
func NewSignerAPI(am *accounts.Manager, chainID int64, noUSB bool, ui UIClientAPI, validator Validator, advancedMode bool, credentials storage.Storage, validateSIWEMode bool) *SignerAPI {
|
||||
if advancedMode {
|
||||
log.Info("Clef is in advanced mode: will warn instead of reject")
|
||||
}
|
||||
signer := &SignerAPI{big.NewInt(chainID), am, ui, validator, !advancedMode, credentials}
|
||||
signer := &SignerAPI{big.NewInt(chainID), am, ui, validator, !advancedMode, validateSIWEMode, credentials}
|
||||
if !noUSB {
|
||||
signer.startUSBListener()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,16 +115,17 @@ func tmpDirName(t *testing.T) string {
|
|||
return d
|
||||
}
|
||||
|
||||
func setup(t *testing.T) (*core.SignerAPI, *headlessUi) {
|
||||
func setup(t *testing.T, enableSIWE bool) (*core.SignerAPI, *headlessUi) {
|
||||
db, err := fourbyte.New()
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
ui := &headlessUi{make(chan string, 20), make(chan string, 20)}
|
||||
am := core.StartClefAccountManager(tmpDirName(t), true, true, "")
|
||||
api := core.NewSignerAPI(am, 1337, true, ui, db, true, &storage.NoStorage{})
|
||||
api := core.NewSignerAPI(am, 1337, true, ui, db, true, &storage.NoStorage{}, enableSIWE)
|
||||
return api, ui
|
||||
}
|
||||
|
||||
func createAccount(ui *headlessUi, api *core.SignerAPI, t *testing.T) {
|
||||
ui.approveCh <- "Y"
|
||||
ui.inputCh <- "a_long_password"
|
||||
|
|
@ -170,7 +171,7 @@ func list(ui *headlessUi, api *core.SignerAPI, t *testing.T) ([]common.Address,
|
|||
|
||||
func TestNewAcc(t *testing.T) {
|
||||
t.Parallel()
|
||||
api, control := setup(t)
|
||||
api, control := setup(t, false)
|
||||
verifyNum := func(num int) {
|
||||
list, err := list(control, api, t)
|
||||
if err != nil {
|
||||
|
|
@ -243,7 +244,7 @@ func TestSignTx(t *testing.T) {
|
|||
err error
|
||||
)
|
||||
|
||||
api, control := setup(t)
|
||||
api, control := setup(t, false)
|
||||
createAccount(control, api, t)
|
||||
control.approveCh <- "A"
|
||||
list, err = api.List(context.Background())
|
||||
|
|
|
|||
|
|
@ -38,6 +38,17 @@ import (
|
|||
// Note, the produced signature conforms to the secp256k1 curve R, S and V values,
|
||||
// where the V value will be 27 or 28 for legacy reasons, if legacyV==true.
|
||||
func (api *SignerAPI) sign(req *SignDataRequest, legacyV bool) (hexutil.Bytes, error) {
|
||||
if api.validateSIWEMode {
|
||||
// perform SIWE validation
|
||||
err := validateSIWE(req)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrMalformedSIWEMEssage) {
|
||||
api.UI.ShowInfo(err.Error())
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
// We make the request prior to looking up if we actually have the account, to prevent
|
||||
// account-enumeration via the API
|
||||
res, err := api.UI.ApproveSignData(req)
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/signer/core"
|
||||
"github.com/ethereum/go-ethereum/signer/core/apitypes"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var typesStandard = apitypes.Types{
|
||||
|
|
@ -184,7 +185,7 @@ var typedData = apitypes.TypedData{
|
|||
|
||||
func TestSignData(t *testing.T) {
|
||||
t.Parallel()
|
||||
api, control := setup(t)
|
||||
api, control := setup(t, false)
|
||||
//Create two accounts
|
||||
createAccount(control, api, t)
|
||||
createAccount(control, api, t)
|
||||
|
|
@ -1060,3 +1061,48 @@ func TestEncodeDataRecursiveBytes(t *testing.T) {
|
|||
t.Fatalf("got err %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignInWithEtherium(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testFiles, err := os.ReadDir(filepath.Join("testdata", "siwe"))
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, fInfo := range testFiles {
|
||||
if !strings.HasSuffix(fInfo.Name(), "txt") {
|
||||
continue
|
||||
}
|
||||
t.Run(fInfo.Name(), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
expectedFailure := strings.HasPrefix(fInfo.Name(), "expfail")
|
||||
expectedWarning := strings.HasPrefix(fInfo.Name(), "expwarn")
|
||||
message, err := os.ReadFile(filepath.Join("testdata", "siwe", fInfo.Name()))
|
||||
require.NoError(t, err)
|
||||
|
||||
api, control := setup(t, true)
|
||||
createAccount(control, api, t)
|
||||
createAccount(control, api, t)
|
||||
control.approveCh <- "1"
|
||||
list, err := api.List(context.Background())
|
||||
require.NoError(t, err)
|
||||
a := common.NewMixedcaseAddress(list[0])
|
||||
|
||||
control.approveCh <- "Y"
|
||||
control.inputCh <- "a_long_password"
|
||||
signature, err := api.SignData(context.Background(), apitypes.TextPlain.Mime, a, hexutil.Encode([]byte(message)))
|
||||
|
||||
if expectedFailure {
|
||||
require.Error(t, err)
|
||||
return
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
if expectedWarning {
|
||||
// todo: monitor for warning
|
||||
}
|
||||
if signature == nil || len(signature) != 65 {
|
||||
t.Errorf("Expected 65 byte signature (got %d bytes)", len(signature))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
107
signer/core/siwevalidation.go
Normal file
107
signer/core/siwevalidation.go
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
// Copyright 2025 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Regular expression to match SIWE messages
|
||||
// Scheme (optional)
|
||||
var scheme = `(?:[a-zA-Z][a-zA-Z0-9+\-.]*://)?`
|
||||
|
||||
// Domain
|
||||
var domain = `(?:[^ ]+)`
|
||||
|
||||
// Static message
|
||||
var wantsMsg = ` wants you to sign in with your Ethereum account:\n`
|
||||
|
||||
// Ethereum address (with basic 0x prefix + 40 hex digits)
|
||||
var address = `0x[a-fA-F0-9]{40}\n`
|
||||
|
||||
// Optional statement (any line not containing "\n\n")
|
||||
var statement = `(?:[^\n]+\n)?`
|
||||
|
||||
// URI
|
||||
var uri = `URI: (.+)\n`
|
||||
|
||||
// Version
|
||||
var version = `Version: 1\n`
|
||||
|
||||
// Chain ID
|
||||
var chainID = `Chain ID: (\d+)\n`
|
||||
|
||||
// Nonce (min 8 alphanumeric characters)
|
||||
var nonce = `Nonce: [a-zA-Z0-9]{8,}\n`
|
||||
|
||||
// Issued At (RFC 3339 date-time)
|
||||
var issuedAt = `Issued At: ([0-9T:\-+.Z]+)`
|
||||
|
||||
// Optional Expiration Time
|
||||
var expiration = `(?:\nExpiration Time: ([0-9T:\-+.Z]+))?`
|
||||
|
||||
// Optional Not Before
|
||||
var notBefore = `(?:\nNot Before: ([0-9T:\-+.Z]+))?`
|
||||
|
||||
// Optional Request ID
|
||||
var requestID = `(?:\nRequest ID: ([^\n]+))?`
|
||||
|
||||
// Optional Resources
|
||||
var resources = `(?:\nResources:(?:\n- .+)+)?`
|
||||
|
||||
// SIWE Message Regex
|
||||
var siweMessageRegex = regexp.MustCompile(`(?m)^` + scheme + domain + wantsMsg +
|
||||
address + `\n` + statement + `\n` +
|
||||
uri + version + chainID + nonce + issuedAt +
|
||||
expiration + notBefore + requestID + resources + `$`)
|
||||
|
||||
var ErrMalformedSIWEMEssage = errors.New("the message is asking to sign in with Ethereum but does not conform to EIP-4361")
|
||||
|
||||
func validateSIWE(req *SignDataRequest) error {
|
||||
for _, message := range req.Messages {
|
||||
s, ok := message.Value.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(s, "wants you to sign in with your Ethereum account") {
|
||||
continue
|
||||
}
|
||||
patterns := siweMessageRegex.FindStringSubmatch(s)
|
||||
if len(patterns) != 14 {
|
||||
return ErrMalformedSIWEMEssage
|
||||
}
|
||||
scheme := "https"
|
||||
if patterns[0] != "" {
|
||||
scheme = patterns[0]
|
||||
}
|
||||
if err := validateDomain(req, scheme, patterns[1]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDomain(request *SignDataRequest, scheme, domain string) error {
|
||||
siweOrigin := fmt.Sprintf("%s://%s", scheme, domain)
|
||||
if siweOrigin != request.Meta.Origin {
|
||||
return fmt.Errorf("sign in request domain (%s) does not match source: %s", siweOrigin, request.Meta.Origin)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
1
signer/core/testdata/siwe/eip1271.txt
vendored
Normal file
1
signer/core/testdata/siwe/eip1271.txt
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
localhost:4361 wants you to sign in with your Ethereum account:\n0xa5b3A53800cD49669F34DE80f2C569c6D4Ca3009\n\nSIWE Notepad Example\n\nURI: http://localhost:4361\nVersion: 1\nChain ID: 1\nNonce: FbYd6TNB4m0IUHDG7\nIssued At: 2022-04-19T18:55:04.444Z
|
||||
1
signer/core/testdata/siwe/eip1271_2.txt
vendored
Normal file
1
signer/core/testdata/siwe/eip1271_2.txt
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
localhost:4361 wants you to sign in with your Ethereum account:\n0x0e565A6dFc43DE21455a67bbF196f7F7b15447A7\n\nSIWE Notepad Example\n\nURI: http://localhost:4361\nVersion: 1\nChain ID: 1\nNonce: b19JyMHnM0Jdm20as\nIssued At: 2022-04-19T18:57:09.490Z
|
||||
1
signer/core/testdata/siwe/expwarn_malformed_1.txt
vendored
Normal file
1
signer/core/testdata/siwe/expwarn_malformed_1.txt
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
wants you to sign in with your Ethereum account
|
||||
1
signer/core/testdata/siwe/no_optional_fields.txt.
vendored
Normal file
1
signer/core/testdata/siwe/no_optional_fields.txt.
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
service.org wants you to sign in with your Ethereum account:\n0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nNonce: 32891757\nIssued At: 2021-09-30T16:25:24.000Z
|
||||
1
signer/core/testdata/siwe/with_optional_fields.txt
vendored
Normal file
1
signer/core/testdata/siwe/with_optional_fields.txt
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
service.org wants you to sign in with your Ethereum account:\n0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nNonce: 32891757\nIssued At: 2021-09-30T16:25:24.000Z\nResources:\n- ipfs://Qme7ss3ARVgxv6rXqVPiikMJ8u2NLgmgszg13pYrDKEoiu\n- https://example.com/my-web2-claim.json
|
||||
Loading…
Reference in a new issue