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",
|
Name: "stdio-ui-test",
|
||||||
Usage: "Mechanism to test interface between Clef and UI. Requires 'stdio-ui'.",
|
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{
|
initCommand = &cli.Command{
|
||||||
Action: initializeSecrets,
|
Action: initializeSecrets,
|
||||||
Name: "init",
|
Name: "init",
|
||||||
|
|
@ -283,6 +288,7 @@ func init() {
|
||||||
ruleFlag,
|
ruleFlag,
|
||||||
stdiouiFlag,
|
stdiouiFlag,
|
||||||
testFlag,
|
testFlag,
|
||||||
|
validateSIWEFlag,
|
||||||
advancedMode,
|
advancedMode,
|
||||||
acceptFlag,
|
acceptFlag,
|
||||||
}
|
}
|
||||||
|
|
@ -409,7 +415,7 @@ func initInternalApi(c *cli.Context) (*core.UIServerAPI, core.UIClientAPI, error
|
||||||
lightKdf = c.Bool(utils.LightKDFFlag.Name)
|
lightKdf = c.Bool(utils.LightKDFFlag.Name)
|
||||||
)
|
)
|
||||||
am := core.StartClefAccountManager(ksLoc, true, lightKdf, "")
|
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)
|
internalApi := core.NewUIServerAPI(api)
|
||||||
return internalApi, ui, nil
|
return internalApi, ui, nil
|
||||||
}
|
}
|
||||||
|
|
@ -694,18 +700,19 @@ func signer(c *cli.Context) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var (
|
var (
|
||||||
chainId = c.Int64(chainIdFlag.Name)
|
chainId = c.Int64(chainIdFlag.Name)
|
||||||
ksLoc = c.String(keystoreFlag.Name)
|
ksLoc = c.String(keystoreFlag.Name)
|
||||||
lightKdf = c.Bool(utils.LightKDFFlag.Name)
|
lightKdf = c.Bool(utils.LightKDFFlag.Name)
|
||||||
advanced = c.Bool(advancedMode.Name)
|
advanced = c.Bool(advancedMode.Name)
|
||||||
nousb = c.Bool(utils.NoUSBFlag.Name)
|
nousb = c.Bool(utils.NoUSBFlag.Name)
|
||||||
scpath = c.String(utils.SmartCardDaemonPathFlag.Name)
|
scpath = c.String(utils.SmartCardDaemonPathFlag.Name)
|
||||||
|
validateSIWEFlag = c.Bool(validateSIWEFlag.Name)
|
||||||
)
|
)
|
||||||
log.Info("Starting signer", "chainid", chainId, "keystore", ksLoc,
|
log.Info("Starting signer", "chainid", chainId, "keystore", ksLoc,
|
||||||
"light-kdf", lightKdf, "advanced", advanced)
|
"light-kdf", lightKdf, "advanced", advanced)
|
||||||
am := core.StartClefAccountManager(ksLoc, nousb, lightKdf, scpath)
|
am := core.StartClefAccountManager(ksLoc, nousb, lightKdf, scpath)
|
||||||
defer am.Close()
|
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
|
// Establish the bidirectional communication, by creating a new UI backend and registering
|
||||||
// it with the UI.
|
// it with the UI.
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,7 @@ type SignerAPI struct {
|
||||||
UI UIClientAPI
|
UI UIClientAPI
|
||||||
validator Validator
|
validator Validator
|
||||||
rejectMode bool
|
rejectMode bool
|
||||||
|
validateSIWEMode bool
|
||||||
credentials storage.Storage
|
credentials storage.Storage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -281,11 +282,11 @@ var ErrRequestDenied = errors.New("request denied")
|
||||||
// key that is generated when a new Account is created.
|
// key that is generated when a new Account is created.
|
||||||
// noUSB disables USB support that is required to support hardware devices such as
|
// noUSB disables USB support that is required to support hardware devices such as
|
||||||
// ledger and trezor.
|
// 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 {
|
if advancedMode {
|
||||||
log.Info("Clef is in advanced mode: will warn instead of reject")
|
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 {
|
if !noUSB {
|
||||||
signer.startUSBListener()
|
signer.startUSBListener()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -115,16 +115,17 @@ func tmpDirName(t *testing.T) string {
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
func setup(t *testing.T) (*core.SignerAPI, *headlessUi) {
|
func setup(t *testing.T, enableSIWE bool) (*core.SignerAPI, *headlessUi) {
|
||||||
db, err := fourbyte.New()
|
db, err := fourbyte.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err.Error())
|
t.Fatal(err.Error())
|
||||||
}
|
}
|
||||||
ui := &headlessUi{make(chan string, 20), make(chan string, 20)}
|
ui := &headlessUi{make(chan string, 20), make(chan string, 20)}
|
||||||
am := core.StartClefAccountManager(tmpDirName(t), true, true, "")
|
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
|
return api, ui
|
||||||
}
|
}
|
||||||
|
|
||||||
func createAccount(ui *headlessUi, api *core.SignerAPI, t *testing.T) {
|
func createAccount(ui *headlessUi, api *core.SignerAPI, t *testing.T) {
|
||||||
ui.approveCh <- "Y"
|
ui.approveCh <- "Y"
|
||||||
ui.inputCh <- "a_long_password"
|
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) {
|
func TestNewAcc(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
api, control := setup(t)
|
api, control := setup(t, false)
|
||||||
verifyNum := func(num int) {
|
verifyNum := func(num int) {
|
||||||
list, err := list(control, api, t)
|
list, err := list(control, api, t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -243,7 +244,7 @@ func TestSignTx(t *testing.T) {
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
api, control := setup(t)
|
api, control := setup(t, false)
|
||||||
createAccount(control, api, t)
|
createAccount(control, api, t)
|
||||||
control.approveCh <- "A"
|
control.approveCh <- "A"
|
||||||
list, err = api.List(context.Background())
|
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,
|
// 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.
|
// 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) {
|
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
|
// We make the request prior to looking up if we actually have the account, to prevent
|
||||||
// account-enumeration via the API
|
// account-enumeration via the API
|
||||||
res, err := api.UI.ApproveSignData(req)
|
res, err := api.UI.ApproveSignData(req)
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
"github.com/ethereum/go-ethereum/signer/core"
|
"github.com/ethereum/go-ethereum/signer/core"
|
||||||
"github.com/ethereum/go-ethereum/signer/core/apitypes"
|
"github.com/ethereum/go-ethereum/signer/core/apitypes"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
var typesStandard = apitypes.Types{
|
var typesStandard = apitypes.Types{
|
||||||
|
|
@ -184,7 +185,7 @@ var typedData = apitypes.TypedData{
|
||||||
|
|
||||||
func TestSignData(t *testing.T) {
|
func TestSignData(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
api, control := setup(t)
|
api, control := setup(t, false)
|
||||||
//Create two accounts
|
//Create two accounts
|
||||||
createAccount(control, api, t)
|
createAccount(control, api, t)
|
||||||
createAccount(control, api, t)
|
createAccount(control, api, t)
|
||||||
|
|
@ -1060,3 +1061,48 @@ func TestEncodeDataRecursiveBytes(t *testing.T) {
|
||||||
t.Fatalf("got err %v", err)
|
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