wip: starting to add tests

This commit is contained in:
shwoop 2025-06-25 17:08:59 +02:00
parent 8a171dce1f
commit 324dce3f84
No known key found for this signature in database
GPG key ID: 95A35B68C1E5F95A
11 changed files with 193 additions and 15 deletions

View file

@ -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.

View file

@ -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()
} }

View file

@ -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())

View file

@ -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)

View file

@ -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))
}
})
}
}

View 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
View 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

View 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

View file

@ -0,0 +1 @@
wants you to sign in with your Ethereum account

View 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

View 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